Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion apps/ui/locales/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,8 @@
"features": {
"searchPlaceholder": "Search features...",
"loadMore": "See more",
"viewFeature": "Learn more"
"viewFeature": "Learn more",
"filterTagsLabel": "Categories"
},
"blog": {
"title": "Blog",
Expand Down
56 changes: 11 additions & 45 deletions apps/ui/src/components/case-study/CaseStudiesGrid.tsx
Original file line number Diff line number Diff line change
@@ -1,14 +1,11 @@
"use client"

import { MagnifyingGlassIcon } from "@phosphor-icons/react/ssr"
import type { Data } from "@repo/strapi-types"
import { useTranslations } from "next-intl"
import { useEffect, useMemo, useRef, useState, useTransition } from "react"

import { SearchFilterSidebar } from "@/components/elementary/SearchFilterSidebar"
import { Button } from "@/components/ui/button"
import { Checkbox } from "@/components/ui/checkbox"
import { Input } from "@/components/ui/input"
import { Label } from "@/components/ui/label"
import { cn } from "@/lib/styles"

import type {
Expand Down Expand Up @@ -148,47 +145,16 @@ export function CaseStudiesGrid({

return (
<div className={cn("flex flex-col gap-8 lg:flex-row", className)}>
<aside className="flex flex-col gap-6 lg:w-1/4 lg:shrink-0">
<div className="relative">
<MagnifyingGlassIcon className="text-muted-foreground absolute top-1/2 left-3 size-4 -translate-y-1/2" />
<Input
placeholder={t("searchPlaceholder")}
value={query}
onChange={(e) => setQuery(e.target.value)}
className="pl-9"
/>
</div>

{categoryOptions.length > 0 && (
<div className="flex flex-col gap-3">
<p className="text-foreground text-sm font-semibold tracking-[0.5px] uppercase">
{t("filterCategoriesLabel")}
</p>

<div className="flex flex-col gap-2">
{categoryOptions.map((option) => {
const isChecked = selectedCategories.has(option.value)

return (
<div key={option.value} className="flex items-center gap-2">
<Checkbox
id={`case-study-category-${option.value}`}
checked={isChecked}
onCheckedChange={() => toggleCategory(option.value)}
/>
<Label
htmlFor={`case-study-category-${option.value}`}
className="cursor-pointer text-base font-normal"
>
{option.label}
</Label>
</div>
)
})}
</div>
</div>
)}
</aside>
<SearchFilterSidebar
query={query}
onQueryChange={setQuery}
searchPlaceholder={t("searchPlaceholder")}
filterLabel={t("filterCategoriesLabel")}
filterOptions={categoryOptions}
selectedValues={selectedCategories}
onToggleValue={toggleCategory}
idPrefix="case-study-category"
/>

<div className="flex min-w-0 flex-1 flex-col gap-8">
<div className="grid grid-cols-1 gap-6 md:grid-cols-2 lg:grid-cols-3">
Expand Down
43 changes: 42 additions & 1 deletion apps/ui/src/components/elementary/GlobalSearchModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {
FeatherIcon,
BookOpenIcon,
AddressBookIcon,
LightningIcon,
} from "@phosphor-icons/react/ssr"
import { Command } from "cmdk"
import { useLocale } from "next-intl"
Expand Down Expand Up @@ -36,6 +37,7 @@ const EMPTY_RESULT: GlobalSearchResult = {
caseStudies: [],
pages: [],
blogPosts: [],
features: [],
docs: [],
}

Expand Down Expand Up @@ -132,7 +134,8 @@ export function GlobalSearchModal({
results.pages.length > 0 ||
results.docs.length > 0 ||
results.blogPosts.length > 0 ||
results.caseStudies.length > 0
results.caseStudies.length > 0 ||
results.features.length > 0

return (
<Dialog open={open} onOpenChange={handleOpenChange}>
Expand Down Expand Up @@ -273,6 +276,44 @@ export function GlobalSearchModal({
</Command.Group>
)}

{results.features.length > 0 && (
<Command.Group heading="Features" className={groupClass}>
{results.features.map((item) => {
const href = item.url ?? ""
const isExternal = /^https?:\/\//.test(href)

return (
<Command.Item
key={`feature-${item.title}`}
value={`feature-${item.title}`}
onSelect={() => {
if (!href) return
if (isExternal) {
handleOpenChange(false)
window.open(href, "_blank", "noopener,noreferrer")
} else {
navigateAndClose(href)
}
}}
className={itemClass}
>
<LightningIcon className="text-muted-foreground size-5 min-h-5 min-w-5" />
<div className="flex flex-col">
<span className="text-foreground font-medium">
{item.title}
</span>
{item.feature_tag && (
<span className="text-muted-foreground text-xs">
{item.feature_tag}
</span>
)}
</div>
</Command.Item>
)
})}
</Command.Group>
)}

{results.caseStudies.length > 0 && (
<Command.Group heading="Case Studies" className={groupClass}>
{results.caseStudies.map((item) => {
Expand Down
87 changes: 87 additions & 0 deletions apps/ui/src/components/elementary/SearchFilterSidebar.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
"use client"

import { MagnifyingGlassIcon } from "@phosphor-icons/react/ssr"

import { Checkbox } from "@/components/ui/checkbox"
import { Input } from "@/components/ui/input"
import { Label } from "@/components/ui/label"
import { cn } from "@/lib/styles"

export interface FilterOption {
readonly label: string
readonly value: string
}

interface SearchFilterSidebarProps {
readonly query: string
readonly onQueryChange: (next: string) => void
readonly searchPlaceholder: string
readonly filterLabel?: string
readonly filterOptions?: readonly FilterOption[]
readonly selectedValues?: ReadonlySet<string>
readonly onToggleValue?: (value: string) => void
readonly idPrefix: string
readonly className?: string
}

export function SearchFilterSidebar({
query,
onQueryChange,
searchPlaceholder,
filterLabel,
filterOptions,
selectedValues,
onToggleValue,
idPrefix,
className,
}: SearchFilterSidebarProps) {
const showFilters =
filterOptions != null && filterOptions.length > 0 && filterLabel != null

return (
<aside
className={cn("flex flex-col gap-6 lg:w-1/4 lg:shrink-0", className)}
>
<div className="relative">
<MagnifyingGlassIcon className="text-muted-foreground absolute top-1/2 left-3 size-4 -translate-y-1/2" />
<Input
placeholder={searchPlaceholder}
value={query}
onChange={(e) => onQueryChange(e.target.value)}
className="pl-9"
/>
</div>

{showFilters && (
<div className="flex flex-col gap-3">
<p className="text-foreground text-sm font-semibold tracking-[0.5px] uppercase">
{filterLabel}
</p>

<div className="flex flex-col gap-2">
{filterOptions.map((option) => {
const id = `${idPrefix}-${option.value}`
const isChecked = selectedValues?.has(option.value) ?? false

return (
<div key={option.value} className="flex items-center gap-2">
<Checkbox
id={id}
checked={isChecked}
onCheckedChange={() => onToggleValue?.(option.value)}
/>
<Label
htmlFor={id}
className="cursor-pointer text-base font-normal"
>
{option.label}
</Label>
</div>
)
})}
</div>
</div>
)}
</aside>
)
}
13 changes: 11 additions & 2 deletions apps/ui/src/components/elementary/global-search-action.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import {
DOCS_INDEX_NAME,
getBlogPostsIndexName,
getCaseStudiesIndexName,
getFeaturesIndexName,
getMeilisearchClient,
getMeilisearchDocsClient,
getPagesIndexName,
Expand All @@ -13,6 +14,7 @@ import type {
BlogPostGlobalHit,
CaseStudyGlobalHit,
DocsGlobalHit,
FeatureGlobalHit,
GlobalSearchResult,
PageGlobalHit,
} from "./global-search-types"
Expand All @@ -35,7 +37,7 @@ export async function globalSearch({
const trimmed = query.trim()

if (trimmed.length === 0) {
return { caseStudies: [], pages: [], blogPosts: [], docs: [] }
return { caseStudies: [], pages: [], blogPosts: [], features: [], docs: [] }
}

const [siteRes, docsRes] = await Promise.all([
Expand All @@ -60,6 +62,12 @@ export async function globalSearch({
limit: PER_INDEX_LIMIT,
attributesToRetrieve: ["slug", "title", "description"],
},
{
indexUid: getFeaturesIndexName(),
q: trimmed,
limit: PER_INDEX_LIMIT,
attributesToRetrieve: ["title", "description", "url", "feature_tag"],
},
],
}),
getMeilisearchDocsClient()
Expand All @@ -78,14 +86,15 @@ export async function globalSearch({
.catch(() => ({ hits: [] })),
])

const [caseStudies, pages, blogPosts] = siteRes.results
const [caseStudies, pages, blogPosts, features] = siteRes.results

return {
caseStudies: (caseStudies?.hits ??
[]) as unknown as readonly CaseStudyGlobalHit[],
pages: (pages?.hits ?? []) as unknown as readonly PageGlobalHit[],
blogPosts: (blogPosts?.hits ??
[]) as unknown as readonly BlogPostGlobalHit[],
features: (features?.hits ?? []) as unknown as readonly FeatureGlobalHit[],
docs: (docsRes.hits ?? []) as unknown as readonly DocsGlobalHit[],
}
}
8 changes: 8 additions & 0 deletions apps/ui/src/components/elementary/global-search-types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,13 @@ export interface BlogPostGlobalHit {
readonly description?: string | null
}

export interface FeatureGlobalHit {
readonly title: string
readonly description?: string | null
readonly url?: string | null
readonly feature_tag?: string | null
}

export interface DocsGlobalHit {
readonly url: string
readonly hierarchy_lvl0?: string | null
Expand All @@ -30,5 +37,6 @@ export interface GlobalSearchResult {
readonly caseStudies: readonly CaseStudyGlobalHit[]
readonly pages: readonly PageGlobalHit[]
readonly blogPosts: readonly BlogPostGlobalHit[]
readonly features: readonly FeatureGlobalHit[]
readonly docs: readonly DocsGlobalHit[]
}
Loading
Loading