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
18 changes: 18 additions & 0 deletions apps/web/components/Sidebar/Sidebar.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import React from "react"

interface SidebarProps {
children?: React.ReactNode
}

/**
* Sidebar component for filtering and navigating through UI components.
* Hidden below 590px screen width.
* @returns
*/
export default function Sidebar({ children }: SidebarProps) {
return (
<aside className="hidden h-screen sticky top-0 min-[590px]:block w-64 p-6 bg-white shadow-sm border-r border-t overflow-y-auto dark:bg-gray-800 dark:border-gray-700 ">
{children}
</aside>
)
}
77 changes: 77 additions & 0 deletions apps/web/components/Sidebar/search/SidebarCategories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
import { ComponentMetadata } from "@/types"

interface SidebarCategoriesProps {
categories: string[]
groupedComponents: Record<string, ComponentMetadata[]>
components: ComponentMetadata[]
selectedCategory: string | null
setSelectedCategory: (_category: string | null) => void
}

/** Sidebar component for filtering by categories.
* To be used within the Sidebar component.
* @example
* <SidebarCategories
* categories={['Buttons', 'Forms', 'Modals']}
* components={[]}
* selectedCategory={null}
* setSelectedCategory={() => {}}
* />
*
* @param categories - List of available categories.
* @param groupedComponents - Map of category names to their components.
* @param components - List of all components (used to count total components).
* @param selectedCategory - Currently selected category.
* @param setSelectedCategory - Function to update the selected category.
* @returns JSX.Element
*/
export default function SidebarCategories({
categories,
groupedComponents,
components,
selectedCategory,
setSelectedCategory,
}: SidebarCategoriesProps) {
const clearCategories = () => {
setSelectedCategory(null)
}

return (
<div>
<h3 className="text-sm font-semibold text-gray-900 uppercase tracking-wide mb-3 dark:text-gray-100">
Categories
</h3>
<div className="space-y-1">
<button
onClick={clearCategories}
className={`w-full text-left px-3 py-2 rounded-md text-sm transition-colors ${
selectedCategory === null
? "bg-blue-100 text-blue-700 font-medium dark:bg-blue-900 dark:text-blue-300"
: "text-gray-700 hover:bg-gray-100 dark:text-gray-300 dark:hover:bg-gray-700"
}`}
>
All Components ({components?.length})
</button>
{categories?.map(category => {
const count = groupedComponents[category]?.length || 0
return (
<button
key={category}
onClick={() => setSelectedCategory(category)}
className={`w-full text-left px-3 py-2 rounded-md text-sm transition-colors ${
selectedCategory === category
? "bg-blue-100 text-blue-700 font-medium dark:bg-blue-900 dark:text-blue-300"
: "text-gray-700 hover:bg-gray-100 dark:text-gray-300 dark:hover:bg-gray-700"
}`}
>
<span className="capitalize">{category}</span>
<span className="text-gray-500 ml-2 dark:text-gray-400">
({count})
</span>
</button>
)
})}
</div>
</div>
)
}
80 changes: 80 additions & 0 deletions apps/web/components/Sidebar/search/SidebarComponentLinks.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
import Link from "next/link"
import { ComponentMetadata } from "@/types"
import { useMemo } from "react"

interface SidebarComponentLinksProps {
components: ComponentMetadata[]
currentComponent?: string
}

/** Sidebar component for listing and selecting components.
* To be used within the Sidebar component.
* @example
* <SidebarComponentLinks
* components={components}
* currentComponent={currentComponent}
* />
* @param components - List of all components to display.
* @param currentComponent - Currently selected component name.
* @returns JSX.Element
*/
export default function SidebarComponentLinks({
components,
currentComponent,
}: SidebarComponentLinksProps) {
// Group components by framework
const frameworkGroups = useMemo(() => {
const groups = components.reduce(
(groups, component) => {
const framework = component.framework || "Other"
if (!groups[framework]) {
groups[framework] = []
}
groups[framework].push(component)
return groups
},
{} as Record<string, ComponentMetadata[]>
)

//Sort frameworks alphabetically
const sortedGroups: Record<string, ComponentMetadata[]> = {}
Object.keys(groups)
.sort()
.forEach(framework => {
sortedGroups[framework] = groups[framework]
})
return sortedGroups
}, [components])

return (
<>
{components.length === 0 && (
<p className="flex justify-center text-sm text-gray-500 dark:text-gray-400">
No components found.
</p>
)}
{Object.keys(frameworkGroups).map(framework => (
<div key={framework} className="mb-6">
<h3 className="text-sm font-semibold text-gray-900 uppercase tracking-wide mb-3 dark:text-gray-100">
{framework} ({frameworkGroups[framework].length})
</h3>
{frameworkGroups[framework].map(comp => (
<div key={comp.name} className="mt-2">
<Link
href={`/component/${comp.path}`}
className={
"w-full text-left px-3 py-2 rounded-md text-sm transition-colors " +
(comp.name === currentComponent
? "bg-blue-100 text-blue-700 font-medium dark:bg-blue-900 dark:text-blue-300"
: "text-gray-700 hover:bg-gray-100 dark:text-gray-300 dark:hover:bg-gray-700")
}
>
{comp.name}
</Link>
</div>
))}
</div>
))}
</>
)
}
96 changes: 96 additions & 0 deletions apps/web/components/Sidebar/search/SidebarFrameworks.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
import { ComponentMetadata } from "@/types"
import { useMemo } from "react"

interface SidebarFrameworksProps {
frameworks: string[]
components: ComponentMetadata[]
selectedFramework: string | null
setSelectedFramework: (_framework: string | null) => void
}

