diff --git a/__tests__/components/component-categories.test.tsx b/__tests__/components/component-categories.test.tsx index ef0b20d6..57d29a32 100644 --- a/__tests__/components/component-categories.test.tsx +++ b/__tests__/components/component-categories.test.tsx @@ -21,6 +21,10 @@ describe("SectionsList", () => { it("category should have componentList === null if comingSoonCategory === true", () => { for (const section of SectionsList) { + if (section.type !== "multiple-component") { + continue; + } + //TODO: Handle the single-component test for (const category of section.categoriesList) { if (category.comingSoonCategory) { expect(category.componentList).toBeNull(); @@ -51,6 +55,9 @@ describe("SectionsList", () => { }; for (const section of SectionsList) { + if (section.type !== "multiple-component") { + continue; + } for (const category of section.categoriesList) { if (category.componentList) { for (const component of category.componentList) { diff --git a/biome.json b/biome.json index e72046c7..79c2428d 100644 --- a/biome.json +++ b/biome.json @@ -44,7 +44,7 @@ "all": true, "noDefaultExport": "off", "noNamespaceImport": "info", - "useNamingConvention": "info" + "useNamingConvention": "off" }, "suspicious": { "all": true, @@ -56,4 +56,4 @@ } } } -} +} \ No newline at end of file diff --git a/package.json b/package.json index e34fe1db..dec15afc 100644 --- a/package.json +++ b/package.json @@ -17,12 +17,6 @@ "test": "vitest", "generate-package-list-check": "node scripts/generate-package-list-check.js" }, - "git": { - "pre-commit": "lint-staged" - }, - "lint-staged": { - "*": "biome format --write ./src" - }, "dependencies": { "@ctrl/tinycolor": "4.1.0", "@radix-ui/react-dialog": "1.1.1", @@ -81,4 +75,4 @@ "typescript": "5.4.5", "vitest": "2.0.5" } -} +} \ No newline at end of file diff --git a/readme.md b/readme.md index f3d97eee..b942af27 100644 --- a/readme.md +++ b/readme.md @@ -52,6 +52,16 @@ The component roadmap can be find on the [featurebase page](https://cuicui.featu - [ ] Create the pages for the category sections (i.e https://cuicui.day/application-ui) -> Display the grid of the components as the home page but for each section - [ ] Improve SEO with better title and meta tags on components & site pages - [ ] Improve homepage performance by replacing component integrations by images +- [ ] Re-add precommit (fix importer order & console rules) + +```json +"git": { + "pre-commit": "lint-staged" +}, +"lint-staged": { + "*": "biome format --write ./src" +}, +``` ## License diff --git a/src/app/(components)/[section]/[category]/multiple-component-section.tsx b/src/app/(components)/[section]/[category]/multiple-component-section.tsx new file mode 100644 index 00000000..c3002dad --- /dev/null +++ b/src/app/(components)/[section]/[category]/multiple-component-section.tsx @@ -0,0 +1,57 @@ +import { fetchMultipleComponentData } from "#/src/app/(components)/[section]/[category]/process-variant-data"; +import ComingSoonCard from "#/src/components/coming-soon"; +import HeaderComponent from "#/src/components/component-wrapper/header-component"; +import InspirationComponentFooter from "#/src/components/component-wrapper/inspiration-component-footer"; +import VariantTabs from "#/src/components/component-wrapper/variant-tabs"; +import type { CategoryType } from "#/src/lib/types/component"; +import { notFound } from "next/navigation"; +import { Fragment } from "react"; + +export default async function MultipleComponentCategory({ + category, +}: Readonly<{ category: CategoryType }>) { + if (category.comingSoonCategory) { + return ; + } + + if (!category.componentList) { + return notFound(); + } + + // If the componentList is not an array, we convert it to an array + const componentArray = Array.isArray(category.componentList) + ? category.componentList + : [category.componentList]; + + const componentList = await fetchMultipleComponentData({ + categorySlug: category.slug, + componentList: componentArray, + }); + + return ( +
+ {componentList.map((component) => ( + + + + + + ))} +
+ ); +} diff --git a/src/app/(components)/[section]/[category]/page.tsx b/src/app/(components)/[section]/[category]/page.tsx index e812be43..6db04b37 100644 --- a/src/app/(components)/[section]/[category]/page.tsx +++ b/src/app/(components)/[section]/[category]/page.tsx @@ -1,20 +1,33 @@ import { notFound } from "next/navigation"; -import { Fragment } from "react"; -import { fetchComponentData } from "#/src/app/(components)/[section]/[category]/process-variant-data"; -import ComingSoonCard from "#/src/components/coming-soon"; -import HeaderComponent from "#/src/components/component-wrapper/header-component"; -import InspirationComponentFooter from "#/src/components/component-wrapper/inspiration-component-footer"; -import VariantTabs from "#/src/components/component-wrapper/variant-tabs"; import { SectionsList } from "#/src/lib/cuicui-components/sections-list"; +import SingleComponentCategory from "#/src/app/(components)/[section]/[category]/single-component-section"; +import MultipleComponentCategory from "#/src/app/(components)/[section]/[category]/multiple-component-section"; +import type { + CategoryType, + MultiComponentSectionType, + PageSectionType, + SectionType, + SingleComponentCategoryType, + SingleComponentSectionType, +} from "#/src/lib/types/component"; export async function generateStaticParams() { // Iterate over the SectionsList to create all possible combinations of section and category - const params = SectionsList.flatMap((section) => - section.categoriesList.map((category) => ({ - section: section.slug, - category: category.slug, - })), - ); + + const params = SectionsList.flatMap((section) => { + if ( + section.type === "multiple-component" || + section.type === "single-component" + ) { + return section.categoriesList.map((category) => ({ + section: section.slug, + category: category.slug, + })); + } + if (section.type === "page") { + //TODO : Handle pages + } + }); // Return the generated params array return params; @@ -31,51 +44,57 @@ export default async function Page({ const section = SectionsList.find( (section) => section.slug === params.section, ); - - const category = section?.categoriesList.find( - (category) => category.slug === params.category, - ); - - if (!category) { + if (!section) { return notFound(); } + if (section?.type === "page") { + return; + } - if (category.comingSoonCategory) { - return ; + if (section?.type === "single-component") { + const category = findCategoryBySlug(section, params.category); + if (!category) { + return notFound(); + } + return ; } - if (!category.componentList) { - return notFound(); + + if (section?.type === "multiple-component") { + const category = findCategoryBySlug(section, params.category); + if (!category) { + return notFound(); + } + return ; } +} - const componentList = await fetchComponentData({ - categorySlug: category.slug, - componentList: category.componentList, - }); +// Function Overloads +function findCategoryBySlug( + section: SingleComponentSectionType, + categoryParamSlug: string, +): SingleComponentCategoryType | null; +function findCategoryBySlug( + section: MultiComponentSectionType, + categoryParamSlug: string, +): CategoryType | null; + +function findCategoryBySlug( + section: PageSectionType, + categoryParamSlug: string, +): null; + +// General Implementation +function findCategoryBySlug( + section: SectionType, + categoryParamSlug: string, +): SingleComponentCategoryType | CategoryType | null { + if (section.type === "page") { + return null; + } return ( -
- {componentList.map((component) => ( - - - - - - ))} -
+ section?.categoriesList.find( + (category) => category.slug === categoryParamSlug, + ) ?? null ); } diff --git a/src/app/(components)/[section]/[category]/process-variant-data.ts b/src/app/(components)/[section]/[category]/process-variant-data.ts index d719b256..55627892 100644 --- a/src/app/(components)/[section]/[category]/process-variant-data.ts +++ b/src/app/(components)/[section]/[category]/process-variant-data.ts @@ -1,10 +1,11 @@ import type { ComponentType, - ComponentVariantType, + SingleComponentType, + VariantType, } from "#/src/lib/types/component"; import { getFileContentAsString } from "#/src/utils/get-file-content-as-string"; -interface ProcessedVariant extends ComponentVariantType { +interface ProcessedVariant extends VariantType { previewCode: string; componentCode?: string; } @@ -13,27 +14,46 @@ interface ProcessedComponent extends ComponentType { componentList: ProcessedVariant[]; } -export async function fetchVariantData( - categorySlug: string, - componentSlug: string, - variant: ComponentVariantType, -) { +export async function fetchVariantData({ + categorySlug, + componentSlug, + variant, +}: { + categorySlug: string; + componentSlug?: string; + variant: VariantType; +}) { const variantName = variant.name; const variantSlugPreviewFile = variant.slugPreviewFile ?? variantName; const variantSlugComponentFile = variant.slugComponentFile ?? variantName; - const previewCode = await getFileContentAsString({ - componentSlug: categorySlug, - variantName: `${componentSlug}/${variantSlugPreviewFile}`, - }); + let previewCode: string | null = null; + let componentCode: string | undefined; + if (componentSlug) { + previewCode = await getFileContentAsString({ + componentSlug: categorySlug, + variantName: `${componentSlug}/${variantSlugPreviewFile}`, + }); - const componentCode = variant.slugComponentFile - ? await getFileContentAsString({ - componentSlug: categorySlug, - variantName: `${componentSlug}/${variantSlugComponentFile}`, - }) - : undefined; + componentCode = variant.slugComponentFile + ? await getFileContentAsString({ + componentSlug: categorySlug, + variantName: `${componentSlug}/${variantSlugComponentFile}`, + }) + : undefined; + } else { + previewCode = await getFileContentAsString({ + componentSlug: categorySlug, + variantName: variantSlugPreviewFile, + }); + componentCode = variant.slugComponentFile + ? await getFileContentAsString({ + componentSlug: categorySlug, + variantName: variantSlugComponentFile, + }) + : undefined; + } return { ...variant, previewCode, @@ -41,7 +61,7 @@ export async function fetchVariantData( }; } -export async function fetchComponentData({ +export async function fetchMultipleComponentData({ componentList, categorySlug, }: { @@ -54,11 +74,11 @@ export async function fetchComponentData({ const processedVariants: ProcessedVariant[] = []; for (const variant of component.variantList) { - const processedVariant = await fetchVariantData( + const processedVariant = await fetchVariantData({ categorySlug, - component.slug, + componentSlug: component.slug, variant, - ); + }); processedVariants.push(processedVariant); } @@ -72,3 +92,23 @@ export async function fetchComponentData({ return processedComponents; } + +export async function fetchSingleComponentData({ + categorySlug, + component, +}: { + categorySlug: string; + component: SingleComponentType; +}) { + const processedVariants: ProcessedVariant[] = []; + + for (const variant of component.variantList) { + const processedVariant = await fetchVariantData({ categorySlug, variant }); + processedVariants.push(processedVariant); + } + + return { + ...component, + componentList: processedVariants, + }; +} diff --git a/src/app/(components)/[section]/[category]/single-component-section.tsx b/src/app/(components)/[section]/[category]/single-component-section.tsx new file mode 100644 index 00000000..ae334116 --- /dev/null +++ b/src/app/(components)/[section]/[category]/single-component-section.tsx @@ -0,0 +1,49 @@ +import { fetchSingleComponentData } from "#/src/app/(components)/[section]/[category]/process-variant-data"; +import ComingSoonCard from "#/src/components/coming-soon"; +import HeaderComponent from "#/src/components/component-wrapper/header-component"; +import InspirationComponentFooter from "#/src/components/component-wrapper/inspiration-component-footer"; +import VariantTabs from "#/src/components/component-wrapper/variant-tabs"; +import type { SingleComponentCategoryType } from "#/src/lib/types/component"; +import { notFound } from "next/navigation"; +import { Fragment } from "react"; + +export default async function SingleComponentCategory({ + category, +}: Readonly<{ category: SingleComponentCategoryType }>) { + if (category.comingSoonCategory) { + return ; + } + + if (!category.component) { + return notFound(); + } + + const component = await fetchSingleComponentData({ + categorySlug: category.slug, + component: category.component, + }); + + return ( +
+ + + + + +
+ ); +} diff --git a/src/app/card.tsx b/src/app/card.tsx index 3b9d6578..2a2af657 100644 --- a/src/app/card.tsx +++ b/src/app/card.tsx @@ -1,12 +1,15 @@ import Image from "next/image"; import { cn } from "#/src/utils/cn"; -import type { CategoryType } from "../lib/types/component"; +import type { + CategoryType, + SingleComponentCategoryType, +} from "../lib/types/component"; export const MainMenuCard = ({ category, }: { - category?: CategoryType; + category?: CategoryType | SingleComponentCategoryType; }) => { if (category?.comingSoonCategory) { return ( diff --git a/src/app/page.tsx b/src/app/page.tsx index 6c560df8..91781bd3 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -44,22 +44,23 @@ export default function HomePage() {

{section.name}

- {section.categoriesList.map((category) => { - return ( - - { + return ( + - - - {/* */} - - ); - })} + + + + {/* */} + + ); + })}
))} diff --git a/src/app/sitemap.ts b/src/app/sitemap.ts index 1215b3e2..7d26d5fe 100644 --- a/src/app/sitemap.ts +++ b/src/app/sitemap.ts @@ -5,7 +5,7 @@ export const port = process.env.PORT ?? 3000; export const HOST = process.env.NEXT_PUBLIC_SITE_URL ?? `http://localhost:${port}`; -export default async function sitemap(): Promise { +export default function sitemap(): MetadataRoute.Sitemap { // Google's limit is 50,000 URLs per sitemap return [...staticSitemap, ...getComponentsSitemap()]; @@ -35,17 +35,22 @@ const staticSitemap: MetadataRoute.Sitemap = [ function getComponentsSitemap(): MetadataRoute.Sitemap { const componentSitemap: MetadataRoute.Sitemap = []; SectionsList.flatMap((section) => { - for (const category of section.categoriesList) { - if (category.comingSoonCategory) { - return; - } - if (section.slug && category.slug) { - componentSitemap.push({ - url: `${HOST}/${section.slug}/${category.slug}`, - lastModified: category.releaseDateCategory, - changeFrequency: "monthly", - priority: 0.9, - }); + if ( + section.type === "multiple-component" || + section.type === "single-component" + ) { + for (const category of section.categoriesList) { + if (category.comingSoonCategory) { + return; + } + if (section.slug && category.slug) { + componentSitemap.push({ + url: `${HOST}/${section.slug}/${category.slug}`, + lastModified: category.releaseDateCategory, + changeFrequency: "monthly", + priority: 0.9, + }); + } } } }); diff --git a/src/app/tools/color-converter/components/color-converter.tsx b/src/app/tools/color-converter/components/color-converter.tsx index 2d51442c..ddf93c7a 100644 --- a/src/app/tools/color-converter/components/color-converter.tsx +++ b/src/app/tools/color-converter/components/color-converter.tsx @@ -7,7 +7,7 @@ import { getExactColorTailwindNameFromHexaValue, } from "#/src/app/tools/color-converter/components/to-tailwind"; import { InlineCode } from "#/src/ui/cuicui/application-ui/code/inline-code/inline-code"; -import { useCopyToClipboard } from "#/src/ui/cuicui/hooks/use-copy-to-clipboard/hook/use-copy-to-clipboard"; +import { useCopyToClipboard } from "#/src/ui/cuicui/hooks/use-copy-to-clipboard/use-copy-to-clipboard"; import { CheckIcon, CopyIcon } from "lucide-react"; import HoverCard from "#/src/ui/cuicui/common-ui/cards/hover-effect-card/hover-effect-card"; import { TailwindCssLogo } from "#/src/assets/logo/tailwind-css-logo"; diff --git a/src/components/component-wrapper/component-tab-renderer.tsx b/src/components/component-wrapper/component-tab-renderer.tsx index b5379f37..0633293c 100644 --- a/src/components/component-wrapper/component-tab-renderer.tsx +++ b/src/components/component-wrapper/component-tab-renderer.tsx @@ -9,7 +9,7 @@ import { import StepToInstall from "#/src/components/steps-to-install/step-to-install"; import type { ComponentHeightType, - VariantComponent, + ProcessVariantType, } from "#/src/lib/types/component"; import CodeHighlighter from "#/src/ui/code-highlighter"; import { @@ -49,7 +49,7 @@ export default function ComponentTabRenderer({ isIframed?: boolean; size: ComponentHeightType; isChildUsingHeightFull?: boolean; -} & VariantComponent) { +} & ProcessVariantType) { return ( @@ -39,7 +39,7 @@ export default function VariantTabs({ "flex items-center p-0.5 *:data-[state=active]:rounded-lg *:data-[state=active]:border-neutral-500/20 *:data-[state=active]:bg-neutral-400/15 *:data-[state=active]:text-neutral-900", )} > - {componentList.map((variant, _index) => { + {variantList.map((variant, _index) => { return ( - {componentList.map((variant, _index) => ( + {variantList.map((variant, _index) => ( void }>) { + const router = useRouter(); + return ( + + {firstMenuSection.categoryList.map((category) => ( + { + if (category.href) { + window.open(category.href, "_blank"); + return; + } + router.push(`/${category.slug}`); + closeSearchMenu(); + }} + > + + {category.name} + + ))} + + ); +} diff --git a/src/components/search-menu/search-group-multi-component-section.tsx b/src/components/search-menu/search-group-multi-component-section.tsx new file mode 100644 index 00000000..c2185c20 --- /dev/null +++ b/src/components/search-menu/search-group-multi-component-section.tsx @@ -0,0 +1,39 @@ +import type { SectionType } from "#/src/lib/types/component"; +import { CommandGroup, CommandItem } from "#/src/ui/shadcn/command"; +import { useRouter } from "next/navigation"; + +//TODO: Add variant lists +export function SearchGroupComponentSection({ + closeSearchMenu, + section, +}: Readonly<{ closeSearchMenu: () => void; section: SectionType }>) { + const router = useRouter(); + + if ( + section.type === "multiple-component" || + section.type === "single-component" + ) { + return ( + + {section.categoriesList.map((category) => { + const Icon = category.icon; + return ( + { + router.push(`/${section.slug}/${category.slug}`); + closeSearchMenu(); + }} + > + {Icon && } + + {category.name} + + ); + })} + + ); + } + + return null; +} diff --git a/src/components/search-menu/search-menu.tsx b/src/components/search-menu/search-menu.tsx new file mode 100644 index 00000000..b2d7d187 --- /dev/null +++ b/src/components/search-menu/search-menu.tsx @@ -0,0 +1,106 @@ +"use client"; + +import { SearchIcon } from "lucide-react"; + +import { SectionsList } from "#/src/lib/cuicui-components/sections-list"; +import { + CommandDialog, + CommandEmpty, + CommandInput, + CommandList, +} from "#/src/ui/shadcn/command"; +import { useState } from "react"; +import { useKeyPress } from "#/src/ui/cuicui/hooks/use-key-press/use-key-press"; +import { FirstSectionCommandGroup } from "#/src/components/search-menu/first-section-command-group"; +import { SearchGroupComponentSection } from "#/src/components/search-menu/search-group-multi-component-section"; + +export function SearchMenu() { + const [open, setOpen] = useState(false); + + useKeyPress({ + keyPressItems: [ + { + keys: ["KeyK", "Meta"], + event: () => setOpen((prev) => !prev), + }, + { + keys: ["KeyK", "Control"], + event: () => setOpen((prev) => !prev), + }, + ], + // Important to trigger even on input fields (override default behavior) + tagsToIgnore: [""], + }); + + return ( + <> + + + + + No results found. + + setOpen(false)} /> + + {SectionsList.map((section) => { + return ( + setOpen(false)} + section={section} + /> + ); + })} + {/* {SectionsList.map((section) => { + if (section.type !== "multiple-component") { + return; + } + return ( + + {section.categoriesList.map((category) => { + return category.componentList?.map((component) => { + const Icon = category.icon; + return ( + { + router.push( + `/${section.slug}/${category.slug}#${component.slug}`, + ); + setOpen(false); + }} + > + {Icon && ( + + )} + + {component.title} + + ); + }); + })} + + ); + })} */} + + + + ); +} diff --git a/src/lib/cuicui-components/sections-list.tsx b/src/lib/cuicui-components/sections-list.tsx index 42a47b1f..252a4cc6 100644 --- a/src/lib/cuicui-components/sections-list.tsx +++ b/src/lib/cuicui-components/sections-list.tsx @@ -11,6 +11,7 @@ export const SectionsList: SectionType[] = [ commonUiSection, applicationUiCategoryList, { + type: "multiple-component", name: "Marketing UI", slug: "marketing-ui", categoriesList: marketingUiComponentList, diff --git a/src/lib/types/component.d.ts b/src/lib/types/component.d.ts index 7413eb1b..6058010f 100644 --- a/src/lib/types/component.d.ts +++ b/src/lib/types/component.d.ts @@ -4,7 +4,7 @@ import type { JSX, ReactNode } from "react"; import type { ComponentBadgeList } from "../badges.const"; export type Variant = `variant${number}`; -export type VariantComponent = { +export type ProcessVariantType = { name: string; component: JSX.Element; previewCode: string; @@ -41,11 +41,36 @@ export type PreviewComponent = { ------------------------------------ */ -export type SectionType = { +// Base interface for common properties +interface BaseSectionType { name: string; slug: string; +} + +// Section with single-component type +interface SingleComponentSectionType extends BaseSectionType { + type: "single-component"; + categoriesList: SingleComponentCategoryType[]; +} + +// Section with multi-component type +interface MultiComponentSectionType extends BaseSectionType { + type: "multiple-component"; categoriesList: CategoryType[]; -}; +} + +// Section with page type +interface PageSectionType extends BaseSectionType { + type: "page"; + href: string; + icon: LucideIcon; +} + +// Union type for SectionType +export type SectionType = + | SingleComponentSectionType + | MultiComponentSectionType + | PageSectionType; /* ------------------------------------ @@ -64,6 +89,17 @@ type CategoryType = { componentList: ComponentType[] | null; }; +type SingleComponentCategoryType = { + slug: string; + name: string; + description: string; + icon: LucideIcon; + comingSoonCategory?: boolean; + releaseDateCategory: Date; + previewCategory?: PreviewComponent; + component: SingleComponentType | null; +}; + /* ------------------------------------ ************ Components ************ @@ -84,16 +120,32 @@ export type ComponentType = { inspirationLink?: string; sizePreview: ComponentHeightType; slug: string; - variantList: ComponentVariantType[]; + variantList: VariantType[]; +}; + +export type SingleComponentType = { + // Excluded: title, description, slug + + releaseDateComponent?: Date; + lastUpdatedDateComponent?: Date; + isResizable?: boolean; + componentBadges?: ComponentBadgeSlug[]; + isIframed?: boolean; + rerenderButton?: boolean; + isChildUsingHeightFull?: boolean; + inspiration?: string; + inspirationLink?: string; + sizePreview: ComponentHeightType; + variantList: VariantType[]; }; /* ------------------------------------ -************ Variants ************ +************* Variants ************* ------------------------------------ */ -export type ComponentVariantType = { +export type VariantType = { name: string; component: JSX.Element; slugComponentFile?: string; diff --git a/src/ui/cuicui/application-ui/application-ui.section.tsx b/src/ui/cuicui/application-ui/application-ui.section.tsx index 6af88ea8..8a83e2d5 100644 --- a/src/ui/cuicui/application-ui/application-ui.section.tsx +++ b/src/ui/cuicui/application-ui/application-ui.section.tsx @@ -15,6 +15,7 @@ import { themeCategory } from "#/src/ui/cuicui/application-ui/theme/theme.catego import { treeCategory } from "#/src/ui/cuicui/application-ui/tree/tree.category"; export const applicationUiCategoryList: SectionType = { + type: "multiple-component", name: "Application UI", slug: "application-ui", categoriesList: [ diff --git a/src/ui/cuicui/application-ui/code/code-snippet/variant1.tsx b/src/ui/cuicui/application-ui/code/code-snippet/variant1.tsx index 3dd8b25e..4e2f05c8 100644 --- a/src/ui/cuicui/application-ui/code/code-snippet/variant1.tsx +++ b/src/ui/cuicui/application-ui/code/code-snippet/variant1.tsx @@ -1,7 +1,7 @@ "use client"; import { ClipboardIcon } from "lucide-react"; import { toast } from "sonner"; -import { useCopyToClipboard } from "#/src/ui/cuicui/hooks/use-copy-to-clipboard/hook/use-copy-to-clipboard"; +import { useCopyToClipboard } from "#/src/ui/cuicui/hooks/use-copy-to-clipboard/use-copy-to-clipboard"; export function CommandCode({ children }: Readonly<{ children: string }>) { const [copiedText, copy] = useCopyToClipboard(); diff --git a/src/ui/cuicui/application-ui/signature/react-signature/react-signature.tsx b/src/ui/cuicui/application-ui/signature/react-signature/react-signature.tsx index 5c81c434..b7b6e94d 100644 --- a/src/ui/cuicui/application-ui/signature/react-signature/react-signature.tsx +++ b/src/ui/cuicui/application-ui/signature/react-signature/react-signature.tsx @@ -8,7 +8,7 @@ import { RefreshCcwIcon, } from "lucide-react"; import { type ComponentProps, useRef, useState } from "react"; -import { useCopyToClipboard } from "#/src/ui/cuicui/hooks/use-copy-to-clipboard/hook/use-copy-to-clipboard"; +import { useCopyToClipboard } from "#/src/ui/cuicui/hooks/use-copy-to-clipboard/use-copy-to-clipboard"; import { cn } from "#/src/utils/cn"; export function ReactSignature({ diff --git a/src/ui/cuicui/common-ui/common-ui.section.tsx b/src/ui/cuicui/common-ui/common-ui.section.tsx index 238f8f26..e49a0402 100644 --- a/src/ui/cuicui/common-ui/common-ui.section.tsx +++ b/src/ui/cuicui/common-ui/common-ui.section.tsx @@ -14,6 +14,7 @@ import { toggleCategory } from "#/src/ui/cuicui/common-ui/toggle/toggle.category export const commonUiSection: SectionType = { name: "Common UI", slug: "common-ui", + type: "multiple-component", categoriesList: [ avatarsCategory, badgesCategory, diff --git a/src/ui/cuicui/hooks/hooks.section.tsx b/src/ui/cuicui/hooks/hooks.section.tsx index 32797a4f..aecc6590 100644 --- a/src/ui/cuicui/hooks/hooks.section.tsx +++ b/src/ui/cuicui/hooks/hooks.section.tsx @@ -1,32 +1,60 @@ -import { PictureInPictureIcon } from "lucide-react"; import type { SectionType } from "#/src/lib/types/component"; import { useBatteryCategory } from "#/src/ui/cuicui/hooks/use-battery/use-battery.category"; -import { useCopyToClipboardCategory } from "#/src/ui/cuicui/hooks/use-copy-to-clipboard/use-copy-to-clipboard.component"; -import { useCounterCategory } from "#/src/ui/cuicui/hooks/use-counter/use-counter.category"; -import { useDebounceCategory } from "#/src/ui/cuicui/hooks/use-debounce/use-debounce.category"; -import { useInViewCategory } from "#/src/ui/cuicui/hooks/use-in-view/use-in-view.category"; -import { useMouseCategory } from "#/src/ui/cuicui/hooks/use-mouse/use-mouse.category"; -import { useNetworkCategory } from "#/src/ui/cuicui/hooks/use-network-status/use-network.category"; + +import { useCounterCategory } from "#/src/ui/cuicui/hooks/use-counter/category.use-counter"; +import { useDebounceCategory } from "#/src/ui/cuicui/hooks/use-debounce/category.use-debounce"; +import { useInViewCategory } from "#/src/ui/cuicui/hooks/use-in-view/category.use-in-view"; +import { useMouseCategory } from "#/src/ui/cuicui/hooks/use-mouse/category.use-mouse"; +import { useNetworkCategory } from "#/src/ui/cuicui/hooks/use-network-status/category.use-network"; +import { useKeyPressCategory } from "#/src/ui/cuicui/hooks/use-key-press/category.use-key-press"; +import { useInputValueCategory } from "#/src/ui/cuicui/hooks/use-input-value/category.use-input-value"; +import { useGeoLocationCategory } from "#/src/ui/cuicui/hooks/use-geolocation/category-use-geolocation"; +import { useComponentSizeCategory } from "#/src/ui/cuicui/hooks/use-component-size/category.use-component-size"; +import { useKonamiCodeCategory } from "#/src/ui/cuicui/hooks/use-konami-code/category.use-konami-code"; +import { useLocalStorageCategory } from "#/src/ui/cuicui/hooks/use-local-storage/category.use-local-storage"; +import { useWindowSizeCategory } from "#/src/ui/cuicui/hooks/use-window-size/category.use-window-size"; +import { useWindowScrollPositionCategory } from "#/src/ui/cuicui/hooks/use-window-scroll-position/category.use-window-scroll-postion"; +import { useThrottleCategory } from "#/src/ui/cuicui/hooks/use-throttle/category.use-throttle"; +import { useStepCategory } from "#/src/ui/cuicui/hooks/use-step/category.use-step"; +import { useSessionStorageCategory } from "#/src/ui/cuicui/hooks/use-session-storage/category.use-session-storage"; +import { useOnlineStatusCategory } from "#/src/ui/cuicui/hooks/use-online-status/category.use-online-status"; +import { useLocationCategory } from "#/src/ui/cuicui/hooks/use-location/category.use-location"; +import { useIsomorphicLayoutEffectCategory } from "#/src/ui/cuicui/hooks/use-isomorphic-layout-effect/category.use-isomorphic-layout-effect"; +import { useCopyToClipboardCategory } from "#/src/ui/cuicui/hooks/use-copy-to-clipboard/category.use-copy-to-clipboard"; +import { useEventListenerCategory } from "#/src/ui/cuicui/hooks/use-event-listener/category.use-event-listener"; +import { useEventCallbackCategory } from "#/src/ui/cuicui/hooks/use-event-callback/category.use-event-callback"; +import { useSpeechToTextCategory } from "#/src/ui/cuicui/hooks/use-speech-to-text/category.use-speech-to-text"; +import { useTextToSpeechCategory } from "#/src/ui/cuicui/hooks/use-text-to-speech/category.use-text-to-speech"; export const hooksSection: SectionType = { + type: "single-component", name: "Hooks", slug: "hooks", categoriesList: [ useBatteryCategory, - useDebounceCategory, - { - slug: "picture-in-picture", - name: "Picture in Picture", - description: "Create a picture in picture mode for your videos", - releaseDateCategory: new Date(), - icon: PictureInPictureIcon, - comingSoonCategory: true, - componentList: null, - }, - useNetworkCategory, + useComponentSizeCategory, useCopyToClipboardCategory, useCounterCategory, + useDebounceCategory, + useEventCallbackCategory, + useEventListenerCategory, + useGeoLocationCategory, useInViewCategory, + useInputValueCategory, + useIsomorphicLayoutEffectCategory, + useKeyPressCategory, + useKonamiCodeCategory, + useLocalStorageCategory, + useLocationCategory, useMouseCategory, + useNetworkCategory, + useOnlineStatusCategory, + useSpeechToTextCategory, + useSessionStorageCategory, + useStepCategory, + useTextToSpeechCategory, + useThrottleCategory, + useWindowScrollPositionCategory, + useWindowSizeCategory, ], }; diff --git a/src/ui/cuicui/hooks/use-battery/hook/preview-use-battery.tsx b/src/ui/cuicui/hooks/use-battery/preview-use-battery.tsx similarity index 93% rename from src/ui/cuicui/hooks/use-battery/hook/preview-use-battery.tsx rename to src/ui/cuicui/hooks/use-battery/preview-use-battery.tsx index 6159098b..572ea780 100644 --- a/src/ui/cuicui/hooks/use-battery/hook/preview-use-battery.tsx +++ b/src/ui/cuicui/hooks/use-battery/preview-use-battery.tsx @@ -1,7 +1,7 @@ "use client"; import { BatteryIndicator } from "#/src/ui/cuicui/application-ui/battery/battery-indicator/battery-indicator"; import { ThreeDotSimpleLoader } from "#/src/ui/cuicui/common-ui/loaders/three-dot-simple-loader/three-dot-simple-loader"; // Allows to wait for the battery information to load -import { useBattery } from "#/src/ui/cuicui/hooks/use-battery/hook/use-battery"; +import { useBattery } from "#/src/ui/cuicui/hooks/use-battery/use-battery"; export const PreviewUseBattery = () => { const { diff --git a/src/ui/cuicui/hooks/use-battery/use-battery.category.tsx b/src/ui/cuicui/hooks/use-battery/use-battery.category.tsx index d3eb8218..58487855 100644 --- a/src/ui/cuicui/hooks/use-battery/use-battery.category.tsx +++ b/src/ui/cuicui/hooks/use-battery/use-battery.category.tsx @@ -1,8 +1,8 @@ import { BatteryCharging } from "lucide-react"; -import type { CategoryType } from "#/src/lib/types/component"; -import { PreviewUseBattery } from "#/src/ui/cuicui/hooks/use-battery/hook/preview-use-battery"; +import type { SingleComponentCategoryType } from "#/src/lib/types/component"; +import { PreviewUseBattery } from "#/src/ui/cuicui/hooks/use-battery/preview-use-battery"; -export const useBatteryCategory: CategoryType = { +export const useBatteryCategory: SingleComponentCategoryType = { slug: "use-battery", name: "Use Battery", description: "A hook to get battery informations", @@ -12,23 +12,18 @@ export const useBatteryCategory: CategoryType = { component: , previewScale: 1, }, - componentList: [ - { - rerenderButton: true, - lastUpdatedDateComponent: new Date("2024-09-16"), - sizePreview: "lg", - slug: "hook", - isIframed: false, - title: "Use Battery", - description: "A hook to get battery informations", - variantList: [ - { - name: "variant 1", - component: , - slugComponentFile: "use-battery", - slugPreviewFile: "preview-use-battery", - }, - ], - }, - ], + component: { + rerenderButton: true, + lastUpdatedDateComponent: new Date("2024-09-16"), + sizePreview: "lg", + isIframed: false, + variantList: [ + { + name: "variant 1", + component: , + slugComponentFile: "use-battery", + slugPreviewFile: "preview-use-battery", + }, + ], + }, }; diff --git a/src/ui/cuicui/hooks/use-battery/hook/use-battery.tsx b/src/ui/cuicui/hooks/use-battery/use-battery.tsx similarity index 100% rename from src/ui/cuicui/hooks/use-battery/hook/use-battery.tsx rename to src/ui/cuicui/hooks/use-battery/use-battery.tsx diff --git a/src/ui/cuicui/hooks/use-component-size/category.use-component-size.tsx b/src/ui/cuicui/hooks/use-component-size/category.use-component-size.tsx new file mode 100644 index 00000000..d3475d18 --- /dev/null +++ b/src/ui/cuicui/hooks/use-component-size/category.use-component-size.tsx @@ -0,0 +1,28 @@ +import type { SingleComponentCategoryType } from "#/src/lib/types/component"; +import { PreviewUseComponentSize } from "#/src/ui/cuicui/hooks/use-component-size/preview-use-component-size"; +import { RulerIcon } from "lucide-react"; + +export const useComponentSizeCategory: SingleComponentCategoryType = { + slug: "use-component-size", + name: "Use Component Size", + description: "A hook that allows you to manage the size of a component", + releaseDateCategory: new Date("2024-09-28"), + icon: RulerIcon, + previewCategory: { + component: , + previewScale: 0.8, + }, + component: { + lastUpdatedDateComponent: new Date("2024-09-28"), + sizePreview: "sm", + isIframed: false, + variantList: [ + { + name: "default variant", + component: , + slugComponentFile: "use-component-size", + slugPreviewFile: "preview-use-component-size", + }, + ], + }, +}; diff --git a/src/ui/cuicui/hooks/use-component-size/preview-use-component-size.tsx b/src/ui/cuicui/hooks/use-component-size/preview-use-component-size.tsx new file mode 100644 index 00000000..4802aa55 --- /dev/null +++ b/src/ui/cuicui/hooks/use-component-size/preview-use-component-size.tsx @@ -0,0 +1,29 @@ +"use client"; +import { useComponentSize } from "#/src/ui/cuicui/hooks/use-component-size/use-component-size"; +import { useRef } from "react"; + +export const PreviewUseComponentSize = () => { + const divRef = useRef(null); + const { width, height } = useComponentSize(divRef); + + return ( +
+
+ Resize me! +
+

+ Width: {width}px, Height: {height}px +

+
+ ); +}; diff --git a/src/ui/cuicui/hooks/use-component-size/use-component-size.ts b/src/ui/cuicui/hooks/use-component-size/use-component-size.ts new file mode 100644 index 00000000..830de0aa --- /dev/null +++ b/src/ui/cuicui/hooks/use-component-size/use-component-size.ts @@ -0,0 +1,77 @@ +"use client"; + +import { useState, useLayoutEffect, type RefObject, useCallback } from "react"; + +// Define a type for the size object +interface Size { + width: number; + height: number; +} + +/** + * Custom hook to track the size of a DOM element. + * + * @param ref - RefObject pointing to the DOM element. + * @returns An object containing the width and height of the element. + */ +export function useComponentSize( + ref: RefObject, +): Size { + const getSize = useCallback((element: T | null): Size => { + if (!element) { + return { width: 0, height: 0 }; + } + + return { + width: element.offsetWidth, + height: element.offsetHeight, + }; + }, []); + + const [size, setSize] = useState(() => getSize(ref.current)); + + useLayoutEffect(() => { + if (!ref.current) { + return; + } + + const handleResize = (entries: ResizeObserverEntry[]) => { + if (!Array.isArray(entries)) { + return; + } + const entry = entries[0]; + if (entry) { + const { width, height } = entry.contentRect; + setSize({ width, height }); + } + }; + + // Check if ResizeObserver is supported + if (typeof ResizeObserver !== "undefined") { + const resizeObserver = new ResizeObserver(handleResize); + resizeObserver.observe(ref.current); + + // Initialize size + setSize(getSize(ref.current)); + + return () => { + resizeObserver.disconnect(); + }; + } + // Fallback to window resize event + const handleWindowResize = () => { + setSize(getSize(ref.current)); + }; + + window.addEventListener("resize", handleWindowResize); + + // Initialize size + handleWindowResize(); + + return () => { + window.removeEventListener("resize", handleWindowResize); + }; + }, [ref, getSize]); + + return size; +} diff --git a/src/ui/cuicui/hooks/use-copy-to-clipboard/category.use-copy-to-clipboard.tsx b/src/ui/cuicui/hooks/use-copy-to-clipboard/category.use-copy-to-clipboard.tsx new file mode 100644 index 00000000..1d948333 --- /dev/null +++ b/src/ui/cuicui/hooks/use-copy-to-clipboard/category.use-copy-to-clipboard.tsx @@ -0,0 +1,27 @@ +import { ClipboardCopyIcon } from "lucide-react"; +import type { SingleComponentCategoryType } from "#/src/lib/types/component"; +import PreviewCopyToClipboard from "#/src/ui/cuicui/hooks/use-copy-to-clipboard/preview-copy-to-clipboard"; + +export const useCopyToClipboardCategory: SingleComponentCategoryType = { + slug: "use-copy-to-clipboard", + name: "Use Copy to Clipboard", + description: "A hook that allows you to copy text to the clipboard", + releaseDateCategory: new Date("2024-08-20"), + icon: ClipboardCopyIcon, + previewCategory: { + component: , + previewScale: 0.8, + }, + component: { + sizePreview: "lg", + isIframed: false, + variantList: [ + { + name: "variant 1", + component: , + slugComponentFile: "use-copy-to-clipboard", + slugPreviewFile: "preview-copy-to-clipboard", + }, + ], + }, +}; diff --git a/src/ui/cuicui/hooks/use-copy-to-clipboard/hook/preview-copy-to-clipboard.tsx b/src/ui/cuicui/hooks/use-copy-to-clipboard/preview-copy-to-clipboard.tsx similarity index 96% rename from src/ui/cuicui/hooks/use-copy-to-clipboard/hook/preview-copy-to-clipboard.tsx rename to src/ui/cuicui/hooks/use-copy-to-clipboard/preview-copy-to-clipboard.tsx index e2fb600f..f97734de 100644 --- a/src/ui/cuicui/hooks/use-copy-to-clipboard/hook/preview-copy-to-clipboard.tsx +++ b/src/ui/cuicui/hooks/use-copy-to-clipboard/preview-copy-to-clipboard.tsx @@ -1,6 +1,6 @@ "use client"; -import { useCopyToClipboard } from "#/src/ui/cuicui/hooks/use-copy-to-clipboard/hook/use-copy-to-clipboard"; +import { useCopyToClipboard } from "#/src/ui/cuicui/hooks/use-copy-to-clipboard/use-copy-to-clipboard"; export default function PreviewCopyToClipboard() { const [copiedText, copy] = useCopyToClipboard(); diff --git a/src/ui/cuicui/hooks/use-copy-to-clipboard/use-copy-to-clipboard.component.tsx b/src/ui/cuicui/hooks/use-copy-to-clipboard/use-copy-to-clipboard.component.tsx deleted file mode 100644 index e9e13d93..00000000 --- a/src/ui/cuicui/hooks/use-copy-to-clipboard/use-copy-to-clipboard.component.tsx +++ /dev/null @@ -1,32 +0,0 @@ -import { ClipboardCopyIcon } from "lucide-react"; -import type { CategoryType } from "#/src/lib/types/component"; -import PreviewCopyToClipboard from "#/src/ui/cuicui/hooks/use-copy-to-clipboard/hook/preview-copy-to-clipboard"; - -export const useCopyToClipboardCategory: CategoryType = { - slug: "use-copy-to-clipboard", - name: "Copy to Clipboard", - description: "A hook that allows you to copy text to the clipboard", - releaseDateCategory: new Date("2024-08-20"), - icon: ClipboardCopyIcon, - previewCategory: { - component: , - previewScale: 0.8, - }, - componentList: [ - { - sizePreview: "lg", - slug: "hook", - isIframed: false, - title: "Copy to Clipboard", - description: "A hook that allows you to copy text to the clipboard", - variantList: [ - { - name: "variant 1", - component: , - slugComponentFile: "use-copy-to-clipboard", - slugPreviewFile: "preview-copy-to-clipboard", - }, - ], - }, - ], -}; diff --git a/src/ui/cuicui/hooks/use-copy-to-clipboard/hook/use-copy-to-clipboard.tsx b/src/ui/cuicui/hooks/use-copy-to-clipboard/use-copy-to-clipboard.tsx similarity index 100% rename from src/ui/cuicui/hooks/use-copy-to-clipboard/hook/use-copy-to-clipboard.tsx rename to src/ui/cuicui/hooks/use-copy-to-clipboard/use-copy-to-clipboard.tsx diff --git a/src/ui/cuicui/hooks/use-counter/category.use-counter.tsx b/src/ui/cuicui/hooks/use-counter/category.use-counter.tsx new file mode 100644 index 00000000..058ddbc3 --- /dev/null +++ b/src/ui/cuicui/hooks/use-counter/category.use-counter.tsx @@ -0,0 +1,28 @@ +import { CalculatorIcon } from "lucide-react"; +import type { SingleComponentCategoryType } from "#/src/lib/types/component"; +import PreviewUseCounter from "#/src/ui/cuicui/hooks/use-counter/preview-use-counter"; + +export const useCounterCategory: SingleComponentCategoryType = { + slug: "use-counter", + name: "Use counter", + description: "A hook that allows you to count easily", + releaseDateCategory: new Date("2024-09-16"), + icon: CalculatorIcon, + previewCategory: { + component: , + previewScale: 1, + }, + component: { + lastUpdatedDateComponent: new Date("2024-09-16"), + sizePreview: "lg", + isIframed: false, + variantList: [ + { + name: "variant 1", + component: , + slugComponentFile: "use-counter", + slugPreviewFile: "preview-use-counter", + }, + ], + }, +}; diff --git a/src/ui/cuicui/hooks/use-counter/hook/preview-use-counter.tsx b/src/ui/cuicui/hooks/use-counter/preview-use-counter.tsx similarity index 94% rename from src/ui/cuicui/hooks/use-counter/hook/preview-use-counter.tsx rename to src/ui/cuicui/hooks/use-counter/preview-use-counter.tsx index 7c5bd378..c996ad8f 100644 --- a/src/ui/cuicui/hooks/use-counter/hook/preview-use-counter.tsx +++ b/src/ui/cuicui/hooks/use-counter/preview-use-counter.tsx @@ -1,6 +1,6 @@ "use client"; import { BeforeEffectButton } from "#/src/ui/cuicui/common-ui/buttons/before-effect-button/before-effect-button"; -import { useCounter } from "#/src/ui/cuicui/hooks/use-counter/hook/use-counter"; +import { useCounter } from "#/src/ui/cuicui/hooks/use-counter/use-counter"; export default function PreviewUseCounter() { const [count, { increment, decrement, set, reset }] = useCounter(6, { diff --git a/src/ui/cuicui/hooks/use-counter/use-counter.category.tsx b/src/ui/cuicui/hooks/use-counter/use-counter.category.tsx deleted file mode 100644 index ff1b0972..00000000 --- a/src/ui/cuicui/hooks/use-counter/use-counter.category.tsx +++ /dev/null @@ -1,33 +0,0 @@ -import { CalculatorIcon } from "lucide-react"; -import type { CategoryType } from "#/src/lib/types/component"; -import PreviewUseCounter from "#/src/ui/cuicui/hooks/use-counter/hook/preview-use-counter"; - -export const useCounterCategory: CategoryType = { - slug: "use-counter", - name: "Use counter", - description: "A hook that allows you to count easily", - releaseDateCategory: new Date("2024-09-16"), - icon: CalculatorIcon, - previewCategory: { - component: , - previewScale: 1, - }, - componentList: [ - { - lastUpdatedDateComponent: new Date("2024-09-16"), - sizePreview: "lg", - slug: "hook", - isIframed: false, - title: "Use counter", - description: "A hook that allows you to count easily", - variantList: [ - { - name: "variant 1", - component: , - slugComponentFile: "use-counter", - slugPreviewFile: "preview-use-counter", - }, - ], - }, - ], -}; diff --git a/src/ui/cuicui/hooks/use-counter/hook/use-counter.tsx b/src/ui/cuicui/hooks/use-counter/use-counter.tsx similarity index 100% rename from src/ui/cuicui/hooks/use-counter/hook/use-counter.tsx rename to src/ui/cuicui/hooks/use-counter/use-counter.tsx diff --git a/src/ui/cuicui/hooks/use-debounce/category.use-debounce.tsx b/src/ui/cuicui/hooks/use-debounce/category.use-debounce.tsx new file mode 100644 index 00000000..68bcd0c9 --- /dev/null +++ b/src/ui/cuicui/hooks/use-debounce/category.use-debounce.tsx @@ -0,0 +1,28 @@ +import { TimerIcon } from "lucide-react"; +import type { SingleComponentCategoryType } from "#/src/lib/types/component"; +import PreviewUseDebounce from "#/src/ui/cuicui/hooks/use-debounce/preview-use-debounce"; + +export const useDebounceCategory: SingleComponentCategoryType = { + slug: "use-debounce", + name: "Use debounce", + description: "A hook that allows you to debounce the value of an input", + releaseDateCategory: new Date("2024-09-16"), + icon: TimerIcon, + previewCategory: { + component: , + previewScale: 1, + }, + component: { + lastUpdatedDateComponent: new Date("2024-09-16"), + sizePreview: "lg", + isIframed: false, + variantList: [ + { + name: "variant 1", + component: , + slugComponentFile: "use-debounce", + slugPreviewFile: "preview-use-debounce", + }, + ], + }, +}; diff --git a/src/ui/cuicui/hooks/use-debounce/hook/preview-use-debounce.tsx b/src/ui/cuicui/hooks/use-debounce/preview-use-debounce.tsx similarity index 98% rename from src/ui/cuicui/hooks/use-debounce/hook/preview-use-debounce.tsx rename to src/ui/cuicui/hooks/use-debounce/preview-use-debounce.tsx index 465b64a2..90e797dc 100644 --- a/src/ui/cuicui/hooks/use-debounce/hook/preview-use-debounce.tsx +++ b/src/ui/cuicui/hooks/use-debounce/preview-use-debounce.tsx @@ -1,7 +1,7 @@ "use client"; import { useState } from "react"; import { ModernSimpleInput } from "#/src/ui/cuicui/common-ui/inputs/modern-simple-input/modern-simple-input"; -import { useDebounce } from "#/src/ui/cuicui/hooks/use-debounce/hook/use-debounce"; +import { useDebounce } from "#/src/ui/cuicui/hooks/use-debounce/use-debounce"; export default function PreviewUseDebounce() { const [value, setValue] = useState(""); diff --git a/src/ui/cuicui/hooks/use-debounce/use-debounce.category.tsx b/src/ui/cuicui/hooks/use-debounce/use-debounce.category.tsx deleted file mode 100644 index 96553193..00000000 --- a/src/ui/cuicui/hooks/use-debounce/use-debounce.category.tsx +++ /dev/null @@ -1,33 +0,0 @@ -import { TimerIcon } from "lucide-react"; -import type { CategoryType } from "#/src/lib/types/component"; -import PreviewUseDebounce from "#/src/ui/cuicui/hooks/use-debounce/hook/preview-use-debounce"; - -export const useDebounceCategory: CategoryType = { - slug: "use-debounce", - name: "Use debounce", - description: "A hook that allows you to debounce the value of an input", - releaseDateCategory: new Date("2024-09-16"), - icon: TimerIcon, - previewCategory: { - component: , - previewScale: 1, - }, - componentList: [ - { - lastUpdatedDateComponent: new Date("2024-09-16"), - sizePreview: "lg", - slug: "hook", - isIframed: false, - title: "Use in View", - description: "A hook that allows you to debounce the value of an input", - variantList: [ - { - name: "variant 1", - component: , - slugComponentFile: "use-debounce", - slugPreviewFile: "preview-use-debounce", - }, - ], - }, - ], -}; diff --git a/src/ui/cuicui/hooks/use-debounce/hook/use-debounce.tsx b/src/ui/cuicui/hooks/use-debounce/use-debounce.tsx similarity index 100% rename from src/ui/cuicui/hooks/use-debounce/hook/use-debounce.tsx rename to src/ui/cuicui/hooks/use-debounce/use-debounce.tsx diff --git a/src/ui/cuicui/hooks/use-event-callback/category.use-event-callback.tsx b/src/ui/cuicui/hooks/use-event-callback/category.use-event-callback.tsx new file mode 100644 index 00000000..fb893e2b --- /dev/null +++ b/src/ui/cuicui/hooks/use-event-callback/category.use-event-callback.tsx @@ -0,0 +1,28 @@ +import type { SingleComponentCategoryType } from "#/src/lib/types/component"; +import { PreviewUseEventCallback } from "#/src/ui/cuicui/hooks/use-event-callback/preview-use-event-callback"; +import { SquareFunctionIcon } from "lucide-react"; + +export const useEventCallbackCategory: SingleComponentCategoryType = { + slug: "use-event-callback", + name: "Use Event Callback", + description: "A hook that returns a memoized event callback", + releaseDateCategory: new Date("2024-09-28"), + icon: SquareFunctionIcon, + previewCategory: { + component: , + previewScale: 0.8, + }, + component: { + lastUpdatedDateComponent: new Date("2024-09-28"), + sizePreview: "sm", + isIframed: false, + variantList: [ + { + name: "variant 1", + component: , + slugComponentFile: "use-event-callback", + slugPreviewFile: "preview-use-event-callback", + }, + ], + }, +}; diff --git a/src/ui/cuicui/hooks/use-event-callback/preview-use-event-callback.tsx b/src/ui/cuicui/hooks/use-event-callback/preview-use-event-callback.tsx new file mode 100644 index 00000000..1240764e --- /dev/null +++ b/src/ui/cuicui/hooks/use-event-callback/preview-use-event-callback.tsx @@ -0,0 +1,3 @@ +export function PreviewUseEventCallback() { + return
preview-use-event-callback
; +} diff --git a/src/ui/cuicui/hooks/use-event-callback/use-event-callback.ts b/src/ui/cuicui/hooks/use-event-callback/use-event-callback.ts new file mode 100644 index 00000000..534261ac --- /dev/null +++ b/src/ui/cuicui/hooks/use-event-callback/use-event-callback.ts @@ -0,0 +1,26 @@ +import { useIsomorphicLayoutEffect } from "#/src/ui/cuicui/hooks/use-isomorphic-layout-effect/use-isomorphic-layout-effect"; +import { useCallback, useRef } from "react"; + +export function useEventCallback( + fn: (...args: Args) => R, +): (...args: Args) => R; +export function useEventCallback( + fn: ((...args: Args) => R) | undefined, +): ((...args: Args) => R) | undefined; +export function useEventCallback( + fn: ((...args: Args) => R) | undefined, +): ((...args: Args) => R) | undefined { + const ref = useRef(() => { + throw new Error("Cannot call an event handler while rendering."); + }); + + useIsomorphicLayoutEffect(() => { + ref.current = fn; + }, [fn]); + + //!! To check + // biome-ignore lint/correctness/useExhaustiveDependencies: + return useCallback((...args: Args) => ref.current?.(...args), [ref]) as ( + ...args: Args + ) => R; +} diff --git a/src/ui/cuicui/hooks/use-event-listener/category.use-event-listener.tsx b/src/ui/cuicui/hooks/use-event-listener/category.use-event-listener.tsx new file mode 100644 index 00000000..40240ade --- /dev/null +++ b/src/ui/cuicui/hooks/use-event-listener/category.use-event-listener.tsx @@ -0,0 +1,29 @@ +import { EarIcon } from "lucide-react"; +import type { SingleComponentCategoryType } from "#/src/lib/types/component"; +import { PreviewUseEventListener } from "#/src/ui/cuicui/hooks/use-event-listener/preview-use-event-listener"; + +export const useEventListenerCategory: SingleComponentCategoryType = { + slug: "use-event-listener", + name: "Use Event Listener", + description: + "A hook that allows you to manage event listeners on DOM elements", + releaseDateCategory: new Date("2024-09-28"), + icon: EarIcon, + previewCategory: { + component: , + previewScale: 0.8, + }, + component: { + lastUpdatedDateComponent: new Date("2024-09-28"), + sizePreview: "sm", + isIframed: false, + variantList: [ + { + name: "Default Variant", + component: , + slugComponentFile: "use-event-listener", + slugPreviewFile: "preview-use-event-listener", + }, + ], + }, +}; diff --git a/src/ui/cuicui/hooks/use-event-listener/preview-use-event-listener.tsx b/src/ui/cuicui/hooks/use-event-listener/preview-use-event-listener.tsx new file mode 100644 index 00000000..dde870a5 --- /dev/null +++ b/src/ui/cuicui/hooks/use-event-listener/preview-use-event-listener.tsx @@ -0,0 +1,34 @@ +"use client"; +import { useEventListener } from "#/src/ui/cuicui/hooks/use-event-listener/use-event-listener"; +import { useState } from "react"; +export const PreviewUseEventListener = () => { + // Initialize state with current window dimensions + if (typeof window === "undefined") { + return null; + } + const [windowSize, setWindowSize] = useState({ + width: window.innerWidth, + height: window.innerHeight, + }); + + // Handler function to update state on resize + const handleResize = () => { + setWindowSize({ + width: window.innerWidth, + height: window.innerHeight, + }); + }; + + // Use the custom useEventListener hook to listen for 'resize' events on the window + useEventListener("resize", handleResize); + + return ( +
+

Window Size Demo

+
+

Width: {windowSize.width}px

+

Height: {windowSize.height}px

+
+
+ ); +}; diff --git a/src/ui/cuicui/hooks/use-event-listener/use-event-listener.ts b/src/ui/cuicui/hooks/use-event-listener/use-event-listener.ts new file mode 100644 index 00000000..80e7d5c1 --- /dev/null +++ b/src/ui/cuicui/hooks/use-event-listener/use-event-listener.ts @@ -0,0 +1,93 @@ +"use client"; +import { useEffect, useRef } from "react"; + +import type { RefObject } from "react"; + +import { useIsomorphicLayoutEffect } from "#/src/ui/cuicui/hooks/use-isomorphic-layout-effect/use-isomorphic-layout-effect"; +// MediaQueryList Event based useEventListener interface +function useEventListener( + eventName: K, + handler: (event: MediaQueryListEventMap[K]) => void, + element: RefObject, + options?: boolean | AddEventListenerOptions, +): void; + +// Window Event based useEventListener interface +function useEventListener( + eventName: K, + handler: (event: WindowEventMap[K]) => void, + element?: undefined, + options?: boolean | AddEventListenerOptions, +): void; + +// Element Event based useEventListener interface +function useEventListener< + K extends keyof HTMLElementEventMap & keyof SVGElementEventMap, + T extends Element = K extends keyof HTMLElementEventMap + ? HTMLDivElement + : SVGElement, +>( + eventName: K, + handler: + | ((event: HTMLElementEventMap[K]) => void) + | ((event: SVGElementEventMap[K]) => void), + element: RefObject, + options?: boolean | AddEventListenerOptions, +): void; + +// Document Event based useEventListener interface +function useEventListener( + eventName: K, + handler: (event: DocumentEventMap[K]) => void, + element: RefObject, + options?: boolean | AddEventListenerOptions, +): void; + +function useEventListener< + KW extends keyof WindowEventMap, + KH extends keyof HTMLElementEventMap & keyof SVGElementEventMap, + KM extends keyof MediaQueryListEventMap, + T extends HTMLElement | SVGAElement | MediaQueryList = HTMLElement, +>( + eventName: KW | KH | KM, + handler: ( + event: + | WindowEventMap[KW] + | HTMLElementEventMap[KH] + | SVGElementEventMap[KH] + | MediaQueryListEventMap[KM] + | Event, + ) => void, + element?: RefObject, + options?: boolean | AddEventListenerOptions, +) { + // Create a ref that stores handler + const savedHandler = useRef(handler); + + useIsomorphicLayoutEffect(() => { + savedHandler.current = handler; + }, [handler]); + + useEffect(() => { + // Define the listening target + const targetElement: T | Window = element?.current ?? window; + + if (!targetElement?.addEventListener) { + return; + } + + // Create event listener that calls handler function stored in ref + const listener: typeof handler = (event) => { + savedHandler.current(event); + }; + + targetElement.addEventListener(eventName, listener, options); + + // Remove event listener on cleanup + return () => { + targetElement.removeEventListener(eventName, listener, options); + }; + }, [eventName, element, options]); +} + +export { useEventListener }; diff --git a/src/ui/cuicui/hooks/use-geolocation/category-use-geolocation.tsx b/src/ui/cuicui/hooks/use-geolocation/category-use-geolocation.tsx new file mode 100644 index 00000000..ace999c5 --- /dev/null +++ b/src/ui/cuicui/hooks/use-geolocation/category-use-geolocation.tsx @@ -0,0 +1,28 @@ +import { Globe2Icon } from "lucide-react"; +import type { SingleComponentCategoryType } from "#/src/lib/types/component"; +import { PreviewUseGeolocation } from "#/src/ui/cuicui/hooks/use-geolocation/preview-use-geolocation"; +export const useGeoLocationCategory: SingleComponentCategoryType = { + slug: "use-geolocation", + name: "Use GeoLocation", + description: + "A hook that allows you to access and manage the user's geolocation data", + releaseDateCategory: new Date("2024-09-28"), + icon: Globe2Icon, + previewCategory: { + component: , + previewScale: 0.8, + }, + component: { + lastUpdatedDateComponent: new Date("2024-09-28"), + sizePreview: "sm", + isIframed: false, + variantList: [ + { + name: "variant 1", + component: , + slugComponentFile: "use-geolocation", + slugPreviewFile: "preview-use-geolocation", + }, + ], + }, +}; diff --git a/src/ui/cuicui/hooks/use-geolocation/preview-use-geolocation.tsx b/src/ui/cuicui/hooks/use-geolocation/preview-use-geolocation.tsx new file mode 100644 index 00000000..d5c39aae --- /dev/null +++ b/src/ui/cuicui/hooks/use-geolocation/preview-use-geolocation.tsx @@ -0,0 +1,8 @@ +"use client"; +import { useGeolocation } from "#/src/ui/cuicui/hooks/use-geolocation/use-geolocation"; + +export function PreviewUseGeolocation() { + const state = useGeolocation(); + + return
{JSON.stringify(state, null, 2)}
; +} diff --git a/src/ui/cuicui/hooks/use-geolocation/use-geolocation.tsx b/src/ui/cuicui/hooks/use-geolocation/use-geolocation.tsx new file mode 100644 index 00000000..d90cf1cd --- /dev/null +++ b/src/ui/cuicui/hooks/use-geolocation/use-geolocation.tsx @@ -0,0 +1,97 @@ +import { useState, useEffect, useRef } from "react"; + +// Define the shape of the geolocation data +interface GeolocationData { + accuracy: number; + altitude: number | null; + altitudeAccuracy: number | null; + heading: number | null; + latitude: number; + longitude: number; + speed: number | null; + timestamp: number; +} + +// Define possible geolocation errors +interface GeolocationError { + code: number; + message: string; +} + +// Define the return type of the hook +interface UseGeolocationReturn { + data: GeolocationData | null; + error: GeolocationError | null; + isLoading: boolean; +} + +export const useGeolocation = (): UseGeolocationReturn => { + const [data, setData] = useState(null); + const [error, setError] = useState(null); + const [isLoading, setIsLoading] = useState(true); + const watchIdRef = useRef(null); + + useEffect(() => { + if (!navigator.geolocation) { + setError({ + code: 0, + message: "Geolocation is not supported by your browser.", + }); + setIsLoading(false); + return; + } + + // Success handler + const handleSuccess = (position: GeolocationPosition) => { + const { + accuracy, + altitude, + altitudeAccuracy, + heading, + latitude, + longitude, + speed, + } = position.coords; + const { timestamp } = position; + + setData({ + accuracy, + altitude, + altitudeAccuracy, + heading, + latitude, + longitude, + speed, + timestamp, + }); + setIsLoading(false); + }; + + // Error handler + const handleError = (geolocationError: GeolocationPositionError) => { + setError({ + code: geolocationError.code, + message: geolocationError.message, + }); + setIsLoading(false); + }; + + // Get the current position + navigator.geolocation.getCurrentPosition(handleSuccess, handleError); + + // Watch for position changes + watchIdRef.current = navigator.geolocation.watchPosition( + handleSuccess, + handleError, + ); + + // Cleanup function to clear the watch on unmount + return () => { + if (watchIdRef.current !== null) { + navigator.geolocation.clearWatch(watchIdRef.current); + } + }; + }, []); + + return { data, error, isLoading }; +}; diff --git a/src/ui/cuicui/hooks/use-in-view/category.use-in-view.tsx b/src/ui/cuicui/hooks/use-in-view/category.use-in-view.tsx new file mode 100644 index 00000000..3ccad218 --- /dev/null +++ b/src/ui/cuicui/hooks/use-in-view/category.use-in-view.tsx @@ -0,0 +1,32 @@ +import { ViewIcon } from "lucide-react"; +import PreviewImageUseInView from "#/src/assets/components-preview/use-in-view.png"; +import type { SingleComponentCategoryType } from "#/src/lib/types/component"; +import { PreviewUseInView } from "#/src/ui/cuicui/hooks/use-in-view/preview-in-view"; +export const useInViewCategory: SingleComponentCategoryType = { + slug: "use-in-view", + name: "Use in View", + description: + "A hook that allows you to know if an element is in the viewport", + releaseDateCategory: new Date("2024-08-28"), + icon: ViewIcon, + previewCategory: { + component: , + previewImage: PreviewImageUseInView, + previewScale: 0.8, + }, + component: { + lastUpdatedDateComponent: new Date("2024-08-28"), + sizePreview: "lg", + + isIframed: true, + + variantList: [ + { + name: "variant 1", + component: , + slugComponentFile: "use-in-view", + slugPreviewFile: "preview-in-view", + }, + ], + }, +}; diff --git a/src/ui/cuicui/hooks/use-in-view/hook/preview-in-view.tsx b/src/ui/cuicui/hooks/use-in-view/preview-in-view.tsx similarity index 96% rename from src/ui/cuicui/hooks/use-in-view/hook/preview-in-view.tsx rename to src/ui/cuicui/hooks/use-in-view/preview-in-view.tsx index 05d0def9..44e3c3b9 100644 --- a/src/ui/cuicui/hooks/use-in-view/hook/preview-in-view.tsx +++ b/src/ui/cuicui/hooks/use-in-view/preview-in-view.tsx @@ -1,7 +1,7 @@ "use client"; import { useRef } from "react"; -import useInView from "#/src/ui/cuicui/hooks/use-in-view/hook/use-in-view"; +import useInView from "#/src/ui/cuicui/hooks/use-in-view/use-in-view"; export const PreviewUseInView = () => { const ref1 = useRef(null); diff --git a/src/ui/cuicui/hooks/use-in-view/use-in-view.category.tsx b/src/ui/cuicui/hooks/use-in-view/use-in-view.category.tsx deleted file mode 100644 index 45fecbc5..00000000 --- a/src/ui/cuicui/hooks/use-in-view/use-in-view.category.tsx +++ /dev/null @@ -1,36 +0,0 @@ -import { ViewIcon } from "lucide-react"; -import PreviewImageUseInView from "#/src/assets/components-preview/use-in-view.png"; -import type { CategoryType } from "#/src/lib/types/component"; -import { PreviewUseInView } from "#/src/ui/cuicui/hooks/use-in-view/hook/preview-in-view"; -export const useInViewCategory: CategoryType = { - slug: "use-in-view", - name: "Use in View", - description: - "A hook that allows you to know if an element is in the viewport", - releaseDateCategory: new Date("2024-08-28"), - icon: ViewIcon, - previewCategory: { - component: , - previewImage: PreviewImageUseInView, - previewScale: 0.8, - }, - componentList: [ - { - lastUpdatedDateComponent: new Date("2024-08-28"), - sizePreview: "lg", - slug: "hook", - isIframed: true, - title: "Use in View", - description: - "A hook that allows you to know if an element is in the viewport", - variantList: [ - { - name: "variant 1", - component: , - slugComponentFile: "use-in-view", - slugPreviewFile: "preview-in-view", - }, - ], - }, - ], -}; diff --git a/src/ui/cuicui/hooks/use-in-view/hook/use-in-view.tsx b/src/ui/cuicui/hooks/use-in-view/use-in-view.tsx similarity index 100% rename from src/ui/cuicui/hooks/use-in-view/hook/use-in-view.tsx rename to src/ui/cuicui/hooks/use-in-view/use-in-view.tsx diff --git a/src/ui/cuicui/hooks/use-input-value/category.use-input-value.tsx b/src/ui/cuicui/hooks/use-input-value/category.use-input-value.tsx new file mode 100644 index 00000000..b8973334 --- /dev/null +++ b/src/ui/cuicui/hooks/use-input-value/category.use-input-value.tsx @@ -0,0 +1,29 @@ +import { TextCursorInputIcon } from "lucide-react"; +import type { SingleComponentCategoryType } from "#/src/lib/types/component"; +import { PreviewUseInputValue } from "#/src/ui/cuicui/hooks/use-input-value/preview-use-input-value"; +export const useInputValueCategory: SingleComponentCategoryType = { + slug: "use-input-value", + name: "Use Input Value", + description: "A hook that allows you to manage the value of an input field", + releaseDateCategory: new Date("2024-09-28"), + icon: TextCursorInputIcon, + previewCategory: { + component: , + previewScale: 0.8, + }, + component: { + lastUpdatedDateComponent: new Date("2024-09-28"), + sizePreview: "sm", + + isIframed: false, + + variantList: [ + { + name: "variant 1", + component: , + slugComponentFile: "use-input-value", + slugPreviewFile: "preview-use-input-value", + }, + ], + }, +}; diff --git a/src/ui/cuicui/hooks/use-input-value/preview-use-input-value.tsx b/src/ui/cuicui/hooks/use-input-value/preview-use-input-value.tsx new file mode 100644 index 00000000..18089603 --- /dev/null +++ b/src/ui/cuicui/hooks/use-input-value/preview-use-input-value.tsx @@ -0,0 +1,8 @@ +"use client"; +import { useInputValue } from "#/src/ui/cuicui/hooks/use-input-value/use-input-value"; + +export function PreviewUseInputValue() { + const name = useInputValue("Cuicui"); + + return ; +} diff --git a/src/ui/cuicui/hooks/use-input-value/use-input-value.ts b/src/ui/cuicui/hooks/use-input-value/use-input-value.ts new file mode 100644 index 00000000..8d996d38 --- /dev/null +++ b/src/ui/cuicui/hooks/use-input-value/use-input-value.ts @@ -0,0 +1,15 @@ +"use client"; +import { type ChangeEvent, useState } from "react"; + +export function useInputValue(initialValue: string) { + const [value, setValue] = useState(initialValue); + + function onChange(event: ChangeEvent) { + setValue(event.currentTarget.value); + } + + return { + value, + onChange, + }; +} diff --git a/src/ui/cuicui/hooks/use-isomorphic-layout-effect/category.use-isomorphic-layout-effect.tsx b/src/ui/cuicui/hooks/use-isomorphic-layout-effect/category.use-isomorphic-layout-effect.tsx new file mode 100644 index 00000000..26a4a9bf --- /dev/null +++ b/src/ui/cuicui/hooks/use-isomorphic-layout-effect/category.use-isomorphic-layout-effect.tsx @@ -0,0 +1,29 @@ +import { ServerCogIcon } from "lucide-react"; +import type { SingleComponentCategoryType } from "#/src/lib/types/component"; +import { PreviewUseIsomorphicLayoutEffect } from "#/src/ui/cuicui/hooks/use-isomorphic-layout-effect/preview-use-isomorphic-layout-effect"; + +export const useIsomorphicLayoutEffectCategory: SingleComponentCategoryType = { + slug: "use-isomorphic-layout-effect", + name: "Use Isomorphic Layout Effect", + description: + "A hook that allows you to use the isomorphic version of useLayoutEffect, ensuring compatibility with server-side rendering.", + releaseDateCategory: new Date("2024-09-28"), + icon: ServerCogIcon, + previewCategory: { + component: , + previewScale: 0.8, + }, + component: { + lastUpdatedDateComponent: new Date("2024-09-28"), + sizePreview: "sm", + isIframed: false, + variantList: [ + { + name: "default", + component: , + slugComponentFile: "use-isomorphic-layout-effect", + slugPreviewFile: "preview-use-isomorphic-layout-effect", + }, + ], + }, +}; diff --git a/src/ui/cuicui/hooks/use-isomorphic-layout-effect/preview-use-isomorphic-layout-effect.tsx b/src/ui/cuicui/hooks/use-isomorphic-layout-effect/preview-use-isomorphic-layout-effect.tsx new file mode 100644 index 00000000..8af93ad5 --- /dev/null +++ b/src/ui/cuicui/hooks/use-isomorphic-layout-effect/preview-use-isomorphic-layout-effect.tsx @@ -0,0 +1,11 @@ +export function PreviewUseIsomorphicLayoutEffect() { + return ( + <> +
No preview, check the code to understand how it works
+

+ The useIsomorphicLayoutEffect hook is a custom hook that allows you to + use the useEffect hook in the server and client side. +

+ + ); +} diff --git a/src/ui/cuicui/hooks/use-isomorphic-layout-effect/use-isomorphic-layout-effect.ts b/src/ui/cuicui/hooks/use-isomorphic-layout-effect/use-isomorphic-layout-effect.ts new file mode 100644 index 00000000..ea6a0bc0 --- /dev/null +++ b/src/ui/cuicui/hooks/use-isomorphic-layout-effect/use-isomorphic-layout-effect.ts @@ -0,0 +1,4 @@ +import { useEffect, useLayoutEffect } from "react"; + +export const useIsomorphicLayoutEffect = + typeof window !== "undefined" ? useLayoutEffect : useEffect; diff --git a/src/ui/cuicui/hooks/use-key-press/category.use-key-press.tsx b/src/ui/cuicui/hooks/use-key-press/category.use-key-press.tsx new file mode 100644 index 00000000..4dd149ef --- /dev/null +++ b/src/ui/cuicui/hooks/use-key-press/category.use-key-press.tsx @@ -0,0 +1,29 @@ +import { KeyboardIcon } from "lucide-react"; + +import type { SingleComponentCategoryType } from "#/src/lib/types/component"; +import { PreviewUseKeyPress } from "#/src/ui/cuicui/hooks/use-key-press/preview-use-key-press"; +export const useKeyPressCategory: SingleComponentCategoryType = { + slug: "use-key-press", + name: "Use Key Press", + description: + "A hook that allows you to know if an element is in the viewport", + releaseDateCategory: new Date("2024-09-28"), + icon: KeyboardIcon, + previewCategory: { + component: , + previewScale: 0.8, + }, + component: { + lastUpdatedDateComponent: new Date("2024-09-28"), + sizePreview: "sm", + isIframed: false, + variantList: [ + { + name: "variant 1", + component: , + slugComponentFile: "use-key-press", + slugPreviewFile: "preview-use-key-press", + }, + ], + }, +}; diff --git a/src/ui/cuicui/hooks/use-key-press/preview-use-key-press.tsx b/src/ui/cuicui/hooks/use-key-press/preview-use-key-press.tsx new file mode 100644 index 00000000..e5dc34a3 --- /dev/null +++ b/src/ui/cuicui/hooks/use-key-press/preview-use-key-press.tsx @@ -0,0 +1,37 @@ +"use client"; +import { useKeyPress } from "#/src/ui/cuicui/hooks/use-key-press/use-key-press"; +import { useState } from "react"; + +export const PreviewUseKeyPress = () => { + // Call our hook for each key that we'd like to monitor + const [isAbcKeyPressed, setIsAbcKeyPressed] = useState(false); + + useKeyPress({ + keyPressItems: [ + { + keys: ["KeyA", "Meta"], + event: () => setIsAbcKeyPressed((prev) => !prev), + }, + { + keys: ["KeyA", "Control"], + event: () => setIsAbcKeyPressed((prev) => !prev), + }, + ], + }); + + return ( +
+

+ + "cmd + a" + {" "} + or{" "} + "ctrl + a" + {isAbcKeyPressed ? " to hide" : " to show"} +

+ {isAbcKeyPressed && ( +
+ )} +
+ ); +}; diff --git a/src/ui/cuicui/hooks/use-key-press/use-key-press.ts b/src/ui/cuicui/hooks/use-key-press/use-key-press.ts new file mode 100644 index 00000000..0ff6f976 --- /dev/null +++ b/src/ui/cuicui/hooks/use-key-press/use-key-press.ts @@ -0,0 +1,178 @@ +import { useEffect } from "react"; + +const Keys = { + Backspace: "Backspace", + Tab: "Tab", + Enter: "Enter", + Shift: "Shift", + Control: "Control", + Alt: "Alt", + Pause: "Pause", + CapsLock: "CapsLock", + Escape: "Escape", + Space: " ", + PageUp: "PageUp", + PageDown: "PageDown", + End: "End", + Home: "Home", + LeftArrow: "ArrowLeft", + UpArrow: "ArrowUp", + RightArrow: "ArrowRight", + DownArrow: "ArrowDown", + Insert: "Insert", + Delete: "Delete", + Meta: "Meta", + Key0: "0", + Key1: "1", + Key2: "2", + Key3: "3", + Key4: "4", + Key5: "5", + Key6: "6", + Key7: "7", + Key8: "8", + Key9: "9", + KeyA: "a", + KeyB: "b", + KeyC: "c", + KeyD: "d", + KeyE: "e", + KeyF: "f", + KeyG: "g", + KeyH: "h", + KeyI: "i", + KeyJ: "j", + KeyK: "k", + KeyL: "l", + KeyM: "m", + KeyN: "n", + KeyO: "o", + KeyP: "p", + KeyQ: "q", + KeyR: "r", + KeyS: "s", + KeyT: "t", + KeyU: "u", + KeyV: "v", + KeyW: "w", + KeyX: "x", + KeyY: "y", + KeyZ: "z", + LeftMeta: "Meta", + RightMeta: "Meta", + Select: "Select", + Numpad0: "0", + Numpad1: "1", + Numpad2: "2", + Numpad3: "3", + Numpad4: "4", + Numpad5: "5", + Numpad6: "6", + Numpad7: "7", + Numpad8: "8", + Numpad9: "9", + Multiply: "*", + Add: "+", + Subtract: "-", + Decimal: ".", + Divide: "/", + F1: "F1", + F2: "F2", + F3: "F3", + F4: "F4", + F5: "F5", + F6: "F6", + F7: "F7", + F8: "F8", + F9: "F9", + F10: "F10", + F11: "F11", + F12: "F12", + NumLock: "NumLock", + ScrollLock: "ScrollLock", + Semicolon: ";", + Equals: "=", + Comma: ",", + Dash: "-", + Period: ".", + ForwardSlash: "/", + GraveAccent: "`", + OpenBracket: "[", + BackSlash: "\\", + CloseBracket: "]", + Quote: "'", +}; + +export interface KeyPressItem { + keys: Array; + event: (event: KeyboardEvent) => void; + preventDefault?: boolean; +} + +function checkCombination( + event: KeyboardEvent, + keys: Array, +): boolean { + return keys.every((key) => { + if (key === "Meta") { + return event.metaKey; + } + if (key === "Control") { + return event.ctrlKey; + } + if (key === "Shift") { + return event.shiftKey; + } + if (key === "Alt") { + return event.altKey; + } + return event.key === Keys[key]; + }); +} + +export function useKeyPress({ + keyPressItems, + tagsToIgnore = ["INPUT", "TEXTAREA", "SELECT"], + triggerOnContentEditable = false, +}: { + keyPressItems: KeyPressItem[]; + tagsToIgnore?: string[]; + triggerOnContentEditable?: boolean; +}) { + useEffect(() => { + const keydownListener = (event: KeyboardEvent) => { + for (const keyPressItem of keyPressItems) { + const { + keys, + event: triggerEvent, + preventDefault = true, + } = keyPressItem; + if ( + checkCombination(event, keys) && + shouldFireEvent(event, tagsToIgnore, triggerOnContentEditable) + ) { + if (preventDefault) { + event.preventDefault(); + } + + triggerEvent(event); + } + } + }; + + document.addEventListener("keydown", keydownListener); + return () => document.removeEventListener("keydown", keydownListener); + }, [keyPressItems, tagsToIgnore, triggerOnContentEditable]); +} + +function shouldFireEvent( + event: KeyboardEvent, + tagsToIgnore: string[], + triggerOnContentEditable: boolean, +): boolean { + const target = event.target as HTMLElement; + return !( + (target.isContentEditable && !triggerOnContentEditable) || + tagsToIgnore.includes(target.tagName) + ); +} diff --git a/src/ui/cuicui/hooks/use-konami-code/category.use-konami-code.tsx b/src/ui/cuicui/hooks/use-konami-code/category.use-konami-code.tsx new file mode 100644 index 00000000..530804c6 --- /dev/null +++ b/src/ui/cuicui/hooks/use-konami-code/category.use-konami-code.tsx @@ -0,0 +1,31 @@ +import { GamepadIcon } from "lucide-react"; +import type { SingleComponentCategoryType } from "#/src/lib/types/component"; +import { PreviewUseKonamiCode } from "#/src/ui/cuicui/hooks/use-konami-code/preview-use-konami-code"; + +export const useKonamiCodeCategory: SingleComponentCategoryType = { + slug: "use-konami-code", + name: "Use Konami Code", + description: + "A hook that detects the Konami Code sequence and triggers actions accordingly", + releaseDateCategory: new Date("2024-09-28"), + icon: GamepadIcon, + previewCategory: { + component: , + previewScale: 0.8, + }, + component: { + lastUpdatedDateComponent: new Date("2024-09-28"), + sizePreview: "sm", + + isIframed: false, + + variantList: [ + { + name: "default variant", + component: , + slugComponentFile: "use-konami-code", + slugPreviewFile: "preview-use-konami-code", + }, + ], + }, +}; diff --git a/src/ui/cuicui/hooks/use-konami-code/preview-use-konami-code.tsx b/src/ui/cuicui/hooks/use-konami-code/preview-use-konami-code.tsx new file mode 100644 index 00000000..86610680 --- /dev/null +++ b/src/ui/cuicui/hooks/use-konami-code/preview-use-konami-code.tsx @@ -0,0 +1,44 @@ +"use client"; +import useKonamiCode from "#/src/ui/cuicui/hooks/use-konami-code/use-konami-code"; +import { useState } from "react"; + +export const PreviewUseKonamiCode = () => { + const [isKonamiCodeActivated, setIsKonamiCodeActivated] = useState(false); + + // const konamiSequenceByCode = [ + // "ArrowUp", + // "ArrowDown", + // "ArrowLeft", + // "ArrowRight", + // "KeyB", + // "KeyA", + // ]; + + const konamiSequenceByKey = [ + "ArrowUp", + "ArrowDown", + "ArrowLeft", + "ArrowRight", + "b", + "a", + ]; + + const handleKonamiCode = () => { + setIsKonamiCodeActivated(true); + }; + + useKonamiCode(konamiSequenceByKey, handleKonamiCode, { + matchMode: "key", + }); + + return ( +
+

Welcome to the Konami Code Demo

+

Try entering the Konami Code using your keyboard!

+

+ The Konami Code is: ↑ ↓ ← → b a +

+ {isKonamiCodeActivated &&

Konami Code activated!

} +
+ ); +}; diff --git a/src/ui/cuicui/hooks/use-konami-code/use-konami-code.ts b/src/ui/cuicui/hooks/use-konami-code/use-konami-code.ts new file mode 100644 index 00000000..57a8dbd9 --- /dev/null +++ b/src/ui/cuicui/hooks/use-konami-code/use-konami-code.ts @@ -0,0 +1,108 @@ +"use client"; + +import { useEffect, useState, useCallback, useMemo } from "react"; + +type KonamiCallback = () => void; + +type MatchMode = "code" | "key" | "codeOrKey"; + +interface UseKonamiCodeOptions { + /** + * Determines how key presses are matched against the sequence. + * - "code": Matches using `KeyboardEvent.code`. + * - "key": Matches using `KeyboardEvent.key`. + * - "codeOrKey": Matches if either `code` or `key` matches. + */ + matchMode?: MatchMode; + /** + * Specifies whether the sequence should be case-insensitive. + * Applicable only when `matchMode` is "key" or "codeOrKey". + */ + caseInsensitive?: boolean; +} + +/** + * Custom hook to detect a specific key sequence. + * + * @param sequence - An array of strings representing the desired key sequence. + * @param callback - A function to be executed when the key sequence is successfully entered. + * @param options - Optional configuration for matching mode and case sensitivity. + * @returns The current sequence of keys pressed. + */ +const useKonamiCode = ( + sequence: string[], + callback: KonamiCallback, + options?: UseKonamiCodeOptions, +): string[] => { + const { matchMode = "code", caseInsensitive = false } = options || {}; + const [currentSequence, setCurrentSequence] = useState([]); + + // Memoize the target sequence based on matchMode and case sensitivity + const targetSequence = useMemo(() => { + if (matchMode === "key" || matchMode === "codeOrKey") { + return caseInsensitive + ? sequence.map((key) => key.toLowerCase()) + : sequence; + } + return sequence; + }, [sequence, matchMode, caseInsensitive]); + + // Handler for keydown events + const handleKeyDown = useCallback( + (event: KeyboardEvent) => { + let identifier: string[] = []; + + if (matchMode === "code") { + identifier = [event.code]; + } else if (matchMode === "key") { + identifier = [caseInsensitive ? event.key.toLowerCase() : event.key]; + } else if (matchMode === "codeOrKey") { + identifier = [ + event.code, + caseInsensitive ? event.key.toLowerCase() : event.key, + ]; + } + + setCurrentSequence((prevSequence) => { + let updatedSequence = [...prevSequence]; + + for (const id of identifier) { + updatedSequence = [...updatedSequence, id]; + + // Keep the sequence length within the target sequence length + if (updatedSequence.length > targetSequence.length) { + updatedSequence.shift(); + } + + // Check if the updated sequence matches the target sequence + const isMatch = + updatedSequence.length === targetSequence.length && + targetSequence.every( + (target, index) => target === updatedSequence[index], + ); + + if (isMatch) { + callback(); + updatedSequence = []; // Reset the sequence after successful entry + } + } + + return updatedSequence; + }); + }, + [matchMode, caseInsensitive, targetSequence, callback], + ); + + // Effect to add and clean up the event listener + useEffect(() => { + window.addEventListener("keydown", handleKeyDown); + + return () => { + window.removeEventListener("keydown", handleKeyDown); + }; + }, [handleKeyDown]); + + return currentSequence; +}; + +export default useKonamiCode; diff --git a/src/ui/cuicui/hooks/use-local-storage/category.use-local-storage.tsx b/src/ui/cuicui/hooks/use-local-storage/category.use-local-storage.tsx new file mode 100644 index 00000000..7a37925b --- /dev/null +++ b/src/ui/cuicui/hooks/use-local-storage/category.use-local-storage.tsx @@ -0,0 +1,29 @@ +import { DatabaseIcon } from "lucide-react"; +import type { SingleComponentCategoryType } from "#/src/lib/types/component"; +import { PreviewUseLocalStorage } from "#/src/ui/cuicui/hooks/use-local-storage/preview-use-local-storage"; + +export const useLocalStorageCategory: SingleComponentCategoryType = { + slug: "use-local-storage", + name: "Use Local Storage", + description: + "A hook that allows you to manage and persist state using localStorage", + releaseDateCategory: new Date("2024-09-28"), + icon: DatabaseIcon, + previewCategory: { + component: , + previewScale: 0.8, + }, + component: { + lastUpdatedDateComponent: new Date("2024-09-28"), + sizePreview: "sm", + isIframed: false, + variantList: [ + { + name: "Default Variant", + component: , + slugComponentFile: "use-local-storage", + slugPreviewFile: "preview-use-local-storage", + }, + ], + }, +}; diff --git a/src/ui/cuicui/hooks/use-local-storage/preview-use-local-storage.tsx b/src/ui/cuicui/hooks/use-local-storage/preview-use-local-storage.tsx new file mode 100644 index 00000000..00f15888 --- /dev/null +++ b/src/ui/cuicui/hooks/use-local-storage/preview-use-local-storage.tsx @@ -0,0 +1,36 @@ +"use client"; +import { useLocalStorage } from "#/src/ui/cuicui/hooks/use-local-storage/use-local-storage"; + +export function PreviewUseLocalStorage() { + const [value, setValue, removeValue] = useLocalStorage("test-key", 0); + + return ( +
+

Count: {value}

+ + + +
+ ); +} diff --git a/src/ui/cuicui/hooks/use-local-storage/use-local-storage.ts b/src/ui/cuicui/hooks/use-local-storage/use-local-storage.ts new file mode 100644 index 00000000..0215e7dc --- /dev/null +++ b/src/ui/cuicui/hooks/use-local-storage/use-local-storage.ts @@ -0,0 +1,167 @@ +import { useEventCallback } from "#/src/ui/cuicui/hooks/use-event-callback/use-event-callback"; +import { useEventListener } from "#/src/ui/cuicui/hooks/use-event-listener/use-event-listener"; +import { useCallback, useEffect, useState } from "react"; + +import type { Dispatch, SetStateAction } from "react"; + +declare global { + // eslint-disable-next-line @typescript-eslint/consistent-type-definitions + interface WindowEventMap { + "local-storage": CustomEvent; + } +} + +type UseLocalStorageOptions = { + serializer?: (value: T) => string; + deserializer?: (value: string) => T; + initializeWithValue?: boolean; +}; + +const IS_SERVER = typeof window === "undefined"; + +export function useLocalStorage( + key: string, + initialValue: T | (() => T), + options: UseLocalStorageOptions = {}, +): [T, Dispatch>, () => void] { + const { initializeWithValue = true } = options; + + const serializer = useCallback<(value: T) => string>( + (value) => { + if (options.serializer) { + return options.serializer(value); + } + + return JSON.stringify(value); + }, + [options], + ); + + const deserializer = useCallback<(value: string) => T>( + (value) => { + if (options.deserializer) { + return options.deserializer(value); + } + // Support 'undefined' as a value + if (value === "undefined") { + return undefined as unknown as T; + } + + const defaultValue = + initialValue instanceof Function ? initialValue() : initialValue; + + let parsed: unknown; + try { + parsed = JSON.parse(value); + } catch (error) { + console.error("Error parsing JSON:", error); + return defaultValue; // Return initialValue if parsing fails + } + + return parsed as T; + }, + [options, initialValue], + ); + + // Get from local storage then + // parse stored json or return initialValue + const readValue = useCallback((): T => { + const initialValueToUse = + initialValue instanceof Function ? initialValue() : initialValue; + + // Prevent build error "window is undefined" but keep working + if (IS_SERVER) { + return initialValueToUse; + } + + try { + const raw = window.localStorage.getItem(key); + return raw ? deserializer(raw) : initialValueToUse; + } catch (error) { + console.warn(`Error reading localStorage key “${key}”:`, error); + return initialValueToUse; + } + }, [initialValue, key, deserializer]); + + const [storedValue, setStoredValue] = useState(() => { + if (initializeWithValue) { + return readValue(); + } + + return initialValue instanceof Function ? initialValue() : initialValue; + }); + + // Return a wrapped version of useState's setter function that ... + // ... persists the new value to localStorage. + const setValue: Dispatch> = useEventCallback((value) => { + // Prevent build error "window is undefined" but keeps working + if (IS_SERVER) { + console.warn( + `Tried setting localStorage key “${key}” even though environment is not a client`, + ); + } + + try { + // Allow value to be a function so we have the same API as useState + const newValue = value instanceof Function ? value(readValue()) : value; + + // Save to local storage + window.localStorage.setItem(key, serializer(newValue)); + + // Save state + setStoredValue(newValue); + + // We dispatch a custom event so every similar useLocalStorage hook is notified + window.dispatchEvent(new StorageEvent("local-storage", { key })); + } catch (error) { + console.warn(`Error setting localStorage key “${key}”:`, error); + } + }); + + const removeValue = useEventCallback(() => { + // Prevent build error "window is undefined" but keeps working + if (IS_SERVER) { + console.warn( + `Tried removing localStorage key “${key}” even though environment is not a client`, + ); + } + + const defaultValue = + initialValue instanceof Function ? initialValue() : initialValue; + + // Remove the key from local storage + window.localStorage.removeItem(key); + + // Save state with default value + setStoredValue(defaultValue); + + // We dispatch a custom event so every similar useLocalStorage hook is notified + window.dispatchEvent(new StorageEvent("local-storage", { key })); + }); + + //!! To improve to not ignore the exhaustive-deps + // biome-ignore lint/correctness/useExhaustiveDependencies: + useEffect(() => { + setStoredValue(readValue()); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [key]); + + const handleStorageChange = useCallback( + (event: StorageEvent | CustomEvent) => { + if ((event as StorageEvent).key && (event as StorageEvent).key !== key) { + return; + } + setStoredValue(readValue()); + }, + [key, readValue], + ); + + // this only works for other documents, not the current one + useEventListener("storage", handleStorageChange); + + // this is a custom event, triggered in writeValueToLocalStorage + // See: useLocalStorage() + useEventListener("local-storage", handleStorageChange); + + return [storedValue, setValue, removeValue]; +} diff --git a/src/ui/cuicui/hooks/use-location/category.use-location.tsx b/src/ui/cuicui/hooks/use-location/category.use-location.tsx new file mode 100644 index 00000000..98a53bad --- /dev/null +++ b/src/ui/cuicui/hooks/use-location/category.use-location.tsx @@ -0,0 +1,29 @@ +import { MapIcon } from "lucide-react"; +import type { SingleComponentCategoryType } from "#/src/lib/types/component"; +import { PreviewUseLocation } from "#/src/ui/cuicui/hooks/use-location/preview-use-location"; + +export const useLocationCategory: SingleComponentCategoryType = { + slug: "use-location", + name: "Use Location", + description: + "A hook that allows you to access and manage the current location in your application", + releaseDateCategory: new Date("2024-09-28"), + icon: MapIcon, + previewCategory: { + component: , + previewScale: 0.8, + }, + component: { + lastUpdatedDateComponent: new Date("2024-09-28"), + sizePreview: "xl", + isIframed: false, + variantList: [ + { + name: "variant 1", + component: , + slugComponentFile: "use-location", + slugPreviewFile: "preview-use-location", + }, + ], + }, +}; diff --git a/src/ui/cuicui/hooks/use-location/preview-use-location.tsx b/src/ui/cuicui/hooks/use-location/preview-use-location.tsx new file mode 100644 index 00000000..c0f15149 --- /dev/null +++ b/src/ui/cuicui/hooks/use-location/preview-use-location.tsx @@ -0,0 +1,8 @@ +"use client"; +import useLocation from "#/src/ui/cuicui/hooks/use-location/use-location"; + +export function PreviewUseLocation() { + const location = useLocation(); + + return
{JSON.stringify(location, null, 2)}
; +} diff --git a/src/ui/cuicui/hooks/use-location/use-location.ts b/src/ui/cuicui/hooks/use-location/use-location.ts new file mode 100644 index 00000000..03d554ed --- /dev/null +++ b/src/ui/cuicui/hooks/use-location/use-location.ts @@ -0,0 +1,147 @@ +"use client"; +import { useState, useEffect } from "react"; + +const isClient = typeof window === "object"; + +type HistoryMethod = "pushState" | "replaceState"; + +declare global { + interface WindowEventMap { + pushstate: CustomEvent<{ state: unknown }>; + replacestate: CustomEvent<{ state: unknown }>; + } +} + +const on = ( + obj: Window, + type: keyof WindowEventMap, + listener: (event: Event) => void, +) => obj.addEventListener(type, listener); + +const off = ( + obj: Window, + type: keyof WindowEventMap, + listener: (event: Event) => void, +) => obj.removeEventListener(type, listener); + +const patchHistoryMethod = (method: HistoryMethod) => { + const original = history[method]; + + history[method] = function ( + this: History, + data: unknown, + title: string, + url?: string | null, + ) { + const result = original.apply(this, [data, title, url]); + const event = new CustomEvent<{ state: unknown }>(method.toLowerCase(), { + detail: { state: data }, + }); + window.dispatchEvent(event); + return result; + }; +}; + +if (isClient) { + patchHistoryMethod("pushState"); + patchHistoryMethod("replaceState"); +} + +interface LocationState { + trigger: string; + state: unknown; + length: number; + hash: string; + host: string; + hostname: string; + href: string; + origin: string; + pathname: string; + port: string; + protocol: string; + search: string; +} + +const defaultLocationState: LocationState = { + trigger: "load", + state: null, + length: 1, + hash: "", + host: "", + hostname: "", + href: "", + origin: "", + pathname: "", + port: "", + protocol: "", + search: "", +}; + +const useLocation = (): LocationState => { + const buildState = (trigger: string): LocationState => { + if (!isClient) { + return defaultLocationState; + } + + const { state, length } = history; + const { + hash, + host, + hostname, + href, + origin, + pathname, + port, + protocol, + search, + } = window.location; + + return { + trigger, + state, + length, + hash, + host, + hostname, + href, + origin, + pathname, + port, + protocol, + search, + }; + }; + + const [locationState, setLocationState] = useState( + buildState("load"), + ); + + // !! Should try to use the `useEffect` hook with an exhaustive dependency array + // biome-ignore lint/correctness/useExhaustiveDependencies: + useEffect(() => { + if (!isClient) { + return; + } + const onChange = (trigger: string) => { + setLocationState(buildState(trigger)); + }; + + const handlePopState = () => onChange("popstate"); + const handlePushState = () => onChange("pushstate"); + const handleReplaceState = () => onChange("replacestate"); + + on(window, "popstate", handlePopState); + on(window, "pushstate", handlePushState); + on(window, "replacestate", handleReplaceState); + + return () => { + off(window, "popstate", handlePopState); + off(window, "pushstate", handlePushState); + off(window, "replacestate", handleReplaceState); + }; + }, []); + + return locationState; +}; + +export default useLocation; diff --git a/src/ui/cuicui/hooks/use-mouse/use-mouse.category.tsx b/src/ui/cuicui/hooks/use-mouse/category.use-mouse.tsx similarity index 59% rename from src/ui/cuicui/hooks/use-mouse/use-mouse.category.tsx rename to src/ui/cuicui/hooks/use-mouse/category.use-mouse.tsx index 5c3e5d75..a611c797 100644 --- a/src/ui/cuicui/hooks/use-mouse/use-mouse.category.tsx +++ b/src/ui/cuicui/hooks/use-mouse/category.use-mouse.tsx @@ -1,12 +1,12 @@ import { MousePointer2Icon } from "lucide-react"; -import type { CategoryType } from "#/src/lib/types/component"; -import { useMouseComponent } from "#/src/ui/cuicui/hooks/use-mouse/hook/use-mouse.component"; +import type { SingleComponentCategoryType } from "#/src/lib/types/component"; +import { useMouseComponent } from "#/src/ui/cuicui/hooks/use-mouse/component.use-mouse"; -export const useMouseCategory: CategoryType = { +export const useMouseCategory: SingleComponentCategoryType = { slug: "use-mouse", name: "Use Mouse", icon: MousePointer2Icon, releaseDateCategory: new Date("2024-09-24"), description: "A simple hook to get the mouse position.", - componentList: [useMouseComponent], + component: useMouseComponent, }; diff --git a/src/ui/cuicui/hooks/use-mouse/hook/use-mouse.component.tsx b/src/ui/cuicui/hooks/use-mouse/component.use-mouse.tsx similarity index 83% rename from src/ui/cuicui/hooks/use-mouse/hook/use-mouse.component.tsx rename to src/ui/cuicui/hooks/use-mouse/component.use-mouse.tsx index cf16f667..b3c75199 100644 --- a/src/ui/cuicui/hooks/use-mouse/hook/use-mouse.component.tsx +++ b/src/ui/cuicui/hooks/use-mouse/component.use-mouse.tsx @@ -1,5 +1,5 @@ import type { ComponentType } from "#/src/lib/types/component"; -import PreviewUseMouse from "#/src/ui/cuicui/hooks/use-mouse/hook/preview-use-mouse"; +import PreviewUseMouse from "#/src/ui/cuicui/hooks/use-mouse/preview-use-mouse"; export const useMouseComponent: ComponentType = { sizePreview: "xs", diff --git a/src/ui/cuicui/hooks/use-mouse/hook/preview-use-mouse.tsx b/src/ui/cuicui/hooks/use-mouse/preview-use-mouse.tsx similarity index 89% rename from src/ui/cuicui/hooks/use-mouse/hook/preview-use-mouse.tsx rename to src/ui/cuicui/hooks/use-mouse/preview-use-mouse.tsx index dbd7b3f2..be9cfadf 100644 --- a/src/ui/cuicui/hooks/use-mouse/hook/preview-use-mouse.tsx +++ b/src/ui/cuicui/hooks/use-mouse/preview-use-mouse.tsx @@ -1,6 +1,6 @@ "use client"; -import { useMouse } from "#/src/ui/cuicui/hooks/use-mouse/hook/use-mouse"; +import { useMouse } from "#/src/ui/cuicui/hooks/use-mouse/use-mouse"; export default function PreviewUseMouse() { const [mouse, parentRef] = useMouse(); diff --git a/src/ui/cuicui/hooks/use-mouse/hook/use-mouse.tsx b/src/ui/cuicui/hooks/use-mouse/use-mouse.ts similarity index 100% rename from src/ui/cuicui/hooks/use-mouse/hook/use-mouse.tsx rename to src/ui/cuicui/hooks/use-mouse/use-mouse.ts diff --git a/src/ui/cuicui/hooks/use-network-status/category.use-network.tsx b/src/ui/cuicui/hooks/use-network-status/category.use-network.tsx new file mode 100644 index 00000000..58450b91 --- /dev/null +++ b/src/ui/cuicui/hooks/use-network-status/category.use-network.tsx @@ -0,0 +1,29 @@ +import { NetworkIcon } from "lucide-react"; +import type { SingleComponentCategoryType } from "#/src/lib/types/component"; +import PreviewUseNetworkStatus from "#/src/ui/cuicui/hooks/use-network-status/preview-use-network-status"; + +export const useNetworkCategory: SingleComponentCategoryType = { + slug: "use-network-status", + name: "Use Network Status", + description: "A hook to get the network status", + releaseDateCategory: new Date("2024-09-16"), + icon: NetworkIcon, + previewCategory: { + component: , + previewScale: 1, + }, + component: { + rerenderButton: true, + lastUpdatedDateComponent: new Date("2024-09-16"), + sizePreview: "lg", + isIframed: false, + variantList: [ + { + name: "variant 1", + component: , + slugComponentFile: "use-network-status", + slugPreviewFile: "preview-use-network-status", + }, + ], + }, +}; diff --git a/src/ui/cuicui/hooks/use-network-status/hook/preview-use-network-status.tsx b/src/ui/cuicui/hooks/use-network-status/preview-use-network-status.tsx similarity index 93% rename from src/ui/cuicui/hooks/use-network-status/hook/preview-use-network-status.tsx rename to src/ui/cuicui/hooks/use-network-status/preview-use-network-status.tsx index 7b14c50f..32b86683 100644 --- a/src/ui/cuicui/hooks/use-network-status/hook/preview-use-network-status.tsx +++ b/src/ui/cuicui/hooks/use-network-status/preview-use-network-status.tsx @@ -3,7 +3,7 @@ import { Fragment, useEffect, useState } from "react"; import { type NetworkState, useNetworkStatus, -} from "#/src/ui/cuicui/hooks/use-network-status/hook/use-network-status"; +} from "#/src/ui/cuicui/hooks/use-network-status/use-network-status"; export default function PreviewUseNetworkStatus() { const [isMounted, setIsMounted] = useState(false); diff --git a/src/ui/cuicui/hooks/use-network-status/hook/use-network-status.tsx b/src/ui/cuicui/hooks/use-network-status/use-network-status.ts similarity index 100% rename from src/ui/cuicui/hooks/use-network-status/hook/use-network-status.tsx rename to src/ui/cuicui/hooks/use-network-status/use-network-status.ts diff --git a/src/ui/cuicui/hooks/use-network-status/use-network.category.tsx b/src/ui/cuicui/hooks/use-network-status/use-network.category.tsx deleted file mode 100644 index d73289eb..00000000 --- a/src/ui/cuicui/hooks/use-network-status/use-network.category.tsx +++ /dev/null @@ -1,34 +0,0 @@ -import { NetworkIcon } from "lucide-react"; -import type { CategoryType } from "#/src/lib/types/component"; -import PreviewUseNetworkStatus from "#/src/ui/cuicui/hooks/use-network-status/hook/preview-use-network-status"; - -export const useNetworkCategory: CategoryType = { - slug: "use-network-status", - name: "Use Network Status", - description: "A hook to get the network status", - releaseDateCategory: new Date("2024-09-16"), - icon: NetworkIcon, - previewCategory: { - component: , - previewScale: 1, - }, - componentList: [ - { - rerenderButton: true, - lastUpdatedDateComponent: new Date("2024-09-16"), - sizePreview: "lg", - slug: "hook", - isIframed: false, - title: "Use Network Status", - description: "A hook to get the network status", - variantList: [ - { - name: "variant 1", - component: , - slugComponentFile: "use-network-status", - slugPreviewFile: "preview-use-network-status", - }, - ], - }, - ], -}; diff --git a/src/ui/cuicui/hooks/use-online-status/category.use-online-status.tsx b/src/ui/cuicui/hooks/use-online-status/category.use-online-status.tsx new file mode 100644 index 00000000..721eae60 --- /dev/null +++ b/src/ui/cuicui/hooks/use-online-status/category.use-online-status.tsx @@ -0,0 +1,29 @@ +import { WifiIcon } from "lucide-react"; +import type { SingleComponentCategoryType } from "#/src/lib/types/component"; +import { PreviewUseOnlineStatus } from "#/src/ui/cuicui/hooks/use-online-status/preview-use-online-status"; + +export const useOnlineStatusCategory: SingleComponentCategoryType = { + slug: "use-online-status", + name: "Use Online Status", + description: + "A hook that allows you to monitor the online/offline status of the user's device", + releaseDateCategory: new Date("2024-09-28"), + icon: WifiIcon, + previewCategory: { + component: , + previewScale: 0.8, + }, + component: { + lastUpdatedDateComponent: new Date("2024-09-28"), + sizePreview: "sm", + isIframed: false, + variantList: [ + { + name: "default variant", + component: , + slugComponentFile: "use-online-status", + slugPreviewFile: "preview-use-online-status", + }, + ], + }, +}; diff --git a/src/ui/cuicui/hooks/use-online-status/preview-use-online-status.tsx b/src/ui/cuicui/hooks/use-online-status/preview-use-online-status.tsx new file mode 100644 index 00000000..156e3635 --- /dev/null +++ b/src/ui/cuicui/hooks/use-online-status/preview-use-online-status.tsx @@ -0,0 +1,11 @@ +"use client"; +import { useOnlineStatus } from "#/src/ui/cuicui/hooks/use-online-status/use-online-status"; + +export function PreviewUseOnlineStatus() { + const onlineStatus = useOnlineStatus(); + return ( +
+

You are {onlineStatus ? "Online" : "Offline"}

+
+ ); +} diff --git a/src/ui/cuicui/hooks/use-online-status/use-online-status.ts b/src/ui/cuicui/hooks/use-online-status/use-online-status.ts new file mode 100644 index 00000000..7aa0da86 --- /dev/null +++ b/src/ui/cuicui/hooks/use-online-status/use-online-status.ts @@ -0,0 +1,30 @@ +"use client"; +import { useEffect, useState } from "react"; + +function getOnlineStatus() { + return typeof navigator !== "undefined" && + typeof navigator.onLine === "boolean" + ? navigator.onLine + : true; +} + +export function useOnlineStatus() { + const [onlineStatus, setOnlineStatus] = useState(getOnlineStatus()); + + useEffect(() => { + function goOnline() { + setOnlineStatus(true); + } + function goOffline() { + setOnlineStatus(false); + } + window.addEventListener("online", goOnline); + window.addEventListener("offline", goOffline); + return () => { + window.removeEventListener("online", goOnline); + window.removeEventListener("offline", goOffline); + }; + }, []); + + return onlineStatus; +} diff --git a/src/ui/cuicui/hooks/use-session-storage/category.use-session-storage.tsx b/src/ui/cuicui/hooks/use-session-storage/category.use-session-storage.tsx new file mode 100644 index 00000000..c9f3fd8a --- /dev/null +++ b/src/ui/cuicui/hooks/use-session-storage/category.use-session-storage.tsx @@ -0,0 +1,31 @@ +// Coming soon +import { ViewIcon } from "lucide-react"; +import type { SingleComponentCategoryType } from "#/src/lib/types/component"; +// import { PreviewUseSessionStorage } from "#/src/ui/cuicui/hooks/use-session-storage/preview-use-session-storage"; + +export const useSessionStorageCategory: SingleComponentCategoryType = { + slug: "use-session-storage", + name: "Use Session Storage", + description: "A hook that allows you to manage session storage values", + releaseDateCategory: new Date("2024-08-28"), + icon: ViewIcon, + // previewCategory: { + // component: , + // previewScale: 0.8, + // }, + comingSoonCategory: true, + component: null, + // component: { + // lastUpdatedDateComponent: new Date("2024-08-28"), + // sizePreview: "sm", + // isIframed: false, + // variantList: [ + // { + // name: "variant 1", + // component: , + // slugComponentFile: "use-session-storage", + // slugPreviewFile: "preview-use-session-storage", + // }, + // ], + // }, +}; diff --git a/src/ui/cuicui/hooks/use-session-storage/preview-use-session-storage.tsx b/src/ui/cuicui/hooks/use-session-storage/preview-use-session-storage.tsx new file mode 100644 index 00000000..c2b9d7f2 --- /dev/null +++ b/src/ui/cuicui/hooks/use-session-storage/preview-use-session-storage.tsx @@ -0,0 +1 @@ +// Coming soon diff --git a/src/ui/cuicui/hooks/use-session-storage/use-session-storage.ts b/src/ui/cuicui/hooks/use-session-storage/use-session-storage.ts new file mode 100644 index 00000000..9bb4e5a4 --- /dev/null +++ b/src/ui/cuicui/hooks/use-session-storage/use-session-storage.ts @@ -0,0 +1 @@ +// Coming Soon diff --git a/src/ui/cuicui/hooks/use-speech-to-text/category.use-speech-to-text.tsx b/src/ui/cuicui/hooks/use-speech-to-text/category.use-speech-to-text.tsx new file mode 100644 index 00000000..a11d551d --- /dev/null +++ b/src/ui/cuicui/hooks/use-speech-to-text/category.use-speech-to-text.tsx @@ -0,0 +1,32 @@ +import type { SingleComponentCategoryType } from "#/src/lib/types/component"; +import { AudioLinesIcon } from "lucide-react"; + +export const useSpeechToTextCategory: SingleComponentCategoryType = { + slug: "use-speech-to-text", + name: "Use Speech to Text", + description: "A hook that allows you to convert speech to text", + releaseDateCategory: new Date("2024-09-28"), + icon: AudioLinesIcon, + // previewCategory: { + // component: , + // previewImage: PreviewImageUseTextToSpeech, + // previewScale: 0.8, + // }, + comingSoonCategory: true, + component: null, + // component: { + // lastUpdatedDateComponent: new Date("2024-08-28"), + // sizePreview: "lg", + + // isIframed: true, + + // variantList: [ + // { + // name: "variant 1", + // component: , + // slugComponentFile: "use-text-to-speech", + // slugPreviewFile: "preview-text-to-speech", + // }, + // ], + // }, +}; diff --git a/src/ui/cuicui/hooks/use-speech-to-text/preview-use-speech-to-text.tsx b/src/ui/cuicui/hooks/use-speech-to-text/preview-use-speech-to-text.tsx new file mode 100644 index 00000000..c2b9d7f2 --- /dev/null +++ b/src/ui/cuicui/hooks/use-speech-to-text/preview-use-speech-to-text.tsx @@ -0,0 +1 @@ +// Coming soon diff --git a/src/ui/cuicui/hooks/use-speech-to-text/use-speech-to-text.ts b/src/ui/cuicui/hooks/use-speech-to-text/use-speech-to-text.ts new file mode 100644 index 00000000..b3cac976 --- /dev/null +++ b/src/ui/cuicui/hooks/use-speech-to-text/use-speech-to-text.ts @@ -0,0 +1,139 @@ +// import { useCallback, useEffect, useState } from "react"; + +// // Define custom types for SpeechRecognition and SpeechRecognitionEvent +// interface ISpeechRecognitionEvent extends Event { +// results: SpeechRecognitionResultList; +// resultIndex: number; +// } + +// interface ISpeechRecognition extends EventTarget { +// lang: string; +// continuous: boolean; +// interimResults: boolean; +// maxAlternatives: number; +// start: () => void; +// stop: () => void; +// onresult: (event: ISpeechRecognitionEvent) => void; +// onerror: (event: Event) => void; +// onend: () => void; +// } + +// declare global { +// interface Window { +// speechRecognition: new () => ISpeechRecognition; +// webkitSpeechRecognition: new () => ISpeechRecognition; +// } +// } + +// interface UseSpeechToTextProps { +// lang?: string; +// continuous?: boolean; +// interimResults?: boolean; +// maxAlternatives?: number; +// onResult?: (result: string) => void; +// onError?: (error: string) => void; +// } + +// export const useSpeechToText = ({ +// lang = "en-US", +// continuous = true, +// interimResults = true, +// maxAlternatives = 1, +// onResult, +// onError, +// }: UseSpeechToTextProps = {}) => { +// const [isListening, setIsListening] = useState(false); +// const [transcript, setTranscript] = useState(""); +// const [lastProcessedIndex, setLastProcessedIndex] = useState(0); + +// const recognition: ISpeechRecognition | null = +// typeof window !== "undefined" && +// (window.speechRecognition || window.webkitSpeechRecognition) +// ? new (window.speechRecognition || window.webkitSpeechRecognition)() +// : null; + +// const handleResult = useCallback( +// (event: ISpeechRecognitionEvent) => { +// let interimTranscript = ""; +// let finalTranscript = ""; + +// // Iterate through all the current results +// for (let i = lastProcessedIndex; i < event.results.length; i++) { +// const result = event.results[i]; +// // If the result is final, append to the final transcript +// if (result.isFinal) { +// finalTranscript += `${result[0].transcript} `; +// setLastProcessedIndex(i + 1); +// } else { +// // Otherwise, append to the interim transcript +// interimTranscript += `${result[0].transcript} `; +// } +// } + +// // Update the transcript state with a combination of the final and interim results +// setTranscript(transcript + finalTranscript + interimTranscript); + +// // Invoke callback with the latest transcript +// onResult?.(transcript + finalTranscript + interimTranscript); +// }, +// [onResult, transcript, lastProcessedIndex], +// ); + +// // start and stop functions using useCallback +// const start = useCallback(() => { +// if (!recognition || isListening) { +// return; +// } +// setTranscript(""); +// setLastProcessedIndex(0); +// setIsListening(true); +// recognition.start(); +// }, [recognition, isListening]); + +// const stop = useCallback(() => { +// if (!(recognition && isListening)) { +// return; +// } +// recognition.stop(); +// setIsListening(false); +// }, [recognition, isListening]); + +// useEffect(() => { +// if (!recognition) { +// onError && +// onError("Speech recognition is not supported in this browser."); +// return; +// } + +// recognition.lang = lang; +// recognition.continuous = continuous; +// recognition.interimResults = interimResults; +// recognition.maxAlternatives = maxAlternatives; +// recognition.onresult = handleResult; +// recognition.onerror = (event) => onError?.(event.type); +// recognition.onend = () => { +// setIsListening(false); +// }; + +// return () => { +// if (isListening) { +// recognition.stop(); +// } +// }; +// }, [ +// lang, +// continuous, +// interimResults, +// maxAlternatives, +// handleResult, +// onError, +// recognition, +// start, +// stop, +// isListening, +// ]); + +// return { start, stop, transcript, isListening }; +// }; + +// export default useSpeechToText; diff --git a/src/ui/cuicui/hooks/use-step/category.use-step.tsx b/src/ui/cuicui/hooks/use-step/category.use-step.tsx new file mode 100644 index 00000000..a1df15da --- /dev/null +++ b/src/ui/cuicui/hooks/use-step/category.use-step.tsx @@ -0,0 +1,25 @@ +import type { SingleComponentCategoryType } from "#/src/lib/types/component"; +import { PreviewUseStep } from "#/src/ui/cuicui/hooks/use-step/preview-use-step"; +import { ListEndIcon } from "lucide-react"; + +export const useStepCategory: SingleComponentCategoryType = { + name: "Use Step", + slug: "use-step", + description: "Create a step component", + releaseDateCategory: new Date("2024-09-26"), + icon: ListEndIcon, + component: { + rerenderButton: true, + lastUpdatedDateComponent: new Date("2024-09-26"), + sizePreview: "md", + isIframed: false, + variantList: [ + { + name: "variant 1", + component: , + slugComponentFile: "use-step", + slugPreviewFile: "preview-use-step", + }, + ], + }, +}; diff --git a/src/ui/cuicui/hooks/use-step/preview-use-step.tsx b/src/ui/cuicui/hooks/use-step/preview-use-step.tsx new file mode 100644 index 00000000..711b2ca6 --- /dev/null +++ b/src/ui/cuicui/hooks/use-step/preview-use-step.tsx @@ -0,0 +1,40 @@ +"use client"; +import { useStep } from "#/src/ui/cuicui/hooks/use-step/use-step"; + +export function PreviewUseStep() { + const [currentStep, helpers] = useStep(5); + + const { + canGoToPrevStep, + canGoToNextStep, + goToNextStep, + goToPrevStep, + reset, + setStep, + } = helpers; + + return ( + <> +

Current step is {currentStep}

+

Can go to previous step {canGoToPrevStep ? "yes" : "no"}

+

Can go to next step {canGoToNextStep ? "yes" : "no"}

+ + + + + + ); +} diff --git a/src/ui/cuicui/hooks/use-step/use-step.ts b/src/ui/cuicui/hooks/use-step/use-step.ts new file mode 100644 index 00000000..57081b16 --- /dev/null +++ b/src/ui/cuicui/hooks/use-step/use-step.ts @@ -0,0 +1,65 @@ +"use client"; +import { useCallback, useState } from "react"; + +import type { Dispatch, SetStateAction } from "react"; + +type UseStepActions = { + goToNextStep: () => void; + goToPrevStep: () => void; + reset: () => void; + canGoToNextStep: boolean; + canGoToPrevStep: boolean; + setStep: Dispatch>; +}; + +type SetStepCallbackType = (step: number | ((step: number) => number)) => void; + +export function useStep(maxStep: number): [number, UseStepActions] { + const [currentStep, setCurrentStep] = useState(1); + + const canGoToNextStep = currentStep + 1 <= maxStep; + const canGoToPrevStep = currentStep - 1 > 0; + + const setStep = useCallback( + (step) => { + // Allow value to be a function so we have the same API as useState + const newStep = step instanceof Function ? step(currentStep) : step; + + if (newStep >= 1 && newStep <= maxStep) { + setCurrentStep(newStep); + return; + } + + throw new Error("Step not valid"); + }, + [maxStep, currentStep], + ); + + const goToNextStep = useCallback(() => { + if (canGoToNextStep) { + setCurrentStep((step) => step + 1); + } + }, [canGoToNextStep]); + + const goToPrevStep = useCallback(() => { + if (canGoToPrevStep) { + setCurrentStep((step) => step - 1); + } + }, [canGoToPrevStep]); + + const reset = useCallback(() => { + setCurrentStep(1); + }, []); + + return [ + currentStep, + { + goToNextStep, + goToPrevStep, + canGoToNextStep, + canGoToPrevStep, + setStep, + reset, + }, + ]; +} diff --git a/src/ui/cuicui/hooks/use-text-to-speech/category.use-text-to-speech.tsx b/src/ui/cuicui/hooks/use-text-to-speech/category.use-text-to-speech.tsx new file mode 100644 index 00000000..9f9a91c6 --- /dev/null +++ b/src/ui/cuicui/hooks/use-text-to-speech/category.use-text-to-speech.tsx @@ -0,0 +1,32 @@ +import type { SingleComponentCategoryType } from "#/src/lib/types/component"; +import { SpeechIcon } from "lucide-react"; + +export const useTextToSpeechCategory: SingleComponentCategoryType = { + slug: "use-text-to-speech", + name: "Use Text to Speech", + description: "A hook that allows you to convert text to speech", + releaseDateCategory: new Date("2024-09-28"), + icon: SpeechIcon, + // previewCategory: { + // component: , + // previewImage: PreviewImageUseTextToSpeech, + // previewScale: 0.8, + // }, + comingSoonCategory: true, + component: null, + // component: { + // lastUpdatedDateComponent: new Date("2024-08-28"), + // sizePreview: "lg", + + // isIframed: true, + + // variantList: [ + // { + // name: "variant 1", + // component: , + // slugComponentFile: "use-text-to-speech", + // slugPreviewFile: "preview-text-to-speech", + // }, + // ], + // }, +}; diff --git a/src/ui/cuicui/hooks/use-text-to-speech/preview-use-text-to-speech.tsx b/src/ui/cuicui/hooks/use-text-to-speech/preview-use-text-to-speech.tsx new file mode 100644 index 00000000..c2b9d7f2 --- /dev/null +++ b/src/ui/cuicui/hooks/use-text-to-speech/preview-use-text-to-speech.tsx @@ -0,0 +1 @@ +// Coming soon diff --git a/src/ui/cuicui/hooks/use-text-to-speech/use-text-to-speech.ts b/src/ui/cuicui/hooks/use-text-to-speech/use-text-to-speech.ts new file mode 100644 index 00000000..b975d2ab --- /dev/null +++ b/src/ui/cuicui/hooks/use-text-to-speech/use-text-to-speech.ts @@ -0,0 +1,33 @@ +// import { useEffect, useRef, useState } from "react"; + +// const useSetState = (initialState = {}) => { +// const [state, set] = useState(initialState); +// const setState = (patch) => { +// Object.assign(state, patch); +// set(state); +// }; + +// return [state, setState]; +// }; + +// const useSpeech = (text, opts = {}) => { +// const [state, setState] = useSetState({ +// isPlaying: false, +// volume: opts.volume || 1, +// }); + +// const uterranceRef = useRef(null); + +// useEffect(() => { +// const utterance = new SpeechSynthesisUtterance(text); +// utterance.volume = opts.volume || 1; +// utterance.onstart = () => setState({ isPlaying: true }); +// utterance.onresume = () => setState({ isPlaying: true }); +// utterance.onend = () => setState({ isPlaying: false }); +// utterance.onpause = () => setState({ isPlaying: false }); +// uterranceRef.current = utterance; +// window.speechSynthesis.speak(uterranceRef.current); +// }, []); + +// return state; +// }; diff --git a/src/ui/cuicui/hooks/use-throttle/category.use-throttle.tsx b/src/ui/cuicui/hooks/use-throttle/category.use-throttle.tsx new file mode 100644 index 00000000..36ce35a9 --- /dev/null +++ b/src/ui/cuicui/hooks/use-throttle/category.use-throttle.tsx @@ -0,0 +1,29 @@ +import { ViewIcon } from "lucide-react"; +import type { SingleComponentCategoryType } from "#/src/lib/types/component"; +import { PreviewUseThrottle } from "#/src/ui/cuicui/hooks/use-throttle/preview-use-throttle"; + +export const useThrottleCategory: SingleComponentCategoryType = { + slug: "use-throttle", + name: "Use Throttle", + description: + "A hook that throttles the execution of a function, limiting how often it can be invoked", + releaseDateCategory: new Date("2024-09-28"), + icon: ViewIcon, + previewCategory: { + component: , + previewScale: 0.8, + }, + component: { + lastUpdatedDateComponent: new Date("2024-09-28"), + sizePreview: "sm", + isIframed: false, + variantList: [ + { + name: "default variant", + component: , + slugComponentFile: "use-throttle", + slugPreviewFile: "preview-use-throttle", + }, + ], + }, +}; diff --git a/src/ui/cuicui/hooks/use-throttle/preview-use-throttle.tsx b/src/ui/cuicui/hooks/use-throttle/preview-use-throttle.tsx new file mode 100644 index 00000000..e224c9ef --- /dev/null +++ b/src/ui/cuicui/hooks/use-throttle/preview-use-throttle.tsx @@ -0,0 +1,29 @@ +"use client"; +import { useThrottle } from "#/src/ui/cuicui/hooks/use-throttle/use-throttle"; +import { useEffect, useState } from "react"; +import { toast } from "sonner"; + +const performSearch = (searchTerm: string) => { + toast(`Searching for: ${searchTerm}`); +}; + +export const PreviewUseThrottle = () => { + const [searchTerm, setSearchTerm] = useState(""); + const throttledSearchTerm = useThrottle(searchTerm, 500); + + useEffect(() => { + if (throttledSearchTerm) { + // Perform the search operation, e.g., API call + performSearch(throttledSearchTerm); + } + }, [throttledSearchTerm]); + + return ( + setSearchTerm(e.target.value)} + placeholder="Search..." + /> + ); +}; diff --git a/src/ui/cuicui/hooks/use-throttle/use-throttle.ts b/src/ui/cuicui/hooks/use-throttle/use-throttle.ts new file mode 100644 index 00000000..bdb17573 --- /dev/null +++ b/src/ui/cuicui/hooks/use-throttle/use-throttle.ts @@ -0,0 +1,38 @@ +"use client"; +import { useState, useEffect, useRef } from "react"; + +/** + * A custom React hook that throttles a value, ensuring that updates occur + * no more frequently than the specified limit. + * + * @param value - The value to be throttled. + * @param limit - The time limit in milliseconds for throttling. + * @returns The throttled value. + * + * @example + * const throttledValue = useThrottle(inputValue, 1000); + */ +export function useThrottle(value: T, limit: number): T { + const [throttledValue, setThrottledValue] = useState(value); + const lastRan = useRef(Date.now()); + + useEffect(() => { + const handler = setTimeout( + () => { + const now = Date.now(); + if (now - lastRan.current >= limit) { + setThrottledValue(value); + lastRan.current = now; + } + }, + limit - (Date.now() - lastRan.current), + ); + + // Cleanup the timeout if value or limit changes before the timeout completes + return () => { + clearTimeout(handler); + }; + }, [value, limit]); + + return throttledValue; +} diff --git a/src/ui/cuicui/hooks/use-window-scroll-position/category.use-window-scroll-postion.tsx b/src/ui/cuicui/hooks/use-window-scroll-position/category.use-window-scroll-postion.tsx new file mode 100644 index 00000000..deec4007 --- /dev/null +++ b/src/ui/cuicui/hooks/use-window-scroll-position/category.use-window-scroll-postion.tsx @@ -0,0 +1,28 @@ +import { ScrollIcon } from "lucide-react"; +import type { SingleComponentCategoryType } from "#/src/lib/types/component"; +import { PreviewUseWindowScrollPosition } from "#/src/ui/cuicui/hooks/use-window-scroll-position/preview-use-window-scroll-position"; + +export const useWindowScrollPositionCategory: SingleComponentCategoryType = { + slug: "use-window-scroll-position", + name: "Use Window Scroll Position", + description: "A hook that allows you to track the window's scroll position", + releaseDateCategory: new Date("2024-09-28"), + icon: ScrollIcon, + previewCategory: { + component: , + previewScale: 0.8, + }, + component: { + lastUpdatedDateComponent: new Date("2024-09-28"), + sizePreview: "sm", + isIframed: false, + variantList: [ + { + name: "variant 1", + component: , + slugComponentFile: "use-window-scroll-position", + slugPreviewFile: "preview-use-window-scroll-position", + }, + ], + }, +}; diff --git a/src/ui/cuicui/hooks/use-window-scroll-position/preview-use-window-scroll-position.tsx b/src/ui/cuicui/hooks/use-window-scroll-position/preview-use-window-scroll-position.tsx new file mode 100644 index 00000000..eb24d951 --- /dev/null +++ b/src/ui/cuicui/hooks/use-window-scroll-position/preview-use-window-scroll-position.tsx @@ -0,0 +1,21 @@ +"use client"; +import { useWindowScrollPosition } from "#/src/ui/cuicui/hooks/use-window-scroll-position/use-window-scroll-position"; + +export const PreviewUseWindowScrollPosition = () => { + const { y } = useWindowScrollPosition({ throttleMs: 200 }); + + return ( +
+ ); +}; diff --git a/src/ui/cuicui/hooks/use-window-scroll-position/use-window-scroll-position.ts b/src/ui/cuicui/hooks/use-window-scroll-position/use-window-scroll-position.ts new file mode 100644 index 00000000..2cf3bdb7 --- /dev/null +++ b/src/ui/cuicui/hooks/use-window-scroll-position/use-window-scroll-position.ts @@ -0,0 +1,82 @@ +"use client"; +import { useEffect, useState, useCallback, useRef } from "react"; + +// Define the shape of the scroll position +interface ScrollPosition { + x: number; + y: number; +} + +// Define the options for the hook +interface UseWindowScrollPositionOptions { + throttleMs?: number; +} + +// Throttle function implementation +const useThrottle = (callback: () => void, delay: number) => { + const lastCall = useRef(0); + const timeout = useRef(null); + + const throttledFunction = useCallback(() => { + const now = Date.now(); + const remaining = delay - (now - lastCall.current); + + if (remaining <= 0) { + if (timeout.current) { + clearTimeout(timeout.current); + timeout.current = null; + } + lastCall.current = now; + callback(); + } else if (!timeout.current) { + timeout.current = setTimeout(() => { + lastCall.current = Date.now(); + timeout.current = null; + callback(); + }, remaining); + } + }, [callback, delay]); + + return throttledFunction; +}; + +export const useWindowScrollPosition = ( + options: UseWindowScrollPositionOptions = {}, +): ScrollPosition => { + const { throttleMs = 100 } = options; + + // Initialize state with (0,0) for SSR compatibility + const [scroll, setScroll] = useState({ + x: typeof window !== "undefined" ? window.scrollY : 0, + y: typeof window !== "undefined" ? window.scrollY : 0, + }); + + // Update scroll position + const updateScroll = useCallback(() => { + setScroll({ + x: window.scrollX, + y: window.scrollY, + }); + }, []); + + // Create a throttled version of the update function + const throttledUpdateScroll = useThrottle(updateScroll, throttleMs); + + useEffect(() => { + // Ensure window is available (avoids issues with SSR) + if (typeof window === "undefined") { + return; + } + + window.addEventListener("scroll", throttledUpdateScroll); + + // Initial update in case the user has already scrolled + throttledUpdateScroll(); + + return () => { + window.removeEventListener("scroll", throttledUpdateScroll); + }; + }, [throttledUpdateScroll]); + + return scroll; +}; diff --git a/src/ui/cuicui/hooks/use-window-size/category.use-window-size.tsx b/src/ui/cuicui/hooks/use-window-size/category.use-window-size.tsx new file mode 100644 index 00000000..e133b731 --- /dev/null +++ b/src/ui/cuicui/hooks/use-window-size/category.use-window-size.tsx @@ -0,0 +1,30 @@ +import type { SingleComponentCategoryType } from "#/src/lib/types/component"; +import { PreviewUseWindowSize } from "#/src/ui/cuicui/hooks/use-window-size/preview-use-window-size"; +import { AppWindowMacIcon } from "lucide-react"; + +export const useWindowSizeCategory: SingleComponentCategoryType = { + slug: "use-window-size", + name: "Use Window Size", + description: "A hook that allows you to track the size of the browser window", + releaseDateCategory: new Date("2024-08-28"), + icon: AppWindowMacIcon, + previewCategory: { + component: , + previewScale: 0.8, + }, + component: { + lastUpdatedDateComponent: new Date("2024-08-28"), + sizePreview: "sm", + + isIframed: false, + + variantList: [ + { + name: "Default Variant", + component: , + slugComponentFile: "use-window-size", + slugPreviewFile: "preview-use-window-size", + }, + ], + }, +}; diff --git a/src/ui/cuicui/hooks/use-window-size/preview-use-window-size.tsx b/src/ui/cuicui/hooks/use-window-size/preview-use-window-size.tsx new file mode 100644 index 00000000..58110665 --- /dev/null +++ b/src/ui/cuicui/hooks/use-window-size/preview-use-window-size.tsx @@ -0,0 +1,7 @@ +"use client"; +import { useWindowSize } from "#/src/ui/cuicui/hooks/use-window-size/use-window-size"; + +export function PreviewUseWindowSize() { + const windowSize = useWindowSize(); + return
{JSON.stringify(windowSize, null, 2)}
; +} diff --git a/src/ui/cuicui/hooks/use-window-size/use-window-size.ts b/src/ui/cuicui/hooks/use-window-size/use-window-size.ts new file mode 100644 index 00000000..bc5f730a --- /dev/null +++ b/src/ui/cuicui/hooks/use-window-size/use-window-size.ts @@ -0,0 +1,42 @@ +"use client"; +import { useEffect, useState } from "react"; + +function getSize() { + // Check if 'window' is defined (i.e., the code is running in the browser) + if (typeof window !== "undefined") { + return { + innerHeight: window.innerHeight, + innerWidth: window.innerWidth, + outerHeight: window.outerHeight, + outerWidth: window.outerWidth, + }; + } + // Provide default values for SSR + return { + innerHeight: 0, + innerWidth: 0, + outerHeight: 0, + outerWidth: 0, + }; +} + +export function useWindowSize() { + const [windowSize, setWindowSize] = useState(getSize()); + + useEffect(() => { + if (typeof window === "undefined") { + return; + } + + function handleResize() { + setWindowSize(getSize()); + } + + window.addEventListener("resize", handleResize); + return () => { + window.removeEventListener("resize", handleResize); + }; + }, []); + + return windowSize; +} diff --git a/src/ui/cuicui/other/cursors/dynamic-cards/card-mouse-highlighting-border.tsx b/src/ui/cuicui/other/cursors/dynamic-cards/card-mouse-highlighting-border.tsx index 46ca9304..fadaa05e 100644 --- a/src/ui/cuicui/other/cursors/dynamic-cards/card-mouse-highlighting-border.tsx +++ b/src/ui/cuicui/other/cursors/dynamic-cards/card-mouse-highlighting-border.tsx @@ -1,4 +1,4 @@ -import { useMouse } from "#/src/ui/cuicui/hooks/use-mouse/hook/use-mouse"; +import { useMouse } from "#/src/ui/cuicui/hooks/use-mouse/use-mouse"; export const CardMouseHighlightingBorder = ({ title, diff --git a/src/ui/cuicui/other/cursors/dynamic-cards/gradient-card.tsx b/src/ui/cuicui/other/cursors/dynamic-cards/gradient-card.tsx index 69c4c9f3..876fecd7 100644 --- a/src/ui/cuicui/other/cursors/dynamic-cards/gradient-card.tsx +++ b/src/ui/cuicui/other/cursors/dynamic-cards/gradient-card.tsx @@ -1,7 +1,7 @@ "use client"; import { ArrowUpRightIcon } from "lucide-react"; import type { ReactNode } from "react"; -import { useMouse } from "#/src/ui/cuicui/hooks/use-mouse/hook/use-mouse"; +import { useMouse } from "#/src/ui/cuicui/hooks/use-mouse/use-mouse"; import { cn } from "#/src/utils/cn"; export const GradientCard = ({ diff --git a/src/ui/cuicui/other/cursors/follow-cursor/replace-cursor.tsx b/src/ui/cuicui/other/cursors/follow-cursor/replace-cursor.tsx index d527f021..151223c3 100644 --- a/src/ui/cuicui/other/cursors/follow-cursor/replace-cursor.tsx +++ b/src/ui/cuicui/other/cursors/follow-cursor/replace-cursor.tsx @@ -1,6 +1,6 @@ "use client"; -import { useMouse } from "#/src/ui/cuicui/hooks/use-mouse/hook/use-mouse"; +import { useMouse } from "#/src/ui/cuicui/hooks/use-mouse/use-mouse"; export default function FollowCursorHideCursor() { const [mouse, parentRef] = useMouse(); diff --git a/src/ui/cuicui/other/cursors/follow-cursor/with-cursor.tsx b/src/ui/cuicui/other/cursors/follow-cursor/with-cursor.tsx index cf64f5a8..e78858bd 100644 --- a/src/ui/cuicui/other/cursors/follow-cursor/with-cursor.tsx +++ b/src/ui/cuicui/other/cursors/follow-cursor/with-cursor.tsx @@ -1,6 +1,6 @@ "use client"; -import { useMouse } from "#/src/ui/cuicui/hooks/use-mouse/hook/use-mouse"; +import { useMouse } from "#/src/ui/cuicui/hooks/use-mouse/use-mouse"; export default function FollowCursorWithCursor() { const [mouse, parentRef] = useMouse(); diff --git a/src/ui/cuicui/other/other.section.tsx b/src/ui/cuicui/other/other.section.tsx index bab9c126..0364516f 100644 --- a/src/ui/cuicui/other/other.section.tsx +++ b/src/ui/cuicui/other/other.section.tsx @@ -7,6 +7,8 @@ import { transitionWrappersCategory } from "#/src/ui/cuicui/other/transition-wra export const otherSection: SectionType = { name: "Other", slug: "other", + type: "multiple-component", + categoriesList: [ cursorCategory, mockUpsCategory, diff --git a/src/ui/cuicui/other/qr-code/qr-code-generator/qr-code-generator.tsx b/src/ui/cuicui/other/qr-code/qr-code-generator/qr-code-generator.tsx index 14b1fc2b..204a3cd2 100644 --- a/src/ui/cuicui/other/qr-code/qr-code-generator/qr-code-generator.tsx +++ b/src/ui/cuicui/other/qr-code/qr-code-generator/qr-code-generator.tsx @@ -3,7 +3,7 @@ import { QRCodeSVG } from "qrcode.react"; import { type RefObject, useRef, useState } from "react"; import { ModernSimpleInput } from "#/src/ui/cuicui/common-ui/inputs/modern-simple-input/modern-simple-input"; -import { useCopyToClipboard } from "#/src/ui/cuicui/hooks/use-copy-to-clipboard/hook/use-copy-to-clipboard"; +import { useCopyToClipboard } from "#/src/ui/cuicui/hooks/use-copy-to-clipboard/use-copy-to-clipboard"; function downloadStringAsFile(data: string, filename: string) { const a = document.createElement("a"); diff --git a/src/ui/cuicui/other/transition-wrappers/slide-in-on-scroll/slide-in-on-scroll.component.tsx b/src/ui/cuicui/other/transition-wrappers/slide-in-on-scroll/slide-in-on-scroll.component.tsx index 0b712b6c..e5ae0e29 100644 --- a/src/ui/cuicui/other/transition-wrappers/slide-in-on-scroll/slide-in-on-scroll.component.tsx +++ b/src/ui/cuicui/other/transition-wrappers/slide-in-on-scroll/slide-in-on-scroll.component.tsx @@ -12,7 +12,8 @@ export const SlideInOnScrollComponent: ComponentType = { { name: "Slide in on scroll", component: , - slugPreviewFile: "slide-in-on-scroll", + slugPreviewFile: "preview-slide-in-on-scroll", + slugComponentFile: "slide-in-on-scroll", }, ], // supportedBrowers: ["chromium"], diff --git a/src/ui/cuicui/utils/cn/cn.category.tsx b/src/ui/cuicui/utils/cn/cn.category.tsx index a1b40ff3..064da491 100644 --- a/src/ui/cuicui/utils/cn/cn.category.tsx +++ b/src/ui/cuicui/utils/cn/cn.category.tsx @@ -1,34 +1,31 @@ -import type { CategoryType } from "#/src/lib/types/component"; -import PreviewCn from "#/src/ui/cuicui/utils/cn/cn/preview-cn"; +import type { SingleComponentCategoryType } from "#/src/lib/types/component"; +import PreviewCn from "#/src/ui/cuicui/utils/cn/preview-cn"; import { ChartNoAxesGanttIcon } from "lucide-react"; -export const cnCategory: CategoryType = { - name: "CN", +export const cnCategory: SingleComponentCategoryType = { + name: "ClassNames", slug: "cn", previewCategory: { previewScale: 0.5, component: , }, - componentList: [ - { - sizePreview: "sm", - slug: "cn", - isIframed: false, - title: "Classnames more known by cn()", - description: "A utility function to merge classnames in a clean way", - releaseDateComponent: new Date("2024-09-26"), - lastUpdatedDateComponent: new Date("2024-09-26"), - variantList: [ - { - name: "Default", - slugPreviewFile: "preview-cn", - slugComponentFile: "cn", - component: , - }, - ], - }, - ], - description: "", + component: { + sizePreview: "sm", + + isIframed: false, + + releaseDateComponent: new Date("2024-09-26"), + lastUpdatedDateComponent: new Date("2024-09-26"), + variantList: [ + { + name: "Default", + slugPreviewFile: "preview-cn", + slugComponentFile: "cn", + component: , + }, + ], + }, + description: "A utility function to merge classnames in a clean way", icon: ChartNoAxesGanttIcon, releaseDateCategory: new Date("2024-09-26"), }; diff --git a/src/ui/cuicui/utils/cn/cn/cn.ts b/src/ui/cuicui/utils/cn/cn.ts similarity index 100% rename from src/ui/cuicui/utils/cn/cn/cn.ts rename to src/ui/cuicui/utils/cn/cn.ts diff --git a/src/ui/cuicui/utils/cn/cn/preview-cn.tsx b/src/ui/cuicui/utils/cn/preview-cn.tsx similarity index 95% rename from src/ui/cuicui/utils/cn/cn/preview-cn.tsx rename to src/ui/cuicui/utils/cn/preview-cn.tsx index ad0ecb30..6dea8883 100644 --- a/src/ui/cuicui/utils/cn/cn/preview-cn.tsx +++ b/src/ui/cuicui/utils/cn/preview-cn.tsx @@ -1,5 +1,5 @@ "use client"; -import { cn } from "#/src/ui/cuicui/utils/cn/cn/cn"; +import { cn } from "#/src/ui/cuicui/utils/cn/cn"; import { useState } from "react"; export default function PreviewCn() { diff --git a/src/ui/cuicui/utils/sleep/sleep/preview-sleep.tsx b/src/ui/cuicui/utils/sleep/preview-sleep.tsx similarity index 100% rename from src/ui/cuicui/utils/sleep/sleep/preview-sleep.tsx rename to src/ui/cuicui/utils/sleep/preview-sleep.tsx diff --git a/src/ui/cuicui/utils/sleep/sleep.category.tsx b/src/ui/cuicui/utils/sleep/sleep.category.tsx index f1eb0f6d..fe324260 100644 --- a/src/ui/cuicui/utils/sleep/sleep.category.tsx +++ b/src/ui/cuicui/utils/sleep/sleep.category.tsx @@ -1,33 +1,29 @@ -import type { CategoryType } from "#/src/lib/types/component"; -import PreviewSleep from "#/src/ui/cuicui/utils/sleep/sleep/preview-sleep"; +import type { SingleComponentCategoryType } from "#/src/lib/types/component"; +import PreviewSleep from "#/src/ui/cuicui/utils/sleep/preview-sleep"; import { HourglassIcon } from "lucide-react"; -export const sleepCategory: CategoryType = { +export const sleepCategory: SingleComponentCategoryType = { name: "Sleep", slug: "sleep", previewCategory: { previewScale: 0.5, component: , }, - componentList: [ - { - sizePreview: "sm", - slug: "sleep", - title: "Sleep", - description: "A utility function to manage sleep", - releaseDateComponent: new Date("2024-09-26"), - lastUpdatedDateComponent: new Date("2024-09-26"), - variantList: [ - { - name: "Default", - slugPreviewFile: "preview-sleep", - slugComponentFile: "sleep", - component: , - }, - ], - }, - ], - description: "", + component: { + sizePreview: "sm", + + releaseDateComponent: new Date("2024-09-26"), + lastUpdatedDateComponent: new Date("2024-09-26"), + variantList: [ + { + name: "Default", + slugPreviewFile: "preview-sleep", + slugComponentFile: "sleep", + component: , + }, + ], + }, + description: "A utility function to manage sleep", icon: HourglassIcon, releaseDateCategory: new Date("2024-09-26"), }; diff --git a/src/ui/cuicui/utils/sleep/sleep/sleep.ts b/src/ui/cuicui/utils/sleep/sleep.ts similarity index 100% rename from src/ui/cuicui/utils/sleep/sleep/sleep.ts rename to src/ui/cuicui/utils/sleep/sleep.ts diff --git a/src/ui/cuicui/utils/utils.section.tsx b/src/ui/cuicui/utils/utils.section.tsx index 521f7e3f..837fc87d 100644 --- a/src/ui/cuicui/utils/utils.section.tsx +++ b/src/ui/cuicui/utils/utils.section.tsx @@ -3,6 +3,7 @@ import { cnCategory } from "#/src/ui/cuicui/utils/cn/cn.category"; import { sleepCategory } from "#/src/ui/cuicui/utils/sleep/sleep.category"; export const utilsSection: SectionType = { + type: "single-component", name: "Utils", slug: "utils", categoriesList: [cnCategory, sleepCategory], diff --git a/src/ui/navigation/animated-background.tsx b/src/ui/navigation/animated-background.tsx new file mode 100644 index 00000000..caf2b857 --- /dev/null +++ b/src/ui/navigation/animated-background.tsx @@ -0,0 +1,93 @@ +"use client"; + +import { cn } from "#/src/utils/cn"; +import { AnimatePresence, type Transition, motion } from "framer-motion"; +import { + Children, + cloneElement, + type ReactElement, + useEffect, + useState, + useId, +} from "react"; + +type AnimatedBackgroundProps = { + children: + | ReactElement<{ "data-id": string }>[] + | ReactElement<{ "data-id": string }>; + defaultValue?: string; + onValueChange?: (newActiveId: string | null) => void; + className?: string; + transition?: Transition; + enableHover?: boolean; +}; + +export default function AnimatedBackground({ + children, + defaultValue, + onValueChange, + className, + transition, + enableHover = false, +}: AnimatedBackgroundProps) { + const [activeId, setActiveId] = useState(null); + const uniqueId = useId(); + + const handleSetActiveId = (id: string | null) => { + setActiveId(id); + + if (onValueChange) { + onValueChange(id); + } + }; + + useEffect(() => { + if (defaultValue !== undefined) { + setActiveId(defaultValue); + } + }, [defaultValue]); + + return Children.map(children, (child: any, index) => { + const id = child.props["data-id"]; + + const interactionProps = enableHover + ? { + onMouseEnter: () => handleSetActiveId(id), + onMouseLeave: () => handleSetActiveId(null), + } + : { + onClick: () => handleSetActiveId(id), + }; + + return cloneElement( + child, + { + //TODO: TRY TO FIX THIS KEY + key: index, + className: cn("relative inline-flex", child.props.className), + "aria-selected": activeId === id, + "data-checked": activeId === id ? "true" : "false", + ...interactionProps, + }, + <> + + {activeId === id && ( + + )} + + {child.props.children} + , + ); + }); +} diff --git a/src/ui/navigation/desktop-menu.tsx b/src/ui/navigation/desktop-menu.tsx index 6f199ca1..41267b7b 100644 --- a/src/ui/navigation/desktop-menu.tsx +++ b/src/ui/navigation/desktop-menu.tsx @@ -13,7 +13,7 @@ import { ScrollArea } from "../shadcn/scrollarea"; import StarGithubProjectButton from "../star-github-project-button"; import ThemeSwitcher from "../theme-switcher"; import NavigationMenu from "./navigation-menu"; -import { SearchMenu } from "./search-menu"; +import { SearchMenu } from "../../components/search-menu/search-menu"; export function DesktopSideMenu({ className, diff --git a/src/ui/navigation/navigation-item.tsx b/src/ui/navigation/navigation-item.tsx new file mode 100644 index 00000000..be71dc8c --- /dev/null +++ b/src/ui/navigation/navigation-item.tsx @@ -0,0 +1,108 @@ +import GradientContainer from "#/src/ui/gradient-container"; +import GradientText from "#/src/ui/gradient-text"; +import { cn } from "#/src/utils/cn"; +import { differenceInDays } from "date-fns"; +import { ArrowUpRightIcon, type LucideIcon } from "lucide-react"; +import Link from "next/link"; +import { useSelectedLayoutSegments } from "next/navigation"; +import type { ReactNode } from "react"; + +export function GlobalNavItem({ + isMobile, + href, + name, + Icon, + isComingSoon = false, + releaseDate, + updatedDate, + target = "sameWindow", +}: Readonly<{ + isMobile?: boolean; + href: string; + name: string; + // biome-ignore lint/style/useNamingConvention: + Icon: LucideIcon | null; + isComingSoon?: boolean; + releaseDate?: Date | null; + updatedDate?: Date | null; + target?: "newWindow" | "sameWindow"; +}>) { + const segments = useSelectedLayoutSegments(); + const splittedHref = href.split("/"); + const lastSegment = splittedHref[splittedHref.length - 1]; + + let isActive = false; + if (segments) { + isActive = segments.some((segment) => segment === lastSegment); + } + + const isNew = differenceInDays(new Date(), releaseDate ?? 0) < 21; + const isUpdated = differenceInDays(new Date(), updatedDate ?? 0) < 14; + + function getTagValue() { + if (isComingSoon) { + return "Coming soon"; + } + if (isNew) { + return "New"; + } + if (isUpdated) { + return "Updated"; + } + return null; + } + + return ( + +
+ {Icon && ( + + )} +

+ {name} +

+
+ {getTagValue() && ( + + {getTagValue()} + + )} + + {target === "newWindow" && ( + + )} + + ); +} + +export const SectionWrapper = ({ + children, + name, +}: { + children: ReactNode; + name: string; +}) => { + return ( +
+
+

{name}

+
+ {children} +
+ ); +}; diff --git a/src/ui/navigation/navigation-menu.tsx b/src/ui/navigation/navigation-menu.tsx index 5c2b633f..4f57d51b 100644 --- a/src/ui/navigation/navigation-menu.tsx +++ b/src/ui/navigation/navigation-menu.tsx @@ -1,45 +1,17 @@ -"use client"; -import { differenceInDays } from "date-fns"; -import { AnimatePresence, motion } from "framer-motion"; -import { - ArrowUpRightIcon, - LayoutListIcon, - PaletteIcon, - TangentIcon, - type LucideIcon, -} from "lucide-react"; -import Link from "next/link"; -import { useSelectedLayoutSegments } from "next/navigation"; -import { type ReactNode, useState } from "react"; +import { LayoutListIcon, PaletteIcon, TangentIcon } from "lucide-react"; import { SectionsList } from "#/src/lib/cuicui-components/sections-list"; import { firstMenuSection } from "#/src/lib/first-menu-section"; import { cn } from "#/src/utils/cn"; - -import GradientContainer from "../gradient-container"; -import GradientText from "../gradient-text"; +import AnimatedBackground from "#/src/ui/navigation/animated-background"; +import { + GlobalNavItem, + SectionWrapper, +} from "#/src/ui/navigation/navigation-item"; export default function NavigationMenu({ isMobile, className, }: Readonly<{ isMobile?: boolean; className?: string }>) { - const [elementFocused, setElementFocused] = useState( - null, - ); - const handleHoverButton = (index: number | string | null) => { - setElementFocused(index); - }; - - function getAlphabeticallySortedSectionList() { - return SectionsList.map((section) => { - return { - ...section, - items: section.categoriesList.toSorted((a, b) => - a.name.localeCompare(b.name), - ), - }; - }); - } - function getClosestUpdatedComponentDate(dateList: (Date | null)[]) { return dateList.reduce((acc, date) => { if (!date) { @@ -52,51 +24,75 @@ export default function NavigationMenu({ }, null); } - const alphabeticallySortedSectionList = getAlphabeticallySortedSectionList(); - return ( -