diff --git a/apps/web/components/Sidebar/Sidebar.tsx b/apps/web/components/Sidebar/Sidebar.tsx new file mode 100644 index 0000000..6f31f5b --- /dev/null +++ b/apps/web/components/Sidebar/Sidebar.tsx @@ -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 ( + + ) +} diff --git a/apps/web/components/Sidebar/search/SidebarCategories.tsx b/apps/web/components/Sidebar/search/SidebarCategories.tsx new file mode 100644 index 0000000..ea1ef74 --- /dev/null +++ b/apps/web/components/Sidebar/search/SidebarCategories.tsx @@ -0,0 +1,77 @@ +import { ComponentMetadata } from "@/types" + +interface SidebarCategoriesProps { + categories: string[] + groupedComponents: Record + components: ComponentMetadata[] + selectedCategory: string | null + setSelectedCategory: (_category: string | null) => void +} + +/** Sidebar component for filtering by categories. + * To be used within the Sidebar component. + * @example + * {}} + * /> + * + * @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 ( +
+

+ Categories +

+
+ + {categories?.map(category => { + const count = groupedComponents[category]?.length || 0 + return ( + + ) + })} +
+
+ ) +} diff --git a/apps/web/components/Sidebar/search/SidebarComponentLinks.tsx b/apps/web/components/Sidebar/search/SidebarComponentLinks.tsx new file mode 100644 index 0000000..b76ba4b --- /dev/null +++ b/apps/web/components/Sidebar/search/SidebarComponentLinks.tsx @@ -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 + * + * @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 + ) + + //Sort frameworks alphabetically + const sortedGroups: Record = {} + Object.keys(groups) + .sort() + .forEach(framework => { + sortedGroups[framework] = groups[framework] + }) + return sortedGroups + }, [components]) + + return ( + <> + {components.length === 0 && ( +

+ No components found. +

+ )} + {Object.keys(frameworkGroups).map(framework => ( +
+

+ {framework} ({frameworkGroups[framework].length}) +

+ {frameworkGroups[framework].map(comp => ( +
+ + {comp.name} + +
+ ))} +
+ ))} + + ) +} diff --git a/apps/web/components/Sidebar/search/SidebarFrameworks.tsx b/apps/web/components/Sidebar/search/SidebarFrameworks.tsx new file mode 100644 index 0000000..be5b5b6 --- /dev/null +++ b/apps/web/components/Sidebar/search/SidebarFrameworks.tsx @@ -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 + * {}} + * /> + * + * @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 + ) + }, [components, frameworks]) + + return ( +
+

+ Frameworks +

+
+ + + {frameworks?.map(framework => { + return ( + + ) + })} +
+
+ ) +} diff --git a/apps/web/components/Sidebar/search/SidebarSearch.tsx b/apps/web/components/Sidebar/search/SidebarSearch.tsx new file mode 100644 index 0000000..ebb212b --- /dev/null +++ b/apps/web/components/Sidebar/search/SidebarSearch.tsx @@ -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 + * + * @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) => { + setSearchQuery(e.target.value) + } + + return ( +
+
+ +
+ + + +
+
+
+ ) +} diff --git a/apps/web/components/Sidebar/search/SidebarTags.tsx b/apps/web/components/Sidebar/search/SidebarTags.tsx new file mode 100644 index 0000000..5c66f54 --- /dev/null +++ b/apps/web/components/Sidebar/search/SidebarTags.tsx @@ -0,0 +1,67 @@ +import TagChip from "@/components/TagChip" +import { Dispatch, SetStateAction } from "react" + +interface SidebarTagsProps { + tags: string[] + selectedTags: string[] + setSelectedTags: Dispatch> +} + +/** Sidebar component for filtering by tags. + * To be used within the Sidebar component. + * @example + * + * @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 ( +
+

+ Tags +

+
+ {tags?.map(tag => ( + + ))} +
+ {selectedTags.length > 0 && ( +
+ +
+ )} +
+ ) +} diff --git a/apps/web/pages/component/[...path].tsx b/apps/web/pages/component/[...path].tsx index ad4db04..3f6a614 100644 --- a/apps/web/pages/component/[...path].tsx +++ b/apps/web/pages/component/[...path].tsx @@ -3,7 +3,21 @@ import Head from "next/head" import { useRouter } from "next/router" import { useEffect, useState } from "react" import ReactPreview from "../../components/ReactPreview" -import type { ComponentWithFiles } from "../../types" +import type { ComponentMetadata, ComponentWithFiles } from "../../types" +import Sidebar from "@/components/Sidebar/Sidebar" +import SidebarSearchbar from "@/components/Sidebar/search/SidebarSearch" +import SidebarComponentLinks from "@/components/Sidebar/search/SidebarComponentLinks" + +// Simple debounce so that the array filtering doesn't happen on every keystroke +const debounce = (callback: (..._args: any[]) => void, wait: number) => { + let timeoutId: number | undefined = undefined + return (...args: any[]) => { + window.clearTimeout(timeoutId) + timeoutId = window.setTimeout(() => { + callback(...args) + }, wait) + } +} export default function ComponentPage() { const router = useRouter() @@ -15,6 +29,50 @@ export default function ComponentPage() { {} ) const [wordWrap, setWordWrap] = useState(false) + const [searchQuery, setSearchQuery] = useState("") + const [components, setComponents] = useState([]) + const [filteredComponents, setFilteredComponents] = useState< + ComponentMetadata[] + >([]) + + function filterComponentsByQuery(query: string) { + if (!query) { + setFilteredComponents(components) + return + } + + const filtered = components.filter(comp => + comp.name.toLowerCase().includes(query.toLowerCase()) + ) + setFilteredComponents(filtered) + } + + const onSearchChange = (query: string) => { + setSearchQuery(query) + debounce(filterComponentsByQuery, 600)(query) + } + + useEffect(() => { + fetch("/api/components") + .then(res => res.json()) + .then(data => { + //Sort components alphabetically for easier browsing + const sortedComponents = data.components.sort( + (a: ComponentMetadata, b: ComponentMetadata) => + a.name.localeCompare(b.name) + ) + + //Have to create a copy of the components to have both original and filtered lists + setComponents(sortedComponents || []) + setFilteredComponents(sortedComponents || []) + setLoading(false) + }) + .catch(err => { + // eslint-disable-next-line no-console + console.error("Failed to load components:", err) + setLoading(false) + }) + }, []) const copyToClipboard = async (text: string, key: string) => { try { @@ -152,21 +210,34 @@ export default function ComponentPage() { - -
-
- {/* Preview Section - Full Width */} -
-

- Preview -

- {isHtmlTailwind && component.files["index.html"] ? ( -
- {/* Subtle checkerboard pattern for better contrast */} -
+ {/* Sidebar - Hidden below 590px */} + + {/* Search Bar */} + + {/* Component Links */} + + +
+
+ {/* Preview Section - Full Width */} +
+

+ Preview +

+ {isHtmlTailwind && component.files["index.html"] ? ( +
+ {/* Subtle checkerboard pattern for better contrast */} +
-