/** Sidebar component for filtering by front-end frameworks.
* To be used within the Sidebar component.
*
* @example
* <SidebarFrameworks
* frameworks={['React', 'Vue', 'Angular']}
* components={[]}
* selectedFramework={null}
* setSelectedFramework={() => {}}
* />
*
* @param frameworks - List of available frameworks.
* @param components - List of all components (used to count components per framework).
* @param selectedFramework - Currently selected framework.
* @param setSelectedFramework - Function to update the selected framework.
* @returns JSX.Element
*/
export default function SidebarFrameworks({
frameworks,
components,
selectedFramework,
setSelectedFramework,
}: SidebarFrameworksProps) {
const clearFrameworks = () => {
setSelectedFramework(null)
}

const onFrameworkSelect = (framework: string) => {
setSelectedFramework(framework)
}

const lowerSelectedFramework = selectedFramework?.toLowerCase() || null

const frameworkCount = useMemo(() => {
return frameworks.reduce(
(acc, framework) => {
acc[framework] = components.filter(
component =>
(component.framework || "").trim().toLowerCase() ===
framework.trim().toLowerCase()
).length
return acc
},
{} as Record<string, number>
)
}, [components, frameworks])

return (
<div className="mt-8">
<h3 className="text-sm font-semibold text-gray-900 uppercase tracking-wide mb-3 dark:text-gray-100">
Frameworks
</h3>
<div className="space-y-1">
<button
onClick={clearFrameworks}
className={`w-full text-left px-3 py-2 rounded-md text-sm transition-colors ${
selectedFramework === null
? "bg-blue-100 text-blue-700 font-medium dark:bg-blue-900 dark:text-blue-300"
: "text-gray-700 hover:bg-gray-100 dark:text-gray-300 dark:hover:bg-gray-700"
}`}
>
All Frameworks ({components?.length})
</button>

{frameworks?.map(framework => {
return (
<button
key={framework}
onClick={() => onFrameworkSelect(framework.toLocaleLowerCase())}
className={`w-full text-left px-3 py-2 rounded-md text-sm transition-colors ${
lowerSelectedFramework === framework.toLowerCase()
? "bg-blue-100 text-blue-700 font-medium dark:bg-blue-900 dark:text-blue-300"
: "text-gray-700 hover:bg-gray-100 dark:text-gray-300 dark:hover:bg-gray-700"
}`}
>
<span className="capitalize">{framework}</span>
<span className="text-gray-500 ml-2 dark:text-gray-400">
({frameworkCount[framework] || 0})
</span>
</button>
)
})}
</div>
</div>
)
}
55 changes: 55 additions & 0 deletions apps/web/components/Sidebar/search/SidebarSearch.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import React from "react"

interface SidebarSearchbarProps {
setSearchQuery: (_query: string) => void
searchQuery: string
}

/** Sidebar search bar component.
* To be used within the Sidebar component.
* @example
* <SidebarSearchbar
* searchQuery={searchQuery}
* setSearchQuery={setSearchQuery}
* />
* @param setSearchQuery - Function to update the search query state.
* @param searchQuery - Current search query state.
* @returns JSX.Element
*/
export default function SidebarSearchbar({
setSearchQuery,
searchQuery,
}: SidebarSearchbarProps) {
const onChange = (e: React.ChangeEvent<HTMLInputElement>) => {
setSearchQuery(e.target.value)
}

return (
<div className="mb-6">
<div className="relative">
<input
type="text"
placeholder="Search components..."
value={searchQuery}
onChange={onChange}
className="w-full pl-10 pr-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent dark:bg-gray-700 dark:border-gray-600 dark:text-white dark:placeholder-gray-400"
/>
<div className="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
<svg
className="h-5 w-5 text-gray-400 dark:text-gray-500"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"
/>
</svg>
</div>
</div>
</div>
)
}
67 changes: 67 additions & 0 deletions apps/web/components/Sidebar/search/SidebarTags.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import TagChip from "@/components/TagChip"
import { Dispatch, SetStateAction } from "react"

interface SidebarTagsProps {
tags: string[]
selectedTags: string[]
setSelectedTags: Dispatch<SetStateAction<string[]>>
}

/** Sidebar component for filtering by tags.
* To be used within the Sidebar component.
* @example
* <SidebarTags
* tags={['accessible', 'responsive', 'dark-mode']}
* components={components}
* selectedTags={selectedTags}
* setSelectedTags={setSelectedTags}
* />
* @param tags - List of available tags.
* @param selectedTags - Currently selected tags.
* @param setSelectedTags - Function to update the selected tags.
* @returns JSX.Element
*/
export default function SidebarTags({
tags,
selectedTags,
setSelectedTags,
}: SidebarTagsProps) {
const clearTags = () => {
setSelectedTags([])
}

const onTagToggle = (tag: string) => {
setSelectedTags((prev: string[]) =>
prev.includes(tag) ? prev.filter(t => t !== tag) : [...prev, tag]
)
}

return (
<div className="mt-8">
<h3 className="text-sm font-semibold text-gray-900 dark:text-white uppercase tracking-wide mb-3">
Tags
</h3>
<div className="flex flex-wrap gap-2">
{tags?.map(tag => (
<TagChip
key={tag}
tag={tag}
active={selectedTags.includes(tag)}
onToggle={onTagToggle}
/>
))}
</div>
{selectedTags.length > 0 && (
<div className="mt-3">
<button
type="button"
onClick={clearTags}
className="text-xs text-gray-600 dark:text-gray-400 hover:text-gray-800 dark:hover:text-gray-200"
>
Clear tags
</button>
</div>
)}
</div>
)
}
Loading
Loading