mirror of
https://github.com/lucide-icons/lucide.git
synced 2025-12-15 21:17:41 +01:00
Add site categories (#876)
* bump package * Add Logo * remove console * prettify it * add favicons and fix issue * Add categorie page * add drag and drop * Make drag and drop working * Add drag options * Add modal * small styling fixes * fix search * Add code editor * Add more styling * Add more categories * create context provider * refactor eslint thing * update chakra-ui * improve, category bar * Add sortly * Add categories * Try to fix new categories * Fix Categories * Add docs Menu Tree data * Start with sectiontitles * Create link list * Add Docs menu to mobile * Add some more pages and text * Optimize text * add license to the menu * update packages * Fix build * Update title * Remove ModifiedTooltip * Fix assets * add yarn to copy-assets command * install deps * update * try something * new categories page * try something * Add icons reorder * add new icons page * add category view button * add categories * Fix vercel build * Add sidebar and costumize button * fix merge conlfict * Remove console.logs * add sidebar * Add icon overview * Fix key render issue * Fix types * Fix category render * Fix build * update lockfile * Added category icon and icon count in list. Moved scrollbar to the left to make it less intrusive --------- Co-authored-by: Eric Fennis <eric.fennis@endurance.com> Co-authored-by: Eric Fennis <eric@dreamteam.nl> Co-authored-by: Karsa <karsa@karsa.org>
This commit is contained in:
15
.vscode/launch.json
vendored
Normal file
15
.vscode/launch.json
vendored
Normal file
@@ -0,0 +1,15 @@
|
||||
{
|
||||
// Use IntelliSense to learn about possible attributes.
|
||||
// Hover to view descriptions of existing attributes.
|
||||
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
|
||||
"version": "0.2.0",
|
||||
"configurations": [
|
||||
{
|
||||
"type": "pwa-chrome",
|
||||
"request": "launch",
|
||||
"name": "Launch Chrome against localhost",
|
||||
"url": "http://localhost:8080",
|
||||
"webRoot": "${workspaceFolder}"
|
||||
}
|
||||
]
|
||||
}
|
||||
7
.vscode/settings.json
vendored
Normal file
7
.vscode/settings.json
vendored
Normal file
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"cSpell.words": [
|
||||
"devs",
|
||||
"preact",
|
||||
"Preact"
|
||||
]
|
||||
}
|
||||
@@ -2,4 +2,4 @@
|
||||
"$schema": "../category.schema.json",
|
||||
"title": "Text formatting",
|
||||
"icon": "type"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { forwardRef, createElement, ReactSVG, SVGProps } from 'react';
|
||||
import defaultAttributes from './defaultAttributes';
|
||||
|
||||
type IconNode = [elementName: keyof ReactSVG, attrs: Record<string, string>][]
|
||||
export type IconNode = [elementName: keyof ReactSVG, attrs: Record<string, string>][]
|
||||
|
||||
export type SVGAttributes = Partial<SVGProps<SVGSVGElement>>
|
||||
|
||||
|
||||
353
pnpm-lock.yaml
generated
353
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@@ -7,6 +7,7 @@ module.exports = {
|
||||
'eslint:recommended',
|
||||
'plugin:@typescript-eslint/recommended',
|
||||
'plugin:@next/next/recommended',
|
||||
'plugin:react-hooks/recommended'
|
||||
],
|
||||
parserOptions: {
|
||||
tsconfigRootDir: __dirname,
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
"scripts": {
|
||||
"dev": "next dev",
|
||||
"copy-assets": "mkdir -p ./public/docs/images && cp -rf ../docs/images ./public/docs",
|
||||
"prebuild": "ts-node --swc scripts/preBuild.ts",
|
||||
"prebuild": "ts-node --swc scripts/preBuild.ts && pnpm -w lucide-react build",
|
||||
"build": "pnpm copy-assets && pnpm prebuild && next build",
|
||||
"export": "next export -o build",
|
||||
"deploy": "pnpm build && pnpm export",
|
||||
@@ -24,16 +24,17 @@
|
||||
"@next/mdx": "^11.0.0",
|
||||
"@svgr/webpack": "^6.3.1",
|
||||
"downloadjs": "^1.4.7",
|
||||
"framer-motion": "^6.2.8",
|
||||
"element-to-path": "^1.2.1",
|
||||
"framer-motion": "^4",
|
||||
"fuse.js": "^6.5.3",
|
||||
"gray-matter": "^4.0.3",
|
||||
"js-yaml": "^4.1.0",
|
||||
"jszip": "^3.7.0",
|
||||
"lodash": "^4.17.20",
|
||||
"lucide-react": "^0.94.0",
|
||||
"lucide-react": "workspace:*",
|
||||
"next": "12",
|
||||
"next-mdx-remote": "^3.0.2",
|
||||
"object-path": "0.11.5",
|
||||
"prism-react-renderer": "^1.2.1",
|
||||
"react": "17.0.2",
|
||||
"react-color": "^2.19.3",
|
||||
@@ -59,6 +60,7 @@
|
||||
"babel-loader": "^8.1.0",
|
||||
"eslint": "^8.22.0",
|
||||
"eslint-config-prettier": "^8.5.0",
|
||||
"eslint-plugin-react-hooks": "^4.6.0",
|
||||
"jest": "^26.5.2",
|
||||
"node-fetch": "2",
|
||||
"prettier": "^2.3.2",
|
||||
|
||||
@@ -17,3 +17,7 @@ body {
|
||||
min-height: 100%;
|
||||
padding-bottom: 80px;
|
||||
}
|
||||
|
||||
html:has(*:target) {
|
||||
scroll-behavior: smooth;
|
||||
}
|
||||
|
||||
123
site/src/components/CategoryChangesBar.tsx
Normal file
123
site/src/components/CategoryChangesBar.tsx
Normal file
@@ -0,0 +1,123 @@
|
||||
import {
|
||||
Box,
|
||||
Button,
|
||||
Flex,
|
||||
Modal,
|
||||
ModalBody,
|
||||
ModalCloseButton,
|
||||
ModalContent,
|
||||
ModalFooter,
|
||||
ModalHeader,
|
||||
ModalOverlay,
|
||||
Text,
|
||||
Divider,
|
||||
} from '@chakra-ui/react';
|
||||
import theme from '../lib/theme';
|
||||
import { useMemo, useState } from 'react';
|
||||
import CodeBlock from './CodeBlock';
|
||||
import CopyButton from './CopyButton';
|
||||
import { IconEntity } from 'src/types';
|
||||
|
||||
const CategoryChangesBar = ({ categories, changes }) => {
|
||||
const [modalOpen, setModalOpen] = useState(false);
|
||||
const [showCode, setShowCode] = useState(false);
|
||||
const handleSubmit = () => {
|
||||
setModalOpen(true);
|
||||
};
|
||||
|
||||
const openPullRequestUrl = 'https://github.com/lucide-icons/lucide/edit/master/categories.json';
|
||||
|
||||
const onClose = () => {
|
||||
setModalOpen(false);
|
||||
};
|
||||
|
||||
const newMappedCategories = useMemo(() => {
|
||||
return Object.fromEntries(
|
||||
Object.entries(categories).map(([category, icons]) => [
|
||||
category,
|
||||
(icons as IconEntity[]).map(({ name }) => name),
|
||||
]),
|
||||
);
|
||||
}, [categories]);
|
||||
|
||||
const categoryCode = useMemo(() => JSON.stringify(newMappedCategories, null, ' '), [
|
||||
newMappedCategories,
|
||||
]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Box
|
||||
borderWidth="1px"
|
||||
rounded="lg"
|
||||
width="full"
|
||||
boxShadow={theme.shadows.xl}
|
||||
position="relative"
|
||||
padding={4}
|
||||
maxW="960px"
|
||||
margin="0 auto"
|
||||
>
|
||||
<Flex alignItems="center" justifyContent="space-between">
|
||||
<Text fontSize="lg">You're editing the overview categories.</Text>
|
||||
<Flex>
|
||||
<Text fontSize="lg" fontWeight="bold" marginLeft={4}>
|
||||
{changes}
|
||||
</Text>
|
||||
<Text fontSize="lg" marginLeft={2}>
|
||||
changes made to 'categories.json'
|
||||
</Text>
|
||||
</Flex>
|
||||
<Button onClick={handleSubmit} colorScheme="red" variant="solid">
|
||||
Submit Pull-request
|
||||
</Button>
|
||||
</Flex>
|
||||
</Box>
|
||||
<Modal isOpen={modalOpen} onClose={onClose} size="xl">
|
||||
<ModalOverlay />
|
||||
<ModalContent>
|
||||
<ModalHeader>Nice changes!</ModalHeader>
|
||||
<ModalCloseButton />
|
||||
<ModalBody>
|
||||
<Text>To submit those changes, follow these steps:</Text>
|
||||
<Text fontWeight="bold" mt={4}>
|
||||
Step 1:
|
||||
</Text>
|
||||
<Text mt={2} mb={2}>
|
||||
Copy all the code,
|
||||
</Text>
|
||||
|
||||
<CopyButton copyText={categoryCode} buttonText="Copy code" mr={4} />
|
||||
<Text fontWeight="bold" mt={6}>
|
||||
Step 2:
|
||||
</Text>
|
||||
<Text mt={2} mb={4}>
|
||||
Open Pull-request, select-all and paste the code with the changes.
|
||||
</Text>
|
||||
<Button as="a" variant="solid" href={openPullRequestUrl} target="__blank">
|
||||
Open pull-request
|
||||
</Button>
|
||||
<Divider mt={8} mb={4} />
|
||||
<Text fontWeight="bold" mt={6} mb={4}>
|
||||
Code:
|
||||
</Text>
|
||||
<Button as="a" variant="solid" onClick={() => setShowCode(show => !show)}>
|
||||
Show code
|
||||
</Button>
|
||||
{showCode && (
|
||||
<Box maxHeight={320} overflow="auto" background="gray.800">
|
||||
<CodeBlock code={categoryCode} language="json" showLines />
|
||||
</Box>
|
||||
)}
|
||||
</ModalBody>
|
||||
|
||||
<ModalFooter>
|
||||
<Button colorScheme="blue" mr={3} onClick={onClose}>
|
||||
Done
|
||||
</Button>
|
||||
</ModalFooter>
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default CategoryChangesBar;
|
||||
@@ -2,7 +2,6 @@ import { Box, BoxProps, chakra, useColorMode } from '@chakra-ui/react';
|
||||
import nightOwlLightTheme from 'prism-react-renderer/themes/nightOwlLight';
|
||||
import nightOwlDarkTheme from 'prism-react-renderer/themes/nightOwl';
|
||||
import uiTheme from '../lib/theme';
|
||||
// import theme from 'prism-react-renderer/themes/nightOwl';
|
||||
import BaseHighlight, { defaultProps, Language } from 'prism-react-renderer';
|
||||
import { CSSProperties } from 'react';
|
||||
import CopyButton from './CopyButton';
|
||||
@@ -49,7 +48,7 @@ function CodeBlock({ code, language, metastring, showLines, ...props }: Highligh
|
||||
const { colorMode } = useColorMode();
|
||||
|
||||
const backgroundColor =
|
||||
colorMode === 'light' ? uiTheme.colors.gray[100] : uiTheme.colors.gray[700];
|
||||
colorMode === 'light' ? uiTheme.colors.gray[100] : uiTheme.colors.gray[900];
|
||||
const codeTheme = colorMode === 'light' ? nightOwlLightTheme : nightOwlDarkTheme;
|
||||
|
||||
const customizedCodeTheme = {
|
||||
@@ -63,6 +62,8 @@ function CodeBlock({ code, language, metastring, showLines, ...props }: Highligh
|
||||
return (
|
||||
<Box position="relative" zIndex="0" {...props}>
|
||||
<CodeContainer bg={backgroundColor}>
|
||||
{/* eslint-disable-next-line @typescript-eslint/ban-ts-comment */}
|
||||
{/* @ts-ignore */}
|
||||
<BaseHighlight
|
||||
{...defaultProps}
|
||||
code={code}
|
||||
|
||||
@@ -4,7 +4,7 @@ const CopyButton = ({ copyText, buttonText = 'copy', ...props }) => {
|
||||
const { hasCopied, onCopy } = useClipboard(copyText);
|
||||
|
||||
return (
|
||||
<Button onClick={onCopy} {...props}>
|
||||
<Button onClick={onCopy} {...props} variant="solid">
|
||||
{hasCopied ? 'copied' : buttonText}
|
||||
</Button>
|
||||
);
|
||||
|
||||
@@ -59,10 +59,21 @@ export function CustomizeIconContext({ children }): JSX.Element {
|
||||
return <IconStyleContext.Provider value={value}>{children}</IconStyleContext.Provider>;
|
||||
}
|
||||
|
||||
export function useCustomizeIconContext(): ICustomIconStyle {
|
||||
export function useCustomizeIconContext() {
|
||||
const context = useContext(IconStyleContext);
|
||||
if (context === undefined) {
|
||||
throw new Error('useCustomizeIconContext must be used within a IconStyleContextProvider');
|
||||
return {
|
||||
color: 'currentColor',
|
||||
size: 24,
|
||||
strokeWidth: 2,
|
||||
iconsRef: { current: {} },
|
||||
/* eslint-disable @typescript-eslint/no-empty-function */
|
||||
setStroke: function() {},
|
||||
setColor: function() {},
|
||||
setSize: function() {},
|
||||
resetStyle: function() {},
|
||||
/* eslint-enable @typescript-eslint/no-empty-function */
|
||||
};
|
||||
}
|
||||
return context;
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Button, Flex, Link, WrapItem, Text, Wrap, Heading } from '@chakra-ui/react';
|
||||
import { Button, Flex, Link, WrapItem, Text, Wrap, Heading, Box } from '@chakra-ui/react';
|
||||
import download from 'downloadjs';
|
||||
import { Download, Github } from 'lucide-react';
|
||||
import NextLink from 'next/link';
|
||||
@@ -11,25 +11,10 @@ import PreactLogo from '../../public/framework-logos/preact.svg';
|
||||
import AngularLogo from '../../public/framework-logos/angular.svg';
|
||||
import FlutterLogo from '../../public/framework-logos/flutter.svg';
|
||||
import SvelteLogo from '../../public/framework-logos/svelte.svg';
|
||||
import { useState } from 'react';
|
||||
import { useCallback, useState } from 'react';
|
||||
import { useCustomizeIconContext } from './CustomizeIconContext';
|
||||
import { IconEntity } from '../types';
|
||||
|
||||
type IconContent = [icon: string, src:string];
|
||||
|
||||
async function generateZip(icons: IconContent[]) {
|
||||
const JSZip = (await import('jszip')).default
|
||||
|
||||
const zip = new JSZip();
|
||||
|
||||
const addingZipPromises = icons.map(([name, src]) =>
|
||||
zip.file(`${name}.svg`, src),
|
||||
);
|
||||
|
||||
await Promise.all(addingZipPromises)
|
||||
|
||||
return zip.generateAsync({ type: 'blob' });
|
||||
}
|
||||
import generateZip, { IconContent } from 'src/lib/generateZip';
|
||||
|
||||
interface HeaderProps {
|
||||
data: IconEntity[];
|
||||
@@ -37,25 +22,27 @@ interface HeaderProps {
|
||||
|
||||
const Header = ({ data }: HeaderProps) => {
|
||||
const [zippingIcons, setZippingIcons] = useState(false);
|
||||
const { iconsRef } = useCustomizeIconContext();
|
||||
const { iconsRef, strokeWidth, color, size } = useCustomizeIconContext();
|
||||
|
||||
const downloadAllIcons = async () => {
|
||||
const downloadAllIcons = useCallback(async () => {
|
||||
setZippingIcons(true);
|
||||
|
||||
let iconEntries: IconContent[] = Object.entries(iconsRef.current).map(([name, svgEl]) => [
|
||||
let iconEntries: IconContent[] = Object.entries(iconsRef.current)
|
||||
.map(([name, svgEl]) => [
|
||||
name,
|
||||
svgEl.outerHTML,
|
||||
]);
|
||||
|
||||
// Fallback
|
||||
if (iconEntries.length === 0) {
|
||||
iconEntries = data.map(icon => [icon.name, icon.src]);
|
||||
const getFallbackZip = (await import('../lib/getFallbackZip')).default
|
||||
iconEntries = getFallbackZip(data, { strokeWidth, color, size })
|
||||
}
|
||||
|
||||
const zip = await generateZip(iconEntries);
|
||||
download(zip, 'lucide.zip');
|
||||
setZippingIcons(false);
|
||||
};
|
||||
}, []);
|
||||
|
||||
const repositoryUrl = 'https://github.com/lucide-icons/lucide';
|
||||
|
||||
@@ -117,83 +104,85 @@ const Header = ({ data }: HeaderProps) => {
|
||||
];
|
||||
|
||||
return (
|
||||
<Flex direction="column" align="center" justify="center">
|
||||
<Heading as="h1" fontSize="4xl" mb="4" textAlign="center">
|
||||
Beautiful & consistent icon toolkit made by the community.
|
||||
</Heading>
|
||||
<Text fontSize="lg" as="p" textAlign="center" mb="1">
|
||||
Open-source project and a fork of{' '}
|
||||
<Link href="https://github.com/feathericons/feather" isExternal>
|
||||
Feather Icons
|
||||
</Link>
|
||||
. <br />
|
||||
We're expanding the icon set as much as possible while keeping it nice-looking -{' '}
|
||||
<Link href={repositoryUrl} isExternal>
|
||||
join us
|
||||
</Link>
|
||||
!
|
||||
</Text>
|
||||
<Wrap
|
||||
marginTop={4}
|
||||
marginBottom={6}
|
||||
spacing={{ base: 4, lg: 6 }}
|
||||
justify="center"
|
||||
align="center"
|
||||
>
|
||||
<WrapItem flexBasis="100%" style={{ marginBottom: 0 }}>
|
||||
<NextLink href="/packages" passHref>
|
||||
<Link _hover={{ opacity: 0.8 }} marginX="auto">
|
||||
<Text fontSize="md" opacity={0.5} as="p" textAlign="center" width="100%">
|
||||
Available for:
|
||||
</Text>
|
||||
</Link>
|
||||
</NextLink>
|
||||
</WrapItem>
|
||||
{packages.map(({ name, href, Logo, label }) => (
|
||||
<WrapItem key={name}>
|
||||
<NextLink href={href} key={name} passHref>
|
||||
<Link _hover={{ opacity: 0.8 }} aria-label={label} title={label}>
|
||||
<Logo />
|
||||
<Box maxW="1250px" mx="auto">
|
||||
<Flex direction="column" align="center" justify="center" py={12}>
|
||||
<Heading as="h1" fontSize="4xl" mb="4" textAlign="center">
|
||||
Beautiful & consistent icon toolkit made by the community.
|
||||
</Heading>
|
||||
<Text fontSize="lg" as="p" textAlign="center" mb="1">
|
||||
Open-source project and a fork of{' '}
|
||||
<Link href="https://github.com/feathericons/feather" isExternal>
|
||||
Feather Icons
|
||||
</Link>
|
||||
. <br />
|
||||
We're expanding the icon set as much as possible while keeping it nice-looking -{' '}
|
||||
<Link href={repositoryUrl} isExternal>
|
||||
join us
|
||||
</Link>
|
||||
!
|
||||
</Text>
|
||||
<Wrap
|
||||
marginTop={4}
|
||||
marginBottom={6}
|
||||
spacing={{ base: 4, lg: 6 }}
|
||||
justify="center"
|
||||
align="center"
|
||||
>
|
||||
<WrapItem flexBasis="100%" style={{ marginBottom: 0 }}>
|
||||
<NextLink href="/packages" passHref>
|
||||
<Link _hover={{ opacity: 0.8 }} marginX="auto">
|
||||
<Text fontSize="md" opacity={0.5} as="p" textAlign="center" width="100%">
|
||||
Available for:
|
||||
</Text>
|
||||
</Link>
|
||||
</NextLink>
|
||||
</WrapItem>
|
||||
))}
|
||||
<WrapItem>
|
||||
<NextLink href="/packages" passHref>
|
||||
<Link _hover={{ opacity: 0.8 }} marginX="auto">
|
||||
<Text fontSize="md" opacity={0.5}>More options</Text>
|
||||
</Link>
|
||||
</NextLink>
|
||||
</WrapItem>
|
||||
</Wrap>
|
||||
<Wrap marginTop={3} marginBottom={12} spacing="15px" justify="center">
|
||||
<WrapItem>
|
||||
<Button
|
||||
leftIcon={<Download />}
|
||||
size="lg"
|
||||
onClick={downloadAllIcons}
|
||||
isLoading={zippingIcons}
|
||||
loadingText="Creating zip.."
|
||||
>
|
||||
Download all
|
||||
</Button>
|
||||
</WrapItem>
|
||||
<WrapItem>
|
||||
<IconCustomizerDrawer />
|
||||
</WrapItem>
|
||||
<WrapItem>
|
||||
<Button
|
||||
as="a"
|
||||
leftIcon={<Github />}
|
||||
size="lg"
|
||||
href={repositoryUrl}
|
||||
target="__blank"
|
||||
>
|
||||
Github
|
||||
</Button>
|
||||
</WrapItem>
|
||||
</Wrap>
|
||||
</Flex>
|
||||
{packages.map(({ name, href, Logo, label }) => (
|
||||
<WrapItem key={name}>
|
||||
<NextLink href={href} key={name} passHref>
|
||||
<Link _hover={{ opacity: 0.8 }} aria-label={label}>
|
||||
<Logo />
|
||||
</Link>
|
||||
</NextLink>
|
||||
</WrapItem>
|
||||
))}
|
||||
<WrapItem>
|
||||
<NextLink href="/packages" passHref>
|
||||
<Link _hover={{ opacity: 0.8 }} marginX="auto">
|
||||
<Text fontSize="md" opacity={0.5}>More options</Text>
|
||||
</Link>
|
||||
</NextLink>
|
||||
</WrapItem>
|
||||
</Wrap>
|
||||
<Wrap marginTop={3} marginBottom={12} spacing="15px" justify="center">
|
||||
<WrapItem>
|
||||
<Button
|
||||
leftIcon={<Download />}
|
||||
size="lg"
|
||||
onClick={downloadAllIcons}
|
||||
isLoading={zippingIcons}
|
||||
loadingText="Creating zip.."
|
||||
>
|
||||
Download all
|
||||
</Button>
|
||||
</WrapItem>
|
||||
<WrapItem>
|
||||
<IconCustomizerDrawer />
|
||||
</WrapItem>
|
||||
<WrapItem>
|
||||
<Button
|
||||
as="a"
|
||||
leftIcon={<Github />}
|
||||
size="lg"
|
||||
href={repositoryUrl}
|
||||
target="__blank"
|
||||
>
|
||||
Github
|
||||
</Button>
|
||||
</WrapItem>
|
||||
</Wrap>
|
||||
</Flex>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
45
site/src/components/IconCategory.tsx
Normal file
45
site/src/components/IconCategory.tsx
Normal file
@@ -0,0 +1,45 @@
|
||||
import { Box, Text, useColorModeValue, BoxProps } from '@chakra-ui/react';
|
||||
import { forwardRef } from 'react';
|
||||
import theme from '../lib/theme';
|
||||
|
||||
interface IconCategoryProps extends BoxProps {
|
||||
active?: boolean;
|
||||
name: string;
|
||||
dragging: boolean;
|
||||
}
|
||||
|
||||
const IconCategory = forwardRef<HTMLDivElement, IconCategoryProps>(
|
||||
({ name, active = false, dragging, children, ...props }: IconCategoryProps, ref) => {
|
||||
const activeBackground = useColorModeValue(theme.colors.gray, theme.colors.gray[700]);
|
||||
const toTitleCase = string =>
|
||||
string
|
||||
.split(' ')
|
||||
.map(word => word[0].toUpperCase() + word.slice(1))
|
||||
.join(' ');
|
||||
|
||||
return (
|
||||
<Box
|
||||
backgroundColor={active ? activeBackground : 'transparent'}
|
||||
borderRadius={8}
|
||||
ref={ref}
|
||||
pointerEvents="all"
|
||||
padding={4}
|
||||
marginBottom={3}
|
||||
transition="background 120ms ease-in"
|
||||
_hover={{
|
||||
background: dragging && activeBackground,
|
||||
}}
|
||||
{...props}
|
||||
>
|
||||
<Box>
|
||||
<Text fontSize="xl" marginBottom={3}>
|
||||
{toTitleCase(name)}
|
||||
</Text>
|
||||
{children}
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
export default IconCategory;
|
||||
168
site/src/components/IconCategoryDrawer.tsx
Normal file
168
site/src/components/IconCategoryDrawer.tsx
Normal file
@@ -0,0 +1,168 @@
|
||||
import { Box, BoxProps, Button, Divider, Drawer, DrawerBody, DrawerCloseButton, DrawerContent, DrawerHeader, DrawerOverlay, useBreakpointValue, useTheme, useColorModeValue } from "@chakra-ui/react"
|
||||
import { motion } from "framer-motion"
|
||||
import { Fragment, useMemo } from "react"
|
||||
import {Category, IconEntity} from "src/types"
|
||||
import {createLucideIcon} from "lucide-react";
|
||||
|
||||
const ListWrapper = ({ children, ...restProps }: BoxProps) => {
|
||||
return (
|
||||
<Box
|
||||
w="full"
|
||||
h="full"
|
||||
overflowY="auto"
|
||||
sx={{
|
||||
direction: 'rtl',
|
||||
'&::-webkit-scrollbar' : {
|
||||
width: '4px',
|
||||
},
|
||||
'&::-webkit-scrollbar-track' : {
|
||||
background: 'transparent',
|
||||
},
|
||||
'&::-webkit-scrollbar-thumb' : {
|
||||
bgColor: useColorModeValue('var(--chakra-colors-gray-300)', 'var(--chakra-colors-whiteAlpha-300)'),
|
||||
borderRadius: 0,
|
||||
},
|
||||
}}
|
||||
{...restProps}
|
||||
>
|
||||
<Box
|
||||
whiteSpace="nowrap"
|
||||
height="100%"
|
||||
display="flex"
|
||||
flexDirection="column"
|
||||
paddingBottom={8}
|
||||
paddingX={2}
|
||||
paddingY={1}
|
||||
paddingRight={4}
|
||||
sx={{
|
||||
direction: 'ltr'
|
||||
}}
|
||||
>
|
||||
{ children }
|
||||
</Box>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
|
||||
const CATEGORY_TOP_OFFSET = 100
|
||||
|
||||
interface IconCategoryDrawerProps {
|
||||
data: IconEntity[];
|
||||
categories: Category[]
|
||||
setCategoryView: (view: boolean) => void
|
||||
open: boolean
|
||||
onClose: () => void
|
||||
}
|
||||
|
||||
const IconCategoryDrawer = ({ open, onClose, categories, data, setCategoryView }: IconCategoryDrawerProps) => {
|
||||
const theme = useTheme()
|
||||
|
||||
const useCustomDrawer = useBreakpointValue({ base: false, md: true });
|
||||
|
||||
const sidebarVariants = {
|
||||
closed: {
|
||||
width: 0,
|
||||
},
|
||||
open: {
|
||||
width: theme.sizes['xs']
|
||||
}
|
||||
}
|
||||
|
||||
const categoryList = useMemo(() => {
|
||||
return (
|
||||
<>
|
||||
{[{ name: 'all', title: 'All', icon: null, iconCount: data.length }, ...categories].map(({ title, name, icon, iconCount }) => {
|
||||
// Show category icon?
|
||||
const iconData = data.find(({ name: iconName }) => iconName === icon)
|
||||
const Icon = iconData ? createLucideIcon(iconData.name, iconData.iconNode) : null
|
||||
|
||||
return (
|
||||
<Fragment key={name}>
|
||||
<Button
|
||||
as="a"
|
||||
colorScheme='gray'
|
||||
variant='ghost'
|
||||
width="100%"
|
||||
justifyContent="space-between"
|
||||
leftIcon={Icon ? <Icon /> : null}
|
||||
onClick={(event) => {
|
||||
event.stopPropagation()
|
||||
|
||||
if (!useCustomDrawer) {
|
||||
onClose()
|
||||
|
||||
const [routePath] = window.location.href.split('#')
|
||||
|
||||
setTimeout(() =>{
|
||||
window.location.href = `${routePath}#${name}`
|
||||
},150)
|
||||
}
|
||||
|
||||
setCategoryView(name !== 'all')
|
||||
}}
|
||||
href={useCustomDrawer ? `#${name}` : undefined}
|
||||
marginBottom={1}
|
||||
sx={{
|
||||
opacity: .7,
|
||||
flexShrink: 0,
|
||||
'&.active': {
|
||||
color: 'brand.500'
|
||||
}
|
||||
}}
|
||||
>
|
||||
<Box width='100%'>{title}</Box>
|
||||
<Box sx={{opacity: .5}}><small>{iconCount}</small></Box>
|
||||
</Button>
|
||||
{name === 'all' && (
|
||||
<Divider marginY={2} />
|
||||
)}
|
||||
</Fragment>
|
||||
)
|
||||
})}
|
||||
<Box h={8} flexShrink={0}></Box>
|
||||
</>
|
||||
)
|
||||
}, [categories, useCustomDrawer])
|
||||
|
||||
if(useCustomDrawer) {
|
||||
return (
|
||||
<motion.div
|
||||
variants={sidebarVariants}
|
||||
animate={(open ?? useCustomDrawer) ? 'open' : 'closed'}
|
||||
initial={false}
|
||||
style={{
|
||||
height: `calc(100vh - ${CATEGORY_TOP_OFFSET}px)`,
|
||||
position: 'sticky',
|
||||
top: '100px'
|
||||
}}
|
||||
>
|
||||
<ListWrapper>
|
||||
{categoryList}
|
||||
</ListWrapper>
|
||||
</motion.div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<Drawer
|
||||
placement="left"
|
||||
onClose={onClose}
|
||||
isOpen={open}
|
||||
size="sm"
|
||||
blockScrollOnMount={false}
|
||||
>
|
||||
<DrawerOverlay />
|
||||
<DrawerContent>
|
||||
<DrawerCloseButton marginTop={3.5} marginRight={3} />
|
||||
<DrawerHeader />
|
||||
<DrawerBody>
|
||||
<ListWrapper>
|
||||
{categoryList}
|
||||
</ListWrapper>
|
||||
</DrawerBody>
|
||||
</DrawerContent>
|
||||
</Drawer>
|
||||
)
|
||||
}
|
||||
|
||||
export default IconCategoryDrawer
|
||||
78
site/src/components/IconCategoryList.tsx
Normal file
78
site/src/components/IconCategoryList.tsx
Normal file
@@ -0,0 +1,78 @@
|
||||
import { Box, BoxProps, Stack, Text, useColorModeValue } from '@chakra-ui/react';
|
||||
import { useMemo } from 'react';
|
||||
import { Category, IconEntity } from 'src/types';
|
||||
import theme from '../lib/theme';
|
||||
import IconList from './IconList';
|
||||
|
||||
interface IconCategoryProps {
|
||||
icons: IconEntity[]
|
||||
data: IconEntity[]
|
||||
categories: Category[]
|
||||
categoryProps?: {
|
||||
innerProps: BoxProps,
|
||||
activeCategory: string | null
|
||||
}
|
||||
}
|
||||
|
||||
const IconCategory = ({
|
||||
icons,
|
||||
data,
|
||||
categories = [],
|
||||
categoryProps = {
|
||||
innerProps: {},
|
||||
activeCategory: null,
|
||||
},
|
||||
}: IconCategoryProps) => {
|
||||
const { innerProps, activeCategory, ...outerProps } = categoryProps;
|
||||
const activeBackground = useColorModeValue(theme.colors.gray, theme.colors.gray[700]);
|
||||
|
||||
const iconCategories = useMemo(
|
||||
() =>
|
||||
categories.reduce((categoryMap, { name, title }) => {
|
||||
const categoryIcons = data.filter(({categories}) => categories.includes(name))
|
||||
|
||||
const isSearching = icons.length !== data.length;
|
||||
const searchResults = isSearching
|
||||
? categoryIcons.filter(icon => icons.some((item) => item?.name === icon?.name))
|
||||
: categoryIcons;
|
||||
|
||||
categoryMap.push({
|
||||
title,
|
||||
name,
|
||||
icons: searchResults,
|
||||
isActive: name === activeCategory,
|
||||
});
|
||||
|
||||
return categoryMap;
|
||||
}, []),
|
||||
[icons, categories, activeCategory],
|
||||
);
|
||||
|
||||
return (
|
||||
<Stack spacing={4}>
|
||||
{iconCategories
|
||||
.filter(({ icons }) => icons.length)
|
||||
.map(({ name, title, icons, isActive }) => (
|
||||
<Box
|
||||
key={name}
|
||||
backgroundColor={isActive ? activeBackground : 'transparent'}
|
||||
borderRadius={8}
|
||||
{...outerProps}
|
||||
>
|
||||
<Box {...innerProps || {}}>
|
||||
<Text fontSize="xl" marginBottom={3} id={name} sx={{
|
||||
'&:target': {
|
||||
scrollMarginTop: 20
|
||||
}
|
||||
}}>
|
||||
{title}
|
||||
</Text>
|
||||
<IconList icons={icons} category={name}/>
|
||||
</Box>
|
||||
</Box>
|
||||
))}
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
||||
export default IconCategory;
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useContext, useState } from 'react';
|
||||
import { IconStyleContext } from './CustomizeIconContext';
|
||||
import { useState } from 'react';
|
||||
import { useCustomizeIconContext } from './CustomizeIconContext';
|
||||
import { Edit } from 'lucide-react';
|
||||
import {
|
||||
Button,
|
||||
@@ -8,30 +8,56 @@ import {
|
||||
DrawerCloseButton,
|
||||
DrawerContent,
|
||||
DrawerHeader,
|
||||
DrawerOverlay,
|
||||
FormControl,
|
||||
FormLabel,
|
||||
Grid,
|
||||
Hide,
|
||||
IconButton,
|
||||
Show,
|
||||
Slider,
|
||||
SliderFilledTrack,
|
||||
SliderThumb,
|
||||
SliderTrack,
|
||||
Flex,
|
||||
Text,
|
||||
ButtonProps,
|
||||
} from '@chakra-ui/react';
|
||||
import ColorPicker from './ColorPicker';
|
||||
|
||||
export function IconCustomizerDrawer() {
|
||||
export const IconCustomizerDrawer = (props: ButtonProps) => {
|
||||
const [showCustomize, setShowCustomize] = useState(false);
|
||||
const { color, setColor, size, setSize, strokeWidth, setStroke, resetStyle } = useContext(IconStyleContext);
|
||||
const {
|
||||
color,
|
||||
setColor,
|
||||
size,
|
||||
setSize,
|
||||
strokeWidth,
|
||||
setStroke,
|
||||
resetStyle,
|
||||
} = useCustomizeIconContext();
|
||||
|
||||
return (
|
||||
<>
|
||||
<Button as="a" leftIcon={<Edit />} size="lg" onClick={() => setShowCustomize(true)}>
|
||||
Customize
|
||||
</Button>
|
||||
<Drawer isOpen={showCustomize} placement="right" onClose={() => setShowCustomize(false)}>
|
||||
<DrawerOverlay />
|
||||
<Hide below='md'>
|
||||
<Button
|
||||
as="a"
|
||||
leftIcon={<Edit />}
|
||||
size="lg"
|
||||
onClick={() => setShowCustomize(true)}
|
||||
{...props}
|
||||
>Customize</Button>
|
||||
</Hide>
|
||||
<Show below='md'>
|
||||
<IconButton
|
||||
aria-label='Customize'
|
||||
variant="solid"
|
||||
color="current"
|
||||
onClick={() => setShowCustomize(true)}
|
||||
icon={<Edit />}
|
||||
{...props}
|
||||
></IconButton>
|
||||
</Show>
|
||||
<Drawer isOpen={showCustomize} placement="right" onClose={() => setShowCustomize(false)} size="md">
|
||||
<DrawerContent>
|
||||
<DrawerCloseButton />
|
||||
<DrawerHeader>Customize Icons</DrawerHeader>
|
||||
@@ -42,7 +68,7 @@ export function IconCustomizerDrawer() {
|
||||
<ColorPicker
|
||||
color={color}
|
||||
value={color}
|
||||
onChangeComplete={(col) => setColor(col.hex)}
|
||||
onChangeComplete={col => setColor(col.hex)}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormControl>
|
||||
@@ -93,4 +119,4 @@ export function IconCustomizerDrawer() {
|
||||
</Drawer>
|
||||
</>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -2,11 +2,11 @@ import { Box, Text, IconButton, useColorMode, Flex, Slide, ButtonGroup, Button,
|
||||
import theme from "../lib/theme";
|
||||
import download from 'downloadjs';
|
||||
import { X as Close } from 'lucide-react';
|
||||
import {useContext, useEffect, useRef} from "react";
|
||||
import {IconStyleContext} from "./CustomizeIconContext";
|
||||
import {IconWrapper} from "./IconWrapper";
|
||||
import { useEffect, useRef } from "react";
|
||||
import {useCustomizeIconContext} from "./CustomizeIconContext";
|
||||
import ModifiedTooltip from "./ModifiedTooltip";
|
||||
import { IconEntity } from "../types";
|
||||
import { createLucideIcon } from 'lucide-react';
|
||||
|
||||
type IconDownload = {
|
||||
src: string;
|
||||
@@ -14,15 +14,17 @@ type IconDownload = {
|
||||
};
|
||||
|
||||
interface IconDetailOverlayProps {
|
||||
open: boolean
|
||||
close: () => void
|
||||
open?: boolean
|
||||
close?: () => void
|
||||
icon?: IconEntity
|
||||
}
|
||||
|
||||
const IconDetailOverlay = ({ open = true, close, icon }: IconDetailOverlayProps) => {
|
||||
const toast = useToast();
|
||||
const { colorMode } = useColorMode();
|
||||
const {color, strokeWidth, size} = useContext(IconStyleContext);
|
||||
|
||||
const { tags = [], name, iconNode } = icon ?? {};
|
||||
const {color, strokeWidth, size} = useCustomizeIconContext();
|
||||
const iconRef = useRef<SVGSVGElement>(null);
|
||||
const [isMobile] = useMediaQuery("(max-width: 560px)")
|
||||
const { isOpen, onOpen, onClose } = useDisclosure()
|
||||
@@ -37,8 +39,6 @@ const IconDetailOverlay = ({ open = true, close, icon }: IconDetailOverlayProps)
|
||||
return null
|
||||
}
|
||||
|
||||
const { tags = [], name = '' } = icon;
|
||||
|
||||
const handleClose = () => {
|
||||
onClose();
|
||||
close();
|
||||
@@ -54,10 +54,12 @@ const IconDetailOverlay = ({ open = true, close, icon }: IconDetailOverlayProps)
|
||||
color: color,
|
||||
};
|
||||
|
||||
const downloadIcon = ({src, name = ''} : IconDownload) => download(src, `${name}.svg`, 'image/svg+xml');
|
||||
const Icon = createLucideIcon(name, iconNode)
|
||||
|
||||
const copyIcon = async ({src, name} : IconDownload) => {
|
||||
const trimmedSrc = src.replace(/(\r\n|\n|\r|\s\s)/gm, "")
|
||||
const downloadIcon = ({name = ''} : IconDownload) => download(iconRef.current.outerHTML, `${name}.svg`, 'image/svg+xml');
|
||||
|
||||
const copyIcon = async ({name} : IconDownload) => {
|
||||
const trimmedSrc = iconRef.current.outerHTML.replace(/(\r\n|\n|\r|\s\s)/gm, "")
|
||||
|
||||
await navigator.clipboard.writeText(trimmedSrc)
|
||||
|
||||
@@ -70,14 +72,14 @@ const IconDetailOverlay = ({ open = true, close, icon }: IconDetailOverlayProps)
|
||||
});
|
||||
}
|
||||
|
||||
const downloadPNG = ({src, name}: IconDownload) => {
|
||||
const downloadPNG = ({name}: IconDownload) => {
|
||||
const canvas = document.createElement('canvas');
|
||||
canvas.width = size;
|
||||
canvas.height = size;
|
||||
const ctx = canvas.getContext("2d");
|
||||
|
||||
const image = new Image();
|
||||
image.src = `data:image/svg+xml;base64,${btoa(src)}`;
|
||||
image.src = `data:image/svg+xml;base64,${btoa(iconRef.current.outerHTML)}`;
|
||||
image.onload = function() {
|
||||
ctx.drawImage(image, 0, 0);
|
||||
|
||||
@@ -98,7 +100,7 @@ const IconDetailOverlay = ({ open = true, close, icon }: IconDetailOverlayProps)
|
||||
height={0}
|
||||
key={name}
|
||||
>
|
||||
<Slide direction="bottom" in={isOpen} style={{ zIndex: 10 }}>
|
||||
<Slide direction="bottom" in={isOpen} style={{ zIndex: 10, pointerEvents: 'none' }}>
|
||||
<Flex
|
||||
alignItems="center"
|
||||
justifyContent="space-between"
|
||||
@@ -120,6 +122,7 @@ const IconDetailOverlay = ({ open = true, close, icon }: IconDetailOverlayProps)
|
||||
? theme.colors.white
|
||||
: theme.colors.gray[700]
|
||||
}
|
||||
style={{ pointerEvents: 'initial' }}
|
||||
padding={8}
|
||||
>
|
||||
<IconButton
|
||||
@@ -151,14 +154,13 @@ const IconDetailOverlay = ({ open = true, close, icon }: IconDetailOverlayProps)
|
||||
style={iconStyling}
|
||||
className="icon-large"
|
||||
>
|
||||
<IconWrapper
|
||||
src={icon.src}
|
||||
<Icon
|
||||
stroke={color}
|
||||
strokeWidth={strokeWidth}
|
||||
height={size}
|
||||
width={size}
|
||||
size={size}
|
||||
ref={iconRef}
|
||||
/>
|
||||
|
||||
</div>
|
||||
|
||||
<svg className="icon-grid" width="24" height="24" viewBox={`0 0 ${size} ${size}`} fill="none" stroke={colorMode == "light" ? '#E2E8F0' : theme.colors.gray[600]} strokeWidth="0.1" xmlns="http://www.w3.org/2000/svg">
|
||||
|
||||
@@ -1,35 +1,19 @@
|
||||
import { Grid } from '@chakra-ui/react';
|
||||
import Link from 'next/link';
|
||||
import { useRouter } from 'next/router';
|
||||
import { memo } from 'react';
|
||||
import IconListItem from './IconListItem';
|
||||
import { IconEntity } from '../types';
|
||||
|
||||
interface IconListProps {
|
||||
icons: IconEntity[];
|
||||
category?: string
|
||||
}
|
||||
|
||||
const IconList = memo(({ icons }: IconListProps) => {
|
||||
const router = useRouter();
|
||||
|
||||
const IconList = memo(({ icons, category = '' }: IconListProps) => {
|
||||
return (
|
||||
<Grid templateColumns={`repeat(auto-fill, minmax(150px, 1fr))`} gap={5} marginBottom="320px">
|
||||
<Grid templateColumns={`repeat(auto-fill, minmax(80px, 1fr))`} gap={5} marginBottom={6} justifyItems={'center'}>
|
||||
{icons.map(icon => {
|
||||
return (
|
||||
<Link
|
||||
key={icon.name}
|
||||
scroll={false}
|
||||
shallow={true}
|
||||
href={{
|
||||
pathname: '/icon/[iconName]',
|
||||
query: {
|
||||
...router.query,
|
||||
iconName: icon.name,
|
||||
},
|
||||
}}
|
||||
>
|
||||
<IconListItem {...icon} />
|
||||
</Link>
|
||||
<IconListItem {...icon} key={`${category}-${icon.name}`}/>
|
||||
);
|
||||
})}
|
||||
</Grid>
|
||||
|
||||
@@ -1,21 +1,25 @@
|
||||
import { Button, ButtonProps, Flex, Text, useToast } from '@chakra-ui/react';
|
||||
import { Button, ButtonProps, Tooltip, useToast } from '@chakra-ui/react';
|
||||
import download from 'downloadjs';
|
||||
import { memo } from 'react';
|
||||
import { memo, useCallback } from 'react';
|
||||
import { createLucideIcon, IconNode } from 'lucide-react';
|
||||
import { useCustomizeIconContext } from './CustomizeIconContext';
|
||||
import { IconWrapper } from './IconWrapper';
|
||||
import { useRouter } from 'next/router';
|
||||
|
||||
interface IconListItemProps {
|
||||
interface IconListItemProps extends ButtonProps {
|
||||
name: string;
|
||||
onClick?: ButtonProps['onClick']
|
||||
src: string;
|
||||
iconNode: IconNode;
|
||||
}
|
||||
|
||||
const IconListItem = ({ name, onClick, src: svg }: IconListItemProps) => {
|
||||
const IconListItem = ({ name, iconNode }: IconListItemProps) => {
|
||||
const router = useRouter()
|
||||
const toast = useToast();
|
||||
const { color, size, strokeWidth, iconsRef } = useCustomizeIconContext();
|
||||
|
||||
const handleClick:ButtonProps['onClick'] = async (event) => {
|
||||
const src = (iconsRef.current[name].outerHTML ?? svg).replace(/(\r\n|\n|\r|(>\s\s<))/gm, "")
|
||||
const Icon = createLucideIcon(name, iconNode)
|
||||
|
||||
const handleClick:ButtonProps['onClick'] = useCallback(async (event) => {
|
||||
const src = (iconsRef.current[name].outerHTML).replace(/(\r\n|\n|\r|(>\s\s<))/gm, "")
|
||||
|
||||
if (event.shiftKey) {
|
||||
await navigator.clipboard.writeText(src)
|
||||
|
||||
@@ -25,46 +29,53 @@ const IconListItem = ({ name, onClick, src: svg }: IconListItemProps) => {
|
||||
status: 'success',
|
||||
duration: 1500,
|
||||
});
|
||||
return
|
||||
}
|
||||
if (event.altKey) {
|
||||
download(src, `${name}.svg`, 'image/svg+xml');
|
||||
return
|
||||
}
|
||||
if (onClick) {
|
||||
onClick(event);
|
||||
}
|
||||
}
|
||||
|
||||
router.push({
|
||||
pathname: `/icon/${name}`,
|
||||
query: (
|
||||
router.query?.search != null
|
||||
? { search: router.query.search }
|
||||
: {}
|
||||
),
|
||||
},
|
||||
undefined,
|
||||
{
|
||||
shallow: true,
|
||||
scroll: false
|
||||
})
|
||||
}, [iconsRef, name, router, toast])
|
||||
|
||||
|
||||
return (
|
||||
<Button
|
||||
variant="ghost"
|
||||
borderWidth="1px"
|
||||
rounded="lg"
|
||||
padding={2}
|
||||
height={32}
|
||||
position="relative"
|
||||
whiteSpace="normal"
|
||||
onClick={handleClick}
|
||||
key={name}
|
||||
alignItems="center"
|
||||
>
|
||||
<Flex direction="column" align="center" justify="stretch" width="100%" gap={4}>
|
||||
<Flex flex={2} flexBasis="100%" minHeight={10} align="flex-end">
|
||||
<IconWrapper
|
||||
src={svg}
|
||||
stroke={color}
|
||||
strokeWidth={strokeWidth}
|
||||
height={size}
|
||||
width={size}
|
||||
ref={iconEl => (iconsRef.current[name] = iconEl)}
|
||||
/>
|
||||
</Flex>
|
||||
<Flex flex={1} minHeight={10} align="center">
|
||||
<Text wordBreak="break-word" maxWidth="100%">
|
||||
{name}
|
||||
</Text>
|
||||
</Flex>
|
||||
</Flex>
|
||||
</Button>
|
||||
<Tooltip label={name}>
|
||||
<Button
|
||||
as="a"
|
||||
variant="ghost"
|
||||
borderWidth="1px"
|
||||
rounded="lg"
|
||||
padding={2}
|
||||
height={20}
|
||||
width={20}
|
||||
position="relative"
|
||||
whiteSpace="normal"
|
||||
alignItems="center"
|
||||
sx={{cursor: 'pointer'}}
|
||||
onClick={handleClick}
|
||||
>
|
||||
<Icon
|
||||
ref={iconEl => (iconsRef.current[name] = iconEl)}
|
||||
size={size}
|
||||
stroke={color}
|
||||
strokeWidth={strokeWidth}
|
||||
/>
|
||||
</Button>
|
||||
</Tooltip>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -1,16 +1,38 @@
|
||||
import { Box, Text } from '@chakra-ui/react';
|
||||
import React, { useState } from 'react';
|
||||
import { Box, Text, IconButton, HStack } from '@chakra-ui/react';
|
||||
import React, { memo, useEffect, useState } from 'react';
|
||||
import useSearch from '../lib/useSearch';
|
||||
import IconList from './IconList';
|
||||
import { SearchInput } from './SearchInput';
|
||||
import { IconEntity } from '../types';
|
||||
import { Category, IconEntity } from '../types';
|
||||
|
||||
import { SidebarClose, SidebarOpen } from 'lucide-react';
|
||||
|
||||
import IconCategoryList from './IconCategoryList';
|
||||
import { IconCustomizerDrawer } from './IconCustomizerDrawer';
|
||||
import IconCategoryDrawer from './IconCategoryDrawer';
|
||||
|
||||
interface IconOverviewProps {
|
||||
data: IconEntity[];
|
||||
categories: Category[]
|
||||
}
|
||||
|
||||
const IconOverview = ({ data }: IconOverviewProps) => {
|
||||
const IconOverview = ({ data, categories }: IconOverviewProps): JSX.Element => {
|
||||
const [query, setQuery] = useState('');
|
||||
const [sidebarOpen, setSidebarOpen] = useState<boolean | undefined>();
|
||||
const [categoryView, setCategoryView] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
if(
|
||||
typeof window !== 'undefined' &&
|
||||
window.location.href.split('#').length === 2
|
||||
) {
|
||||
const [,hash] = window.location.href.split('#')
|
||||
|
||||
setCategoryView(categories.some(({ name }) => name === hash))
|
||||
}
|
||||
}, [])
|
||||
|
||||
const SidebarIcon = sidebarOpen ? SidebarOpen : SidebarClose;
|
||||
|
||||
const searchResults = useSearch(query, data, [
|
||||
{ name: 'name', weight: 2 },
|
||||
@@ -18,20 +40,49 @@ const IconOverview = ({ data }: IconOverviewProps) => {
|
||||
]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<SearchInput onChange={setQuery} count={data.length} />
|
||||
<Box>
|
||||
<HStack position="sticky" top={0} zIndex={1} gap={2} padding={5}>
|
||||
<IconButton
|
||||
aria-label="Close overlay"
|
||||
variant="solid"
|
||||
color="current"
|
||||
onClick={() => setSidebarOpen(currentView => {
|
||||
if(currentView == null) {
|
||||
return false
|
||||
}
|
||||
|
||||
<Box marginTop={5}>
|
||||
{searchResults.length > 0 ? (
|
||||
<IconList icons={searchResults} />
|
||||
) : (
|
||||
<Text fontSize="2xl" fontWeight="bold" textAlign="center" wordBreak="break-word">
|
||||
No results found for "{query}"
|
||||
</Text>
|
||||
)}
|
||||
</Box>
|
||||
</>
|
||||
return !currentView
|
||||
})}
|
||||
icon={<SidebarIcon />}
|
||||
/>
|
||||
<SearchInput onChange={setQuery} count={data.length} />
|
||||
<IconCustomizerDrawer size="md" paddingX={6} />
|
||||
</HStack>
|
||||
|
||||
<HStack marginBottom="320px" padding={5} alignItems="flex-start">
|
||||
<IconCategoryDrawer
|
||||
open={sidebarOpen}
|
||||
onClose={() => setSidebarOpen(false)}
|
||||
categories={categories}
|
||||
data={data}
|
||||
setCategoryView={setCategoryView}
|
||||
/>
|
||||
<Box flex={1} paddingTop={1}>
|
||||
{searchResults.length > 0 ? (
|
||||
categoryView ? (
|
||||
<IconCategoryList icons={searchResults} data={data} categories={categories} />
|
||||
) : (
|
||||
<IconList icons={searchResults} />
|
||||
)
|
||||
) : (
|
||||
<Text fontSize="2xl" fontWeight="bold" textAlign="center" wordBreak="break-word">
|
||||
No results found for "{query}"
|
||||
</Text>
|
||||
)}
|
||||
</Box>
|
||||
</HStack>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export default IconOverview;
|
||||
export default memo(IconOverview)
|
||||
|
||||
66
site/src/components/IconReorder.tsx
Normal file
66
site/src/components/IconReorder.tsx
Normal file
@@ -0,0 +1,66 @@
|
||||
import { Box, BoxProps } from '@chakra-ui/react';
|
||||
import { AnimatePresence, Reorder } from 'framer-motion';
|
||||
import { memo, RefObject } from 'react';
|
||||
import { IconEntity } from '../types';
|
||||
import IconReorderItem from './IconReorderItem';
|
||||
|
||||
interface IconListProps {
|
||||
icons: IconEntity[];
|
||||
setIcons: (icons) => void;
|
||||
dropZones?: RefObject<[string, HTMLDivElement][]>;
|
||||
onDrop?: (name: string, category: string) => void;
|
||||
dragging: boolean;
|
||||
setDragging: (dragging) => void;
|
||||
sx?: BoxProps['sx'];
|
||||
}
|
||||
|
||||
const IconReorder = ({
|
||||
icons,
|
||||
setIcons,
|
||||
dropZones,
|
||||
onDrop,
|
||||
dragging,
|
||||
setDragging,
|
||||
sx,
|
||||
}: IconListProps) => {
|
||||
return (
|
||||
<Box
|
||||
as={Reorder.Group}
|
||||
display="flex"
|
||||
flexWrap="wrap"
|
||||
gap={5}
|
||||
marginBottom={6}
|
||||
onReorder={setIcons}
|
||||
layoutScroll
|
||||
values={icons.map(({ name }) => name)}
|
||||
sx={{
|
||||
'.dragging': {
|
||||
position: 'absolute',
|
||||
},
|
||||
...sx,
|
||||
}}
|
||||
>
|
||||
<AnimatePresence>
|
||||
{icons.map(icon => {
|
||||
return (
|
||||
<IconReorderItem
|
||||
icon={icon}
|
||||
key={icon.name}
|
||||
dropZones={dropZones}
|
||||
onDrop={onDrop}
|
||||
dragging={dragging}
|
||||
setDragging={setDragging}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</AnimatePresence>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export default memo(IconReorder, (prevProps, nextProps) => {
|
||||
const prevIconsNames = prevProps.icons.map(({ name }) => name);
|
||||
const nextIconsNames = nextProps.icons.map(({ name }) => name);
|
||||
|
||||
return JSON.stringify(prevIconsNames) === JSON.stringify(nextIconsNames);
|
||||
});
|
||||
78
site/src/components/IconReorderItem.tsx
Normal file
78
site/src/components/IconReorderItem.tsx
Normal file
@@ -0,0 +1,78 @@
|
||||
import { useState } from 'react';
|
||||
import { useMotionValue, Reorder } from 'framer-motion';
|
||||
import useRaisedShadow from '../hooks/useRaisedShadow';
|
||||
import IconListItem from './IconListItem';
|
||||
import { IconEntity } from '../types';
|
||||
import { RefObject } from 'react';
|
||||
|
||||
interface Props {
|
||||
icon: IconEntity
|
||||
dropZones?: RefObject<[string, HTMLDivElement][]>
|
||||
onDrop?: (name:string, category: string) => void
|
||||
dragging: boolean;
|
||||
setDragging: (dragging) => void;
|
||||
}
|
||||
|
||||
const IconReorderItem = ({ icon, dropZones, onDrop, setDragging }: Props): JSX.Element => {
|
||||
const y = useMotionValue(0);
|
||||
const boxShadow = useRaisedShadow(y);
|
||||
const [dragItem, setDragItem] = useState(false);
|
||||
|
||||
const onDragEnd = (event) => {
|
||||
setDragItem(false)
|
||||
setDragging(false);
|
||||
|
||||
const dropZone = dropZones.current?.find(([, el]) => {
|
||||
if (!Array.isArray(event?.path)) {
|
||||
return false
|
||||
}
|
||||
|
||||
return Array.from(event.path).includes(el)
|
||||
})
|
||||
|
||||
if (dropZone?.[0] && onDrop) {
|
||||
const category = dropZone?.[0]
|
||||
console.log(icon.name, category);
|
||||
|
||||
onDrop(icon.name, category)
|
||||
}
|
||||
};
|
||||
|
||||
const onDrag = () => {
|
||||
setDragItem(true)
|
||||
setDragging(true);
|
||||
};
|
||||
|
||||
return (
|
||||
<Reorder.Item
|
||||
value={icon.name}
|
||||
style={{
|
||||
boxShadow, y,
|
||||
listStyle: 'none',
|
||||
pointerEvents: dragItem ? 'none' : 'all',
|
||||
cursor: dragItem ? 'grab': 'pointer',
|
||||
}}
|
||||
drag
|
||||
dragConstraints={{ top: 0, left: 0, right: 0, bottom: 0 }}
|
||||
dragElastic={1}
|
||||
onDragEnd={onDragEnd}
|
||||
onDrag={onDrag}
|
||||
initial={{
|
||||
opacity: 0,
|
||||
scale: 0,
|
||||
}}
|
||||
animate={{
|
||||
opacity: 1,
|
||||
scale: 1,
|
||||
transition: {
|
||||
duration: 0.2,
|
||||
delay: 0.02,
|
||||
},
|
||||
}}
|
||||
>
|
||||
<IconListItem {...icon} />
|
||||
</Reorder.Item>
|
||||
);
|
||||
};
|
||||
|
||||
export default IconReorderItem;
|
||||
@@ -15,6 +15,7 @@ import NextLink from 'next/link';
|
||||
import { Moon, Sun, Menu, X } from 'lucide-react';
|
||||
import { useMobileNavigationContext, useMobileNavigationValue } from './MobileNavigationProvider';
|
||||
import Logo from './Logo';
|
||||
import menuItems from '../static/menuItems';
|
||||
|
||||
interface LayoutProps extends BoxProps {
|
||||
aside?: BoxProps['children'];
|
||||
@@ -37,12 +38,14 @@ const Layout = ({ aside, children }: LayoutProps) => {
|
||||
};
|
||||
|
||||
function setQuery(query) {
|
||||
router.push({
|
||||
router.push(
|
||||
{
|
||||
pathname: '/',
|
||||
query: { query: query },
|
||||
},
|
||||
undefined,
|
||||
{ shallow: true })
|
||||
},
|
||||
undefined,
|
||||
{ shallow: true },
|
||||
);
|
||||
}
|
||||
|
||||
useKeyBindings({
|
||||
@@ -56,7 +59,7 @@ const Layout = ({ aside, children }: LayoutProps) => {
|
||||
|
||||
return (
|
||||
<Box h="100vh">
|
||||
<Flex mb={16} w="full">
|
||||
<Flex w="full">
|
||||
<Flex
|
||||
alignItems="center"
|
||||
justifyContent="space-between"
|
||||
@@ -72,29 +75,28 @@ const Layout = ({ aside, children }: LayoutProps) => {
|
||||
<Flex justifyContent="center" alignItems="center">
|
||||
{showBaseNavigation ? (
|
||||
<>
|
||||
<NextLink href="/docs" passHref>
|
||||
<Link marginRight={12} fontSize="xl">
|
||||
Documentation
|
||||
</Link>
|
||||
</NextLink>
|
||||
<NextLink href="/packages" passHref>
|
||||
<Link marginRight={12} fontSize="xl">
|
||||
Packages
|
||||
</Link>
|
||||
</NextLink>
|
||||
<NextLink href="/license" passHref>
|
||||
<Link marginRight={12} fontSize="xl">
|
||||
License
|
||||
</Link>
|
||||
</NextLink>
|
||||
<Link
|
||||
href="https://github.com/lucide-icons/lucide"
|
||||
isExternal
|
||||
marginRight={6}
|
||||
fontSize="xl"
|
||||
>
|
||||
Github
|
||||
</Link>
|
||||
{menuItems.map(menuItem => {
|
||||
if (menuItem.isExternal) {
|
||||
return (
|
||||
<Link
|
||||
href={menuItem.href}
|
||||
isExternal
|
||||
marginRight={6}
|
||||
fontSize="lg"
|
||||
key={menuItem.name}
|
||||
>
|
||||
{menuItem.name}
|
||||
</Link>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<NextLink href={menuItem.href} passHref key={menuItem.name}>
|
||||
<Link marginRight={8} fontSize="lg">
|
||||
{menuItem.name}
|
||||
</Link>
|
||||
</NextLink>
|
||||
);
|
||||
})}
|
||||
</>
|
||||
) : null}
|
||||
<IconButton
|
||||
@@ -115,8 +117,12 @@ const Layout = ({ aside, children }: LayoutProps) => {
|
||||
</Flex>
|
||||
</Flex>
|
||||
<Flex>
|
||||
{aside ? <Box as="aside" marginRight={{ base: 0, lg: -240, }}>{aside}</Box> : null}
|
||||
<Flex margin="0 auto" direction="column" maxW="1250px" px={5} width="100%">
|
||||
{aside ? (
|
||||
<Box as="aside" marginRight={{ base: 0, lg: -240 }}>
|
||||
{aside}
|
||||
</Box>
|
||||
) : null}
|
||||
<Flex margin="0 auto" direction="column" width="100%">
|
||||
{children}
|
||||
<Divider mb={6} mt={12} />
|
||||
<p style={{ alignSelf: 'center' }}>
|
||||
|
||||
@@ -12,6 +12,7 @@ import {
|
||||
import NextLink from 'next/link';
|
||||
import { useRouter } from 'next/router';
|
||||
import { ReactNode, useEffect } from 'react';
|
||||
import menuItems from '../static/menuItems';
|
||||
import Logo from './Logo';
|
||||
import { useMobileNavigationContext } from './MobileNavigationProvider';
|
||||
|
||||
@@ -35,31 +36,30 @@ const MobileMenu = ({ children }: { children?: ReactNode }): JSX.Element => {
|
||||
</DrawerHeader>
|
||||
<DrawerBody>
|
||||
<Box mb={4}>
|
||||
<NextLink href="/docs" passHref>
|
||||
<Link fontSize="lg" fontWeight="bold" display="block" mb={2}>
|
||||
Documentation
|
||||
</Link>
|
||||
</NextLink>
|
||||
<NextLink href="/packages" passHref>
|
||||
<Link marginRight={12} fontSize="lg" fontWeight="bold" display="block" mb={2}>
|
||||
Packages
|
||||
</Link>
|
||||
</NextLink>
|
||||
<NextLink href="/license" passHref>
|
||||
<Link marginRight={12} fontSize="xl">
|
||||
License
|
||||
</Link>
|
||||
</NextLink>
|
||||
<Link
|
||||
href="https://github.com/lucide-icons/lucide"
|
||||
isExternal
|
||||
fontSize="lg"
|
||||
fontWeight="bold"
|
||||
display="block"
|
||||
mb={2}
|
||||
>
|
||||
Github
|
||||
</Link>
|
||||
{menuItems.map(menuItem => {
|
||||
if (menuItem.isExternal) {
|
||||
return (
|
||||
<Link
|
||||
href="https://github.com/lucide-icons/lucide"
|
||||
isExternal
|
||||
fontSize="lg"
|
||||
fontWeight="bold"
|
||||
display="block"
|
||||
mb={2}
|
||||
key={menuItem.name}
|
||||
>
|
||||
{menuItem.name}
|
||||
</Link>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<NextLink href={menuItem.href} passHref>
|
||||
<Link fontSize="lg" fontWeight="bold" display="block" mb={2}>
|
||||
{menuItem.name}
|
||||
</Link>
|
||||
</NextLink>
|
||||
);
|
||||
})}
|
||||
</Box>
|
||||
<Divider mt={2} />
|
||||
{children}
|
||||
|
||||
@@ -4,7 +4,7 @@ import {
|
||||
InputGroup,
|
||||
InputLeftElement,
|
||||
useColorMode,
|
||||
useUpdateEffect
|
||||
useUpdateEffect,
|
||||
} from '@chakra-ui/react';
|
||||
import { Search as SearchIcon } from 'lucide-react';
|
||||
import React, { useEffect, useRef, useState } from 'react';
|
||||
@@ -17,59 +17,57 @@ interface SearchInputProps {
|
||||
count: number;
|
||||
}
|
||||
|
||||
export const SearchInput = (
|
||||
({ onChange, count }: SearchInputProps) => {
|
||||
const { colorMode } = useColorMode();
|
||||
export const SearchInput = ({ onChange, count }: SearchInputProps) => {
|
||||
const { colorMode } = useColorMode();
|
||||
|
||||
const [urlValue, setUrlValue] = useRouterParam('search');
|
||||
const [urlValue, setUrlValue] = useRouterParam('search');
|
||||
|
||||
const [inputValue, setInputValue] = useState('');
|
||||
const debouncedValue = useDebounce(inputValue.trim(), 300);
|
||||
const [inputValue, setInputValue] = useState('');
|
||||
const debouncedValue = useDebounce(inputValue.trim(), 300);
|
||||
|
||||
useUpdateEffect(() => {
|
||||
onChange(debouncedValue);
|
||||
setUrlValue(debouncedValue);
|
||||
}, [debouncedValue]);
|
||||
useUpdateEffect(() => {
|
||||
onChange(debouncedValue);
|
||||
setUrlValue(debouncedValue);
|
||||
}, [debouncedValue]);
|
||||
|
||||
useEffect(() => {
|
||||
if (urlValue && !inputValue) {
|
||||
setInputValue(urlValue);
|
||||
onChange(urlValue);
|
||||
useEffect(() => {
|
||||
if (urlValue && !inputValue) {
|
||||
setInputValue(urlValue);
|
||||
onChange(urlValue);
|
||||
}
|
||||
}, [urlValue]);
|
||||
|
||||
const ref = useRef(null);
|
||||
|
||||
// Keyboard `/` shortcut
|
||||
useEffect(() => {
|
||||
const handleKeyDown = (event: KeyboardEvent) => {
|
||||
if (event.key === '/' && ref.current !== document.activeElement) {
|
||||
event.preventDefault();
|
||||
ref.current.focus();
|
||||
}
|
||||
}, [urlValue]);
|
||||
|
||||
const ref = useRef(null);
|
||||
|
||||
// Keyboard `/` shortcut
|
||||
useEffect(() => {
|
||||
const handleKeyDown = (event: KeyboardEvent) => {
|
||||
if (event.key === '/' && ref.current !== document.activeElement) {
|
||||
event.preventDefault();
|
||||
ref.current.focus();
|
||||
}
|
||||
};
|
||||
|
||||
window.addEventListener('keydown', handleKeyDown);
|
||||
return () => window.removeEventListener('keydown', handleKeyDown);
|
||||
}, []);
|
||||
};
|
||||
|
||||
return (
|
||||
<InputGroup position="sticky" top={4} zIndex={1}>
|
||||
<InputLeftElement
|
||||
children={
|
||||
<Icon>
|
||||
<SearchIcon />
|
||||
</Icon>
|
||||
}
|
||||
/>
|
||||
<Input
|
||||
ref={ref}
|
||||
placeholder={`Search ${count} icons (Press "/" to focus)`}
|
||||
onChange={(event) => setInputValue(event.target.value)}
|
||||
value={inputValue}
|
||||
bg={colorMode == 'light' ? theme.colors.white : theme.colors.gray[700]}
|
||||
/>
|
||||
</InputGroup>
|
||||
);
|
||||
}
|
||||
);
|
||||
window.addEventListener('keydown', handleKeyDown);
|
||||
return () => window.removeEventListener('keydown', handleKeyDown);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<InputGroup>
|
||||
<InputLeftElement
|
||||
children={
|
||||
<Icon>
|
||||
<SearchIcon />
|
||||
</Icon>
|
||||
}
|
||||
/>
|
||||
<Input
|
||||
ref={ref}
|
||||
placeholder={`Search ${count} icons (Press "/" to focus)`}
|
||||
onChange={event => setInputValue(event.target.value)}
|
||||
value={inputValue}
|
||||
bg={colorMode == 'light' ? theme.colors.white : theme.colors.gray[700]}
|
||||
/>
|
||||
</InputGroup>
|
||||
);
|
||||
};
|
||||
|
||||
97
site/src/components/UnCategorizedIcons.tsx
Normal file
97
site/src/components/UnCategorizedIcons.tsx
Normal file
@@ -0,0 +1,97 @@
|
||||
import { Box, Heading, useColorModeValue } from '@chakra-ui/react';
|
||||
import { motion } from 'framer-motion';
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { Category, IconEntity } from 'src/types';
|
||||
import theme from '../lib/theme';
|
||||
import IconReorder from './IconReorder';
|
||||
|
||||
const UnCategorizedIcons = ({
|
||||
icons,
|
||||
dropZones,
|
||||
dragging,
|
||||
setDragging,
|
||||
categories,
|
||||
handleChange,
|
||||
}): JSX.Element => {
|
||||
const [scrollPosition, setScrollPosition] = useState(0);
|
||||
const boxBackground = useColorModeValue(theme.colors.white, theme.colors.gray[700]);
|
||||
const allIconContainerRef = useRef(null);
|
||||
const unCategorizedIcons = useMemo(() => {
|
||||
return (icons as IconEntity[]).filter(icon => {
|
||||
return !Object.values(categories as Category[])
|
||||
.flat()
|
||||
.some(categorizedIcon => categorizedIcon.name === icon.name);
|
||||
});
|
||||
}, [icons, categories]);
|
||||
|
||||
const onItemDrop = useCallback(
|
||||
(iconName: string, targetCategory: string) => {
|
||||
const newIcons = [...categories[targetCategory].map(({ name }) => name), iconName];
|
||||
|
||||
handleChange(targetCategory)(newIcons);
|
||||
},
|
||||
[categories],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
allIconContainerRef.current.addEventListener('scroll', () => {
|
||||
setScrollPosition(allIconContainerRef.current.scrollTop);
|
||||
});
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<Box paddingTop={0} position="sticky" top="0">
|
||||
<Box
|
||||
marginTop={5}
|
||||
marginBottom={320}
|
||||
maxWidth="calc(1600px / 2)"
|
||||
width="100%"
|
||||
height="calc(100vh)"
|
||||
borderWidth="1px"
|
||||
boxSizing="border-box"
|
||||
rounded="lg"
|
||||
boxShadow={theme.shadows.xl}
|
||||
bg={boxBackground}
|
||||
padding={8}
|
||||
overflowY={dragging ? 'visible' : 'auto'}
|
||||
ref={allIconContainerRef}
|
||||
as={motion.div}
|
||||
layoutScroll
|
||||
>
|
||||
<Box marginTop={dragging ? scrollPosition * -1 : undefined}>
|
||||
<Heading as="h5" size="sm" marginBottom={4}>
|
||||
Uncategorized Icons
|
||||
</Heading>
|
||||
<IconReorder
|
||||
// key={`uncatogorized-${newKey}`}
|
||||
icons={unCategorizedIcons}
|
||||
dropZones={dropZones}
|
||||
onDrop={onItemDrop}
|
||||
// eslint-disable-next-line @typescript-eslint/no-empty-function
|
||||
setIcons={() => {}}
|
||||
dragging={dragging}
|
||||
setDragging={setDragging}
|
||||
/>
|
||||
|
||||
<Heading as="h5" size="sm" marginBottom={4}>
|
||||
All Icons
|
||||
</Heading>
|
||||
<IconReorder
|
||||
icons={Object.values(icons)}
|
||||
dropZones={dropZones}
|
||||
onDrop={onItemDrop}
|
||||
// eslint-disable-next-line @typescript-eslint/no-empty-function
|
||||
setIcons={() => {}}
|
||||
dragging={dragging}
|
||||
setDragging={setDragging}
|
||||
sx={{
|
||||
opacity: 0.4,
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export default UnCategorizedIcons;
|
||||
28
site/src/hooks/useRaisedShadow.ts
Normal file
28
site/src/hooks/useRaisedShadow.ts
Normal file
@@ -0,0 +1,28 @@
|
||||
import { animate, MotionValue, useMotionValue } from 'framer-motion';
|
||||
import { useEffect } from 'react';
|
||||
|
||||
const inactiveShadow = '0px 0px 0px rgba(0,0,0,0.8)';
|
||||
|
||||
export default function useRaisedShadow(value: MotionValue<number>): MotionValue<string> {
|
||||
const boxShadow = useMotionValue(inactiveShadow);
|
||||
|
||||
useEffect(() => {
|
||||
let isActive = false;
|
||||
value.onChange(latest => {
|
||||
const wasActive = isActive;
|
||||
if (latest !== 0) {
|
||||
isActive = true;
|
||||
if (isActive !== wasActive) {
|
||||
animate(boxShadow, '5px 5px 10px rgba(0,0,0,0.3)');
|
||||
}
|
||||
} else {
|
||||
isActive = false;
|
||||
if (isActive !== wasActive) {
|
||||
animate(boxShadow, inactiveShadow);
|
||||
}
|
||||
}
|
||||
});
|
||||
}, [value, boxShadow]);
|
||||
|
||||
return boxShadow;
|
||||
}
|
||||
32
site/src/lib/categories.ts
Normal file
32
site/src/lib/categories.ts
Normal file
@@ -0,0 +1,32 @@
|
||||
import fs from "fs";
|
||||
import path from "path";
|
||||
import {Category, IconEntity} from "../types";
|
||||
import {getAllData} from "./icons";
|
||||
|
||||
const directory = path.join(process.cwd(), "../categories");
|
||||
|
||||
export function getAllCategoryFiles() {
|
||||
const fileNames = fs.readdirSync(directory).filter((file) => path.extname(file) === '.json');
|
||||
|
||||
return fileNames
|
||||
.map((fileName) => path.basename(fileName, '.json'));
|
||||
}
|
||||
|
||||
export async function getData(name: string, icons: IconEntity[]): Promise<Category> {
|
||||
const jsonPath = path.join(directory, `${name}.json`);
|
||||
const jsonContent = fs.readFileSync(jsonPath, "utf8");
|
||||
const categoryJson = JSON.parse(jsonContent);
|
||||
|
||||
return {
|
||||
...categoryJson,
|
||||
name,
|
||||
iconCount: icons.reduce((acc, curr) => (curr.categories.includes(name) ? ++acc : acc), 0)
|
||||
};
|
||||
}
|
||||
|
||||
export async function getAllCategories(): Promise<Category[]> {
|
||||
const names = getAllCategoryFiles();
|
||||
const icons = await getAllData();
|
||||
|
||||
return Promise.all(names.map((name) => getData(name, icons)));
|
||||
}
|
||||
@@ -1,14 +1,22 @@
|
||||
import NextCache from './nextCache';
|
||||
import {parseSync} from 'svgson';
|
||||
import {getAllData} from './icons';
|
||||
import {getAllData, GetDataOptions} from './icons';
|
||||
|
||||
export type IconNode = [string, object, IconNode[]];
|
||||
export type IconNodes = {[iconName: string]: IconNode};
|
||||
|
||||
export function fetchIconNodes(writeCache = true): Promise<IconNodes> {
|
||||
export function fetchIconNodes(writeCache = true, options?: GetDataOptions): Promise<IconNodes> {
|
||||
if (options?.withChildKeys) {
|
||||
return NextCache.resolve('api-icon-nodes-with-keys', async () => {
|
||||
return (await getAllData({ withChildKeys : true})).reduce((acc, icon) => {
|
||||
acc[icon.name] = icon.iconNode
|
||||
return acc;
|
||||
}, {});
|
||||
}, writeCache);
|
||||
}
|
||||
|
||||
return NextCache.resolve('api-icon-nodes', async () => {
|
||||
return (await getAllData()).reduce((acc, icon) => {
|
||||
acc[icon.name] = parseSync(icon.src).children.map(({name, attributes}) => [name, attributes]);
|
||||
acc[icon.name] = icon.iconNode
|
||||
return acc;
|
||||
}, {});
|
||||
}, writeCache);
|
||||
|
||||
17
site/src/lib/generateZip.ts
Normal file
17
site/src/lib/generateZip.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
export type IconContent = [icon: string, src:string];
|
||||
|
||||
async function generateZip(icons: IconContent[]) {
|
||||
const JSZip = (await import('jszip')).default
|
||||
|
||||
const zip = new JSZip();
|
||||
|
||||
const addingZipPromises = icons.map(([name, src]) =>
|
||||
zip.file(`${name}.svg`, src),
|
||||
);
|
||||
|
||||
await Promise.all(addingZipPromises)
|
||||
|
||||
return zip.generateAsync({ type: 'blob' });
|
||||
}
|
||||
|
||||
export default generateZip
|
||||
16
site/src/lib/getFallbackZip.tsx
Normal file
16
site/src/lib/getFallbackZip.tsx
Normal file
@@ -0,0 +1,16 @@
|
||||
import { createLucideIcon, LucideProps } from "lucide-react"
|
||||
import { IconEntity } from "src/types"
|
||||
import { renderToStaticMarkup } from 'react-dom/server';
|
||||
import { IconContent } from "./generateZip";
|
||||
|
||||
const getFallbackZip = (icons: IconEntity[], params: LucideProps) => {
|
||||
return icons
|
||||
.map<IconContent>((icon) => {
|
||||
const Icon = createLucideIcon(icon.name, icon.iconNode)
|
||||
const src = renderToStaticMarkup(<Icon {...params} />)
|
||||
return [icon.name, src]
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
export default getFallbackZip
|
||||
28
site/src/lib/helpers.ts
Normal file
28
site/src/lib/helpers.ts
Normal file
@@ -0,0 +1,28 @@
|
||||
/**
|
||||
* djb2 hashing function
|
||||
*
|
||||
* @param {string} string
|
||||
* @param {number} seed
|
||||
* @returns {string} A hashed string of 6 characters
|
||||
*/
|
||||
export const hash = (string: string, seed = 5381) => {
|
||||
let i = string.length;
|
||||
|
||||
while (i) {
|
||||
// eslint-disable-next-line no-bitwise, no-plusplus
|
||||
seed = (seed * 33) ^ string.charCodeAt(--i);
|
||||
}
|
||||
|
||||
// eslint-disable-next-line no-bitwise
|
||||
return (seed >>> 0).toString(36).substr(0, 6);
|
||||
};
|
||||
|
||||
/**
|
||||
* Generate Hashed string based on name and attributes
|
||||
*
|
||||
* @param {object} seed
|
||||
* @param {string} seed.name A name, for example an icon name
|
||||
* @param {object} seed.attributes An object of SVGElement Attrbutes
|
||||
* @returns {string} A hashed string of 6 characters
|
||||
*/
|
||||
export const generateHashedKey = ({ name, attributes }) => hash(JSON.stringify([name, attributes]));
|
||||
@@ -1,7 +1,10 @@
|
||||
import fs from "fs";
|
||||
import path from "path";
|
||||
import { parseSync } from "svgson";
|
||||
import { IconNode } from "../../../packages/lucide-react/src/createLucideIcon";
|
||||
import { IconEntity } from "../types";
|
||||
import { getContributors } from "./fetchAllContributors";
|
||||
import { generateHashedKey } from "./helpers";
|
||||
|
||||
const directory = path.join(process.cwd(), "../icons");
|
||||
|
||||
@@ -13,25 +16,42 @@ export function getAllNames() {
|
||||
.map((fileName) => path.basename(fileName, '.json'));
|
||||
}
|
||||
|
||||
export async function getData(name: string) {
|
||||
export interface GetDataOptions {
|
||||
withChildKeys?: boolean
|
||||
}
|
||||
|
||||
export async function getData(name: string, { withChildKeys = false }: GetDataOptions | undefined = {}) {
|
||||
const svgPath = path.join(directory, `${name}.svg`);
|
||||
const svgContent = fs.readFileSync(svgPath, "utf8");
|
||||
const jsonPath = path.join(directory, `${name}.json`);
|
||||
const jsonContent = fs.readFileSync(jsonPath, "utf8");
|
||||
const iconJson = JSON.parse(jsonContent);
|
||||
const { tags, categories } = JSON.parse(jsonContent);
|
||||
|
||||
const iconNode = parseSync(svgContent).children.map(
|
||||
(child) => {
|
||||
const { name, attributes } = child
|
||||
|
||||
if (withChildKeys) {
|
||||
attributes.key = generateHashedKey(child)
|
||||
}
|
||||
|
||||
return [name, attributes]
|
||||
}
|
||||
) as IconNode
|
||||
|
||||
const contributors = await getContributors(name);
|
||||
|
||||
return {
|
||||
...iconJson,
|
||||
name,
|
||||
tags,
|
||||
categories,
|
||||
contributors,
|
||||
src: svgContent
|
||||
iconNode,
|
||||
};
|
||||
}
|
||||
|
||||
export async function getAllData(): Promise<IconEntity[]> {
|
||||
export async function getAllData(options?: GetDataOptions): Promise<IconEntity[]> {
|
||||
const names = getAllNames();
|
||||
|
||||
return Promise.all(names.map((name) => getData(name)));
|
||||
return Promise.all(names.map((name) => getData(name, options)));
|
||||
}
|
||||
|
||||
@@ -21,6 +21,19 @@ const theme = {
|
||||
'800': '#A70B0B',
|
||||
'900': '#720707'
|
||||
},
|
||||
},
|
||||
component: {
|
||||
Button: {
|
||||
variants: {
|
||||
solid: (props) => {
|
||||
// if(props?.colorScheme === 'red') {
|
||||
return {
|
||||
bg: props.colorMode === 'dark' ? 'red.700' : 'red.100',
|
||||
}
|
||||
// }
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -1,8 +1,16 @@
|
||||
import { NextApiRequest, NextApiResponse } from 'next';
|
||||
import {fetchIconNodes} from '../../../lib/fetchIconNodes';
|
||||
|
||||
export default async function handler(req, res) {
|
||||
res.setHeader(
|
||||
'Cache-Control',
|
||||
'public, max-age=86400'
|
||||
).status(200).json(await fetchIconNodes(false));
|
||||
export default async function handler(request: NextApiRequest, response: NextApiResponse) {
|
||||
const params = request.query
|
||||
|
||||
return response
|
||||
.setHeader(
|
||||
'Cache-Control',
|
||||
'public, max-age=86400'
|
||||
)
|
||||
.status(200)
|
||||
.json(
|
||||
await fetchIconNodes(false, params)
|
||||
);
|
||||
}
|
||||
|
||||
105
site/src/pages/edit/categories.tsx
Normal file
105
site/src/pages/edit/categories.tsx
Normal file
@@ -0,0 +1,105 @@
|
||||
import { getAllData } from '../../lib/icons';
|
||||
import Layout from '../../components/Layout';
|
||||
import { Box, Grid, Heading } from '@chakra-ui/react';
|
||||
import IconCategory from '../../components/IconCategory';
|
||||
import CategoryChangesBar from '../../components/CategoryChangesBar';
|
||||
import { useState, useRef, RefAttributes } from 'react';
|
||||
import IconReorder from '../../components/IconReorder';
|
||||
import UnCategorizedIcons from '../../components/UnCategorizedIcons';
|
||||
import { getAllCategories } from 'src/lib/categories';
|
||||
import { Category, IconEntity } from 'src/types';
|
||||
|
||||
interface EditCategoriesPageProps {
|
||||
icons: IconEntity[]
|
||||
categories: Category[]
|
||||
}
|
||||
|
||||
|
||||
const EditCategoriesPage = ({ icons = [], categories: categoryList }: EditCategoriesPageProps) => {
|
||||
const [dragging, setDragging] = useState(false);
|
||||
const [categories, setCategories] = useState<Record<string, IconEntity[]>>(
|
||||
categoryList.reduce((categoryMap, { name }) => {
|
||||
const categoryIcons = icons.filter(({categories}) => categories.includes(name))
|
||||
|
||||
categoryMap[name] = categoryIcons
|
||||
|
||||
return categoryMap;
|
||||
}, {}),
|
||||
);
|
||||
const [changes, setChanges] = useState(0);
|
||||
const dropZones = useRef<[string, HTMLDivElement][]>([]);
|
||||
|
||||
const handleChange = (category: string) => (newIcons: string[]) => {
|
||||
console.log(category, newIcons);
|
||||
|
||||
setCategories(currentCategories => {
|
||||
const newCategories = {
|
||||
...currentCategories,
|
||||
[category]: newIcons.map(iconName => icons[iconName]),
|
||||
};
|
||||
|
||||
if (JSON.stringify(currentCategories) !== JSON.stringify(newCategories)) {
|
||||
setChanges(changes => changes + 1);
|
||||
}
|
||||
|
||||
return newCategories;
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<Layout maxWidth="1600px">
|
||||
<CategoryChangesBar categories={categories} changes={changes} />
|
||||
<Grid templateColumns="1fr 1fr" gridColumnGap={6}>
|
||||
<Box>
|
||||
<Heading as="h4" size="md" paddingTop={4}>
|
||||
All Icons
|
||||
</Heading>
|
||||
<UnCategorizedIcons
|
||||
icons={icons}
|
||||
dropZones={dropZones}
|
||||
dragging={dragging}
|
||||
setDragging={setDragging}
|
||||
categories={categories}
|
||||
handleChange={handleChange}
|
||||
/>
|
||||
</Box>
|
||||
<Box>
|
||||
<Heading as="h4" size="md" paddingTop={4} paddingLeft={4}>
|
||||
Categories
|
||||
</Heading>
|
||||
<Box marginTop={5} marginBottom={320} marginLeft="auto">
|
||||
{Object.entries(categories).map(([category, icons], index) => (
|
||||
<IconCategory
|
||||
name={category}
|
||||
key={category}
|
||||
dragging={dragging}
|
||||
ref={el =>
|
||||
(dropZones.current[index] = [category, el]) as RefAttributes<HTMLDivElement>
|
||||
}
|
||||
>
|
||||
<IconReorder
|
||||
key={`${category}-reorder`}
|
||||
icons={icons}
|
||||
setIcons={handleChange(category)}
|
||||
dropZones={dropZones}
|
||||
// onDrop={onItemDrop}
|
||||
dragging={dragging}
|
||||
setDragging={setDragging}
|
||||
/>
|
||||
</IconCategory>
|
||||
))}
|
||||
</Box>
|
||||
</Box>
|
||||
</Grid>
|
||||
</Layout>
|
||||
);
|
||||
};
|
||||
|
||||
export default EditCategoriesPage;
|
||||
|
||||
export async function getStaticProps() {
|
||||
const icons = await getAllData({ withChildKeys: true });
|
||||
const categories = await getAllCategories()
|
||||
|
||||
return { props: { icons, categories } };
|
||||
}
|
||||
@@ -1,12 +1,13 @@
|
||||
import { useMemo } from 'react';
|
||||
import { useRouter } from 'next/router';
|
||||
import IconDetailOverlay from '../../components/IconDetailOverlay';
|
||||
import { getAllData, getData } from '../../lib/icons';
|
||||
import IconOverview from '../../components/IconOverview';
|
||||
import Layout from '../../components/Layout';
|
||||
import Header from '../../components/Header';
|
||||
import { useMemo } from 'react';
|
||||
import { GetStaticPaths, GetStaticProps } from 'next';
|
||||
import { getAllCategories } from 'src/lib/categories';
|
||||
|
||||
const IconPage = ({ icon, data }) => {
|
||||
const IconPage = ({ icon, data, categories }): JSX.Element => {
|
||||
const router = useRouter();
|
||||
const getIcon = iconName => data.find(({ name }) => name === iconName) || {};
|
||||
|
||||
@@ -21,11 +22,11 @@ const IconPage = ({ icon, data }) => {
|
||||
|
||||
router.push(
|
||||
{
|
||||
pathname: '/',
|
||||
pathname: '/icons',
|
||||
query,
|
||||
},
|
||||
undefined,
|
||||
{ scroll: false },
|
||||
{ scroll: false, shallow: true },
|
||||
);
|
||||
};
|
||||
|
||||
@@ -39,21 +40,22 @@ const IconPage = ({ icon, data }) => {
|
||||
return (
|
||||
<Layout>
|
||||
<IconDetailOverlay key={currentIcon.name} icon={currentIcon} close={onClose} open />
|
||||
<Header {...{ data }} />
|
||||
<IconOverview {...{ data }} />
|
||||
<IconOverview {...{ data, categories }} key="icon-overview" />
|
||||
</Layout>
|
||||
);
|
||||
};
|
||||
|
||||
export default IconPage;
|
||||
|
||||
export async function getStaticProps({ params: { iconName } }) {
|
||||
const data = await getAllData();
|
||||
const icon = await getData(iconName);
|
||||
return { props: { icon, data } };
|
||||
}
|
||||
export const getStaticProps: GetStaticProps = async ({ params: { iconName } }) => {
|
||||
const data = await getAllData({ withChildKeys: true });
|
||||
const icon = await getData(iconName as string, { withChildKeys: true });
|
||||
const categories = await getAllCategories()
|
||||
|
||||
export async function getStaticPaths() {
|
||||
return { props: { icon, data, categories } };
|
||||
};
|
||||
|
||||
export const getStaticPaths: GetStaticPaths = async () => {
|
||||
const data = await getAllData();
|
||||
|
||||
return {
|
||||
@@ -62,4 +64,4 @@ export async function getStaticPaths() {
|
||||
})),
|
||||
fallback: false,
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
28
site/src/pages/icons/index.tsx
Normal file
28
site/src/pages/icons/index.tsx
Normal file
@@ -0,0 +1,28 @@
|
||||
import Layout from '../../components/Layout';
|
||||
import IconOverview from '../../components/IconOverview';
|
||||
import { getAllData } from '../../lib/icons';
|
||||
import { getAllCategories } from 'src/lib/categories';
|
||||
import IconDetailOverlay from 'src/components/IconDetailOverlay';
|
||||
|
||||
const IconsPage = ({ data, categories }) => {
|
||||
return (
|
||||
<Layout>
|
||||
<IconDetailOverlay />
|
||||
<IconOverview {...{ data, categories }} key="icon-overview" />
|
||||
</Layout>
|
||||
);
|
||||
};
|
||||
|
||||
export default IconsPage;
|
||||
|
||||
export async function getStaticProps() {
|
||||
const data = await getAllData({ withChildKeys: true });
|
||||
const categories = await getAllCategories()
|
||||
|
||||
return {
|
||||
props: {
|
||||
data,
|
||||
categories,
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -8,13 +8,15 @@ import Header from '../components/Header';
|
||||
import MobileMenu from '../components/MobileMenu';
|
||||
import { useMemo } from 'react';
|
||||
import { GetStaticPropsResult, NextPage } from 'next';
|
||||
import { IconEntity } from '../types';
|
||||
import { IconEntity, Category } from '../types';
|
||||
import { getAllCategories } from 'src/lib/categories';
|
||||
|
||||
interface HomePageProps {
|
||||
data: IconEntity[]
|
||||
categories: Category[]
|
||||
}
|
||||
|
||||
const HomePage: NextPage<HomePageProps> = ({ data }) => {
|
||||
const HomePage: NextPage<HomePageProps> = ({ data, categories }) => {
|
||||
const router = useRouter();
|
||||
const getIcon = iconName => data.find(({ name }) => name === iconName);
|
||||
|
||||
@@ -28,20 +30,28 @@ const HomePage: NextPage<HomePageProps> = ({ data }) => {
|
||||
<IconDetailOverlay
|
||||
open={!!currentIcon?.name}
|
||||
icon={currentIcon}
|
||||
close={() => router.push('/', undefined, { shallow: true })}
|
||||
close={() => router.push({
|
||||
pathname: '/icon/[iconName]',
|
||||
query: {
|
||||
...router.query,
|
||||
iconName: '',
|
||||
},
|
||||
}, undefined, { shallow: true })}
|
||||
/>
|
||||
<Header {...{ data }} />
|
||||
<IconOverview {...{ data }} />
|
||||
<IconOverview {...{ data, categories }} />
|
||||
</Layout>
|
||||
);
|
||||
};
|
||||
|
||||
export async function getStaticProps(): Promise<GetStaticPropsResult<HomePageProps>> {
|
||||
const data = await getAllData();
|
||||
const data = await getAllData({ withChildKeys: true });
|
||||
const categories = await getAllCategories()
|
||||
|
||||
return {
|
||||
props: {
|
||||
data,
|
||||
categories,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@@ -48,7 +48,12 @@ export const getStaticProps: GetStaticProps = async () => {
|
||||
|
||||
const licenseText = doc
|
||||
.split(/\n{2,}/)
|
||||
.map(paragraph => paragraph.split('\n').join(' ').trim())
|
||||
.map(paragraph =>
|
||||
paragraph
|
||||
.split('\n')
|
||||
.join(' ')
|
||||
.trim(),
|
||||
)
|
||||
.filter(Boolean);
|
||||
|
||||
return { props: { licenseText } };
|
||||
|
||||
23
site/src/static/menuItems.tsx
Normal file
23
site/src/static/menuItems.tsx
Normal file
@@ -0,0 +1,23 @@
|
||||
export default [
|
||||
{
|
||||
name: 'Icons',
|
||||
href: '/icons',
|
||||
},
|
||||
{
|
||||
name: 'Documentation',
|
||||
href: '/docs',
|
||||
},
|
||||
{
|
||||
name: 'Packages',
|
||||
href: '/packages',
|
||||
},
|
||||
{
|
||||
name: 'License',
|
||||
href: '/license',
|
||||
},
|
||||
{
|
||||
name: 'Github',
|
||||
isExternal: true,
|
||||
href: 'https://github.com/lucide-icons/lucide',
|
||||
},
|
||||
];
|
||||
@@ -1,10 +1,20 @@
|
||||
import { IconNode } from "../../packages/lucide-react/dist/lucide-react";
|
||||
|
||||
export interface IconEntity {
|
||||
contributors: Contributor[];
|
||||
name: string;
|
||||
src: string;
|
||||
tags: string[];
|
||||
categories: string[];
|
||||
contributors: Contributor[];
|
||||
iconNode: IconNode;
|
||||
}
|
||||
|
||||
export interface Contributor {
|
||||
author: string;
|
||||
}
|
||||
|
||||
export interface Category {
|
||||
name: string
|
||||
title: string
|
||||
icon?: string
|
||||
iconCount: number
|
||||
}
|
||||
|
||||
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user