mirror of
https://github.com/lucide-icons/lucide.git
synced 2025-12-16 11:47:42 +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",
|
"$schema": "../category.schema.json",
|
||||||
"title": "Text formatting",
|
"title": "Text formatting",
|
||||||
"icon": "type"
|
"icon": "type"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { forwardRef, createElement, ReactSVG, SVGProps } from 'react';
|
import { forwardRef, createElement, ReactSVG, SVGProps } from 'react';
|
||||||
import defaultAttributes from './defaultAttributes';
|
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>>
|
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',
|
'eslint:recommended',
|
||||||
'plugin:@typescript-eslint/recommended',
|
'plugin:@typescript-eslint/recommended',
|
||||||
'plugin:@next/next/recommended',
|
'plugin:@next/next/recommended',
|
||||||
|
'plugin:react-hooks/recommended'
|
||||||
],
|
],
|
||||||
parserOptions: {
|
parserOptions: {
|
||||||
tsconfigRootDir: __dirname,
|
tsconfigRootDir: __dirname,
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "next dev",
|
"dev": "next dev",
|
||||||
"copy-assets": "mkdir -p ./public/docs/images && cp -rf ../docs/images ./public/docs",
|
"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",
|
"build": "pnpm copy-assets && pnpm prebuild && next build",
|
||||||
"export": "next export -o build",
|
"export": "next export -o build",
|
||||||
"deploy": "pnpm build && pnpm export",
|
"deploy": "pnpm build && pnpm export",
|
||||||
@@ -24,16 +24,17 @@
|
|||||||
"@next/mdx": "^11.0.0",
|
"@next/mdx": "^11.0.0",
|
||||||
"@svgr/webpack": "^6.3.1",
|
"@svgr/webpack": "^6.3.1",
|
||||||
"downloadjs": "^1.4.7",
|
"downloadjs": "^1.4.7",
|
||||||
|
"framer-motion": "^6.2.8",
|
||||||
"element-to-path": "^1.2.1",
|
"element-to-path": "^1.2.1",
|
||||||
"framer-motion": "^4",
|
|
||||||
"fuse.js": "^6.5.3",
|
"fuse.js": "^6.5.3",
|
||||||
"gray-matter": "^4.0.3",
|
"gray-matter": "^4.0.3",
|
||||||
"js-yaml": "^4.1.0",
|
"js-yaml": "^4.1.0",
|
||||||
"jszip": "^3.7.0",
|
"jszip": "^3.7.0",
|
||||||
"lodash": "^4.17.20",
|
"lodash": "^4.17.20",
|
||||||
"lucide-react": "^0.94.0",
|
"lucide-react": "workspace:*",
|
||||||
"next": "12",
|
"next": "12",
|
||||||
"next-mdx-remote": "^3.0.2",
|
"next-mdx-remote": "^3.0.2",
|
||||||
|
"object-path": "0.11.5",
|
||||||
"prism-react-renderer": "^1.2.1",
|
"prism-react-renderer": "^1.2.1",
|
||||||
"react": "17.0.2",
|
"react": "17.0.2",
|
||||||
"react-color": "^2.19.3",
|
"react-color": "^2.19.3",
|
||||||
@@ -59,6 +60,7 @@
|
|||||||
"babel-loader": "^8.1.0",
|
"babel-loader": "^8.1.0",
|
||||||
"eslint": "^8.22.0",
|
"eslint": "^8.22.0",
|
||||||
"eslint-config-prettier": "^8.5.0",
|
"eslint-config-prettier": "^8.5.0",
|
||||||
|
"eslint-plugin-react-hooks": "^4.6.0",
|
||||||
"jest": "^26.5.2",
|
"jest": "^26.5.2",
|
||||||
"node-fetch": "2",
|
"node-fetch": "2",
|
||||||
"prettier": "^2.3.2",
|
"prettier": "^2.3.2",
|
||||||
|
|||||||
@@ -17,3 +17,7 @@ body {
|
|||||||
min-height: 100%;
|
min-height: 100%;
|
||||||
padding-bottom: 80px;
|
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 nightOwlLightTheme from 'prism-react-renderer/themes/nightOwlLight';
|
||||||
import nightOwlDarkTheme from 'prism-react-renderer/themes/nightOwl';
|
import nightOwlDarkTheme from 'prism-react-renderer/themes/nightOwl';
|
||||||
import uiTheme from '../lib/theme';
|
import uiTheme from '../lib/theme';
|
||||||
// import theme from 'prism-react-renderer/themes/nightOwl';
|
|
||||||
import BaseHighlight, { defaultProps, Language } from 'prism-react-renderer';
|
import BaseHighlight, { defaultProps, Language } from 'prism-react-renderer';
|
||||||
import { CSSProperties } from 'react';
|
import { CSSProperties } from 'react';
|
||||||
import CopyButton from './CopyButton';
|
import CopyButton from './CopyButton';
|
||||||
@@ -49,7 +48,7 @@ function CodeBlock({ code, language, metastring, showLines, ...props }: Highligh
|
|||||||
const { colorMode } = useColorMode();
|
const { colorMode } = useColorMode();
|
||||||
|
|
||||||
const backgroundColor =
|
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 codeTheme = colorMode === 'light' ? nightOwlLightTheme : nightOwlDarkTheme;
|
||||||
|
|
||||||
const customizedCodeTheme = {
|
const customizedCodeTheme = {
|
||||||
@@ -63,6 +62,8 @@ function CodeBlock({ code, language, metastring, showLines, ...props }: Highligh
|
|||||||
return (
|
return (
|
||||||
<Box position="relative" zIndex="0" {...props}>
|
<Box position="relative" zIndex="0" {...props}>
|
||||||
<CodeContainer bg={backgroundColor}>
|
<CodeContainer bg={backgroundColor}>
|
||||||
|
{/* eslint-disable-next-line @typescript-eslint/ban-ts-comment */}
|
||||||
|
{/* @ts-ignore */}
|
||||||
<BaseHighlight
|
<BaseHighlight
|
||||||
{...defaultProps}
|
{...defaultProps}
|
||||||
code={code}
|
code={code}
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ const CopyButton = ({ copyText, buttonText = 'copy', ...props }) => {
|
|||||||
const { hasCopied, onCopy } = useClipboard(copyText);
|
const { hasCopied, onCopy } = useClipboard(copyText);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Button onClick={onCopy} {...props}>
|
<Button onClick={onCopy} {...props} variant="solid">
|
||||||
{hasCopied ? 'copied' : buttonText}
|
{hasCopied ? 'copied' : buttonText}
|
||||||
</Button>
|
</Button>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -59,10 +59,21 @@ export function CustomizeIconContext({ children }): JSX.Element {
|
|||||||
return <IconStyleContext.Provider value={value}>{children}</IconStyleContext.Provider>;
|
return <IconStyleContext.Provider value={value}>{children}</IconStyleContext.Provider>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useCustomizeIconContext(): ICustomIconStyle {
|
export function useCustomizeIconContext() {
|
||||||
const context = useContext(IconStyleContext);
|
const context = useContext(IconStyleContext);
|
||||||
if (context === undefined) {
|
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;
|
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 from 'downloadjs';
|
||||||
import { Download, Github } from 'lucide-react';
|
import { Download, Github } from 'lucide-react';
|
||||||
import NextLink from 'next/link';
|
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 AngularLogo from '../../public/framework-logos/angular.svg';
|
||||||
import FlutterLogo from '../../public/framework-logos/flutter.svg';
|
import FlutterLogo from '../../public/framework-logos/flutter.svg';
|
||||||
import SvelteLogo from '../../public/framework-logos/svelte.svg';
|
import SvelteLogo from '../../public/framework-logos/svelte.svg';
|
||||||
import { useState } from 'react';
|
import { useCallback, useState } from 'react';
|
||||||
import { useCustomizeIconContext } from './CustomizeIconContext';
|
import { useCustomizeIconContext } from './CustomizeIconContext';
|
||||||
import { IconEntity } from '../types';
|
import { IconEntity } from '../types';
|
||||||
|
import generateZip, { IconContent } from 'src/lib/generateZip';
|
||||||
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' });
|
|
||||||
}
|
|
||||||
|
|
||||||
interface HeaderProps {
|
interface HeaderProps {
|
||||||
data: IconEntity[];
|
data: IconEntity[];
|
||||||
@@ -37,25 +22,27 @@ interface HeaderProps {
|
|||||||
|
|
||||||
const Header = ({ data }: HeaderProps) => {
|
const Header = ({ data }: HeaderProps) => {
|
||||||
const [zippingIcons, setZippingIcons] = useState(false);
|
const [zippingIcons, setZippingIcons] = useState(false);
|
||||||
const { iconsRef } = useCustomizeIconContext();
|
const { iconsRef, strokeWidth, color, size } = useCustomizeIconContext();
|
||||||
|
|
||||||
const downloadAllIcons = async () => {
|
const downloadAllIcons = useCallback(async () => {
|
||||||
setZippingIcons(true);
|
setZippingIcons(true);
|
||||||
|
|
||||||
let iconEntries: IconContent[] = Object.entries(iconsRef.current).map(([name, svgEl]) => [
|
let iconEntries: IconContent[] = Object.entries(iconsRef.current)
|
||||||
|
.map(([name, svgEl]) => [
|
||||||
name,
|
name,
|
||||||
svgEl.outerHTML,
|
svgEl.outerHTML,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
// Fallback
|
// Fallback
|
||||||
if (iconEntries.length === 0) {
|
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);
|
const zip = await generateZip(iconEntries);
|
||||||
download(zip, 'lucide.zip');
|
download(zip, 'lucide.zip');
|
||||||
setZippingIcons(false);
|
setZippingIcons(false);
|
||||||
};
|
}, []);
|
||||||
|
|
||||||
const repositoryUrl = 'https://github.com/lucide-icons/lucide';
|
const repositoryUrl = 'https://github.com/lucide-icons/lucide';
|
||||||
|
|
||||||
@@ -117,83 +104,85 @@ const Header = ({ data }: HeaderProps) => {
|
|||||||
];
|
];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Flex direction="column" align="center" justify="center">
|
<Box maxW="1250px" mx="auto">
|
||||||
<Heading as="h1" fontSize="4xl" mb="4" textAlign="center">
|
<Flex direction="column" align="center" justify="center" py={12}>
|
||||||
Beautiful & consistent icon toolkit made by the community.
|
<Heading as="h1" fontSize="4xl" mb="4" textAlign="center">
|
||||||
</Heading>
|
Beautiful & consistent icon toolkit made by the community.
|
||||||
<Text fontSize="lg" as="p" textAlign="center" mb="1">
|
</Heading>
|
||||||
Open-source project and a fork of{' '}
|
<Text fontSize="lg" as="p" textAlign="center" mb="1">
|
||||||
<Link href="https://github.com/feathericons/feather" isExternal>
|
Open-source project and a fork of{' '}
|
||||||
Feather Icons
|
<Link href="https://github.com/feathericons/feather" isExternal>
|
||||||
</Link>
|
Feather Icons
|
||||||
. <br />
|
</Link>
|
||||||
We're expanding the icon set as much as possible while keeping it nice-looking -{' '}
|
. <br />
|
||||||
<Link href={repositoryUrl} isExternal>
|
We're expanding the icon set as much as possible while keeping it nice-looking -{' '}
|
||||||
join us
|
<Link href={repositoryUrl} isExternal>
|
||||||
</Link>
|
join us
|
||||||
!
|
</Link>
|
||||||
</Text>
|
!
|
||||||
<Wrap
|
</Text>
|
||||||
marginTop={4}
|
<Wrap
|
||||||
marginBottom={6}
|
marginTop={4}
|
||||||
spacing={{ base: 4, lg: 6 }}
|
marginBottom={6}
|
||||||
justify="center"
|
spacing={{ base: 4, lg: 6 }}
|
||||||
align="center"
|
justify="center"
|
||||||
>
|
align="center"
|
||||||
<WrapItem flexBasis="100%" style={{ marginBottom: 0 }}>
|
>
|
||||||
<NextLink href="/packages" passHref>
|
<WrapItem flexBasis="100%" style={{ marginBottom: 0 }}>
|
||||||
<Link _hover={{ opacity: 0.8 }} marginX="auto">
|
<NextLink href="/packages" passHref>
|
||||||
<Text fontSize="md" opacity={0.5} as="p" textAlign="center" width="100%">
|
<Link _hover={{ opacity: 0.8 }} marginX="auto">
|
||||||
Available for:
|
<Text fontSize="md" opacity={0.5} as="p" textAlign="center" width="100%">
|
||||||
</Text>
|
Available for:
|
||||||
</Link>
|
</Text>
|
||||||
</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 />
|
|
||||||
</Link>
|
</Link>
|
||||||
</NextLink>
|
</NextLink>
|
||||||
</WrapItem>
|
</WrapItem>
|
||||||
))}
|
{packages.map(({ name, href, Logo, label }) => (
|
||||||
<WrapItem>
|
<WrapItem key={name}>
|
||||||
<NextLink href="/packages" passHref>
|
<NextLink href={href} key={name} passHref>
|
||||||
<Link _hover={{ opacity: 0.8 }} marginX="auto">
|
<Link _hover={{ opacity: 0.8 }} aria-label={label}>
|
||||||
<Text fontSize="md" opacity={0.5}>More options</Text>
|
<Logo />
|
||||||
</Link>
|
</Link>
|
||||||
</NextLink>
|
</NextLink>
|
||||||
</WrapItem>
|
</WrapItem>
|
||||||
</Wrap>
|
))}
|
||||||
<Wrap marginTop={3} marginBottom={12} spacing="15px" justify="center">
|
<WrapItem>
|
||||||
<WrapItem>
|
<NextLink href="/packages" passHref>
|
||||||
<Button
|
<Link _hover={{ opacity: 0.8 }} marginX="auto">
|
||||||
leftIcon={<Download />}
|
<Text fontSize="md" opacity={0.5}>More options</Text>
|
||||||
size="lg"
|
</Link>
|
||||||
onClick={downloadAllIcons}
|
</NextLink>
|
||||||
isLoading={zippingIcons}
|
</WrapItem>
|
||||||
loadingText="Creating zip.."
|
</Wrap>
|
||||||
>
|
<Wrap marginTop={3} marginBottom={12} spacing="15px" justify="center">
|
||||||
Download all
|
<WrapItem>
|
||||||
</Button>
|
<Button
|
||||||
</WrapItem>
|
leftIcon={<Download />}
|
||||||
<WrapItem>
|
size="lg"
|
||||||
<IconCustomizerDrawer />
|
onClick={downloadAllIcons}
|
||||||
</WrapItem>
|
isLoading={zippingIcons}
|
||||||
<WrapItem>
|
loadingText="Creating zip.."
|
||||||
<Button
|
>
|
||||||
as="a"
|
Download all
|
||||||
leftIcon={<Github />}
|
</Button>
|
||||||
size="lg"
|
</WrapItem>
|
||||||
href={repositoryUrl}
|
<WrapItem>
|
||||||
target="__blank"
|
<IconCustomizerDrawer />
|
||||||
>
|
</WrapItem>
|
||||||
Github
|
<WrapItem>
|
||||||
</Button>
|
<Button
|
||||||
</WrapItem>
|
as="a"
|
||||||
</Wrap>
|
leftIcon={<Github />}
|
||||||
</Flex>
|
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 { useState } from 'react';
|
||||||
import { IconStyleContext } from './CustomizeIconContext';
|
import { useCustomizeIconContext } from './CustomizeIconContext';
|
||||||
import { Edit } from 'lucide-react';
|
import { Edit } from 'lucide-react';
|
||||||
import {
|
import {
|
||||||
Button,
|
Button,
|
||||||
@@ -8,30 +8,56 @@ import {
|
|||||||
DrawerCloseButton,
|
DrawerCloseButton,
|
||||||
DrawerContent,
|
DrawerContent,
|
||||||
DrawerHeader,
|
DrawerHeader,
|
||||||
DrawerOverlay,
|
|
||||||
FormControl,
|
FormControl,
|
||||||
FormLabel,
|
FormLabel,
|
||||||
Grid,
|
Grid,
|
||||||
|
Hide,
|
||||||
|
IconButton,
|
||||||
|
Show,
|
||||||
Slider,
|
Slider,
|
||||||
SliderFilledTrack,
|
SliderFilledTrack,
|
||||||
SliderThumb,
|
SliderThumb,
|
||||||
SliderTrack,
|
SliderTrack,
|
||||||
Flex,
|
Flex,
|
||||||
Text,
|
Text,
|
||||||
|
ButtonProps,
|
||||||
} from '@chakra-ui/react';
|
} from '@chakra-ui/react';
|
||||||
import ColorPicker from './ColorPicker';
|
import ColorPicker from './ColorPicker';
|
||||||
|
|
||||||
export function IconCustomizerDrawer() {
|
export const IconCustomizerDrawer = (props: ButtonProps) => {
|
||||||
const [showCustomize, setShowCustomize] = useState(false);
|
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 (
|
return (
|
||||||
<>
|
<>
|
||||||
<Button as="a" leftIcon={<Edit />} size="lg" onClick={() => setShowCustomize(true)}>
|
<Hide below='md'>
|
||||||
Customize
|
<Button
|
||||||
</Button>
|
as="a"
|
||||||
<Drawer isOpen={showCustomize} placement="right" onClose={() => setShowCustomize(false)}>
|
leftIcon={<Edit />}
|
||||||
<DrawerOverlay />
|
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>
|
<DrawerContent>
|
||||||
<DrawerCloseButton />
|
<DrawerCloseButton />
|
||||||
<DrawerHeader>Customize Icons</DrawerHeader>
|
<DrawerHeader>Customize Icons</DrawerHeader>
|
||||||
@@ -42,7 +68,7 @@ export function IconCustomizerDrawer() {
|
|||||||
<ColorPicker
|
<ColorPicker
|
||||||
color={color}
|
color={color}
|
||||||
value={color}
|
value={color}
|
||||||
onChangeComplete={(col) => setColor(col.hex)}
|
onChangeComplete={col => setColor(col.hex)}
|
||||||
/>
|
/>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
@@ -93,4 +119,4 @@ export function IconCustomizerDrawer() {
|
|||||||
</Drawer>
|
</Drawer>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
};
|
||||||
|
|||||||
@@ -2,11 +2,11 @@ import { Box, Text, IconButton, useColorMode, Flex, Slide, ButtonGroup, Button,
|
|||||||
import theme from "../lib/theme";
|
import theme from "../lib/theme";
|
||||||
import download from 'downloadjs';
|
import download from 'downloadjs';
|
||||||
import { X as Close } from 'lucide-react';
|
import { X as Close } from 'lucide-react';
|
||||||
import {useContext, useEffect, useRef} from "react";
|
import { useEffect, useRef } from "react";
|
||||||
import {IconStyleContext} from "./CustomizeIconContext";
|
import {useCustomizeIconContext} from "./CustomizeIconContext";
|
||||||
import {IconWrapper} from "./IconWrapper";
|
|
||||||
import ModifiedTooltip from "./ModifiedTooltip";
|
import ModifiedTooltip from "./ModifiedTooltip";
|
||||||
import { IconEntity } from "../types";
|
import { IconEntity } from "../types";
|
||||||
|
import { createLucideIcon } from 'lucide-react';
|
||||||
|
|
||||||
type IconDownload = {
|
type IconDownload = {
|
||||||
src: string;
|
src: string;
|
||||||
@@ -14,15 +14,17 @@ type IconDownload = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
interface IconDetailOverlayProps {
|
interface IconDetailOverlayProps {
|
||||||
open: boolean
|
open?: boolean
|
||||||
close: () => void
|
close?: () => void
|
||||||
icon?: IconEntity
|
icon?: IconEntity
|
||||||
}
|
}
|
||||||
|
|
||||||
const IconDetailOverlay = ({ open = true, close, icon }: IconDetailOverlayProps) => {
|
const IconDetailOverlay = ({ open = true, close, icon }: IconDetailOverlayProps) => {
|
||||||
const toast = useToast();
|
const toast = useToast();
|
||||||
const { colorMode } = useColorMode();
|
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 iconRef = useRef<SVGSVGElement>(null);
|
||||||
const [isMobile] = useMediaQuery("(max-width: 560px)")
|
const [isMobile] = useMediaQuery("(max-width: 560px)")
|
||||||
const { isOpen, onOpen, onClose } = useDisclosure()
|
const { isOpen, onOpen, onClose } = useDisclosure()
|
||||||
@@ -37,8 +39,6 @@ const IconDetailOverlay = ({ open = true, close, icon }: IconDetailOverlayProps)
|
|||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
const { tags = [], name = '' } = icon;
|
|
||||||
|
|
||||||
const handleClose = () => {
|
const handleClose = () => {
|
||||||
onClose();
|
onClose();
|
||||||
close();
|
close();
|
||||||
@@ -54,10 +54,12 @@ const IconDetailOverlay = ({ open = true, close, icon }: IconDetailOverlayProps)
|
|||||||
color: color,
|
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 downloadIcon = ({name = ''} : IconDownload) => download(iconRef.current.outerHTML, `${name}.svg`, 'image/svg+xml');
|
||||||
const trimmedSrc = src.replace(/(\r\n|\n|\r|\s\s)/gm, "")
|
|
||||||
|
const copyIcon = async ({name} : IconDownload) => {
|
||||||
|
const trimmedSrc = iconRef.current.outerHTML.replace(/(\r\n|\n|\r|\s\s)/gm, "")
|
||||||
|
|
||||||
await navigator.clipboard.writeText(trimmedSrc)
|
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');
|
const canvas = document.createElement('canvas');
|
||||||
canvas.width = size;
|
canvas.width = size;
|
||||||
canvas.height = size;
|
canvas.height = size;
|
||||||
const ctx = canvas.getContext("2d");
|
const ctx = canvas.getContext("2d");
|
||||||
|
|
||||||
const image = new Image();
|
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() {
|
image.onload = function() {
|
||||||
ctx.drawImage(image, 0, 0);
|
ctx.drawImage(image, 0, 0);
|
||||||
|
|
||||||
@@ -98,7 +100,7 @@ const IconDetailOverlay = ({ open = true, close, icon }: IconDetailOverlayProps)
|
|||||||
height={0}
|
height={0}
|
||||||
key={name}
|
key={name}
|
||||||
>
|
>
|
||||||
<Slide direction="bottom" in={isOpen} style={{ zIndex: 10 }}>
|
<Slide direction="bottom" in={isOpen} style={{ zIndex: 10, pointerEvents: 'none' }}>
|
||||||
<Flex
|
<Flex
|
||||||
alignItems="center"
|
alignItems="center"
|
||||||
justifyContent="space-between"
|
justifyContent="space-between"
|
||||||
@@ -120,6 +122,7 @@ const IconDetailOverlay = ({ open = true, close, icon }: IconDetailOverlayProps)
|
|||||||
? theme.colors.white
|
? theme.colors.white
|
||||||
: theme.colors.gray[700]
|
: theme.colors.gray[700]
|
||||||
}
|
}
|
||||||
|
style={{ pointerEvents: 'initial' }}
|
||||||
padding={8}
|
padding={8}
|
||||||
>
|
>
|
||||||
<IconButton
|
<IconButton
|
||||||
@@ -151,14 +154,13 @@ const IconDetailOverlay = ({ open = true, close, icon }: IconDetailOverlayProps)
|
|||||||
style={iconStyling}
|
style={iconStyling}
|
||||||
className="icon-large"
|
className="icon-large"
|
||||||
>
|
>
|
||||||
<IconWrapper
|
<Icon
|
||||||
src={icon.src}
|
|
||||||
stroke={color}
|
stroke={color}
|
||||||
strokeWidth={strokeWidth}
|
strokeWidth={strokeWidth}
|
||||||
height={size}
|
size={size}
|
||||||
width={size}
|
|
||||||
ref={iconRef}
|
ref={iconRef}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
</div>
|
</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">
|
<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 { Grid } from '@chakra-ui/react';
|
||||||
import Link from 'next/link';
|
|
||||||
import { useRouter } from 'next/router';
|
|
||||||
import { memo } from 'react';
|
import { memo } from 'react';
|
||||||
import IconListItem from './IconListItem';
|
import IconListItem from './IconListItem';
|
||||||
import { IconEntity } from '../types';
|
import { IconEntity } from '../types';
|
||||||
|
|
||||||
interface IconListProps {
|
interface IconListProps {
|
||||||
icons: IconEntity[];
|
icons: IconEntity[];
|
||||||
|
category?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
const IconList = memo(({ icons }: IconListProps) => {
|
const IconList = memo(({ icons, category = '' }: IconListProps) => {
|
||||||
const router = useRouter();
|
|
||||||
|
|
||||||
return (
|
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 => {
|
{icons.map(icon => {
|
||||||
return (
|
return (
|
||||||
<Link
|
<IconListItem {...icon} key={`${category}-${icon.name}`}/>
|
||||||
key={icon.name}
|
|
||||||
scroll={false}
|
|
||||||
shallow={true}
|
|
||||||
href={{
|
|
||||||
pathname: '/icon/[iconName]',
|
|
||||||
query: {
|
|
||||||
...router.query,
|
|
||||||
iconName: icon.name,
|
|
||||||
},
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<IconListItem {...icon} />
|
|
||||||
</Link>
|
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</Grid>
|
</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 download from 'downloadjs';
|
||||||
import { memo } from 'react';
|
import { memo, useCallback } from 'react';
|
||||||
|
import { createLucideIcon, IconNode } from 'lucide-react';
|
||||||
import { useCustomizeIconContext } from './CustomizeIconContext';
|
import { useCustomizeIconContext } from './CustomizeIconContext';
|
||||||
import { IconWrapper } from './IconWrapper';
|
import { useRouter } from 'next/router';
|
||||||
|
|
||||||
interface IconListItemProps {
|
interface IconListItemProps extends ButtonProps {
|
||||||
name: string;
|
name: string;
|
||||||
onClick?: ButtonProps['onClick']
|
iconNode: IconNode;
|
||||||
src: string;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const IconListItem = ({ name, onClick, src: svg }: IconListItemProps) => {
|
const IconListItem = ({ name, iconNode }: IconListItemProps) => {
|
||||||
|
const router = useRouter()
|
||||||
const toast = useToast();
|
const toast = useToast();
|
||||||
const { color, size, strokeWidth, iconsRef } = useCustomizeIconContext();
|
const { color, size, strokeWidth, iconsRef } = useCustomizeIconContext();
|
||||||
|
|
||||||
const handleClick:ButtonProps['onClick'] = async (event) => {
|
const Icon = createLucideIcon(name, iconNode)
|
||||||
const src = (iconsRef.current[name].outerHTML ?? svg).replace(/(\r\n|\n|\r|(>\s\s<))/gm, "")
|
|
||||||
|
const handleClick:ButtonProps['onClick'] = useCallback(async (event) => {
|
||||||
|
const src = (iconsRef.current[name].outerHTML).replace(/(\r\n|\n|\r|(>\s\s<))/gm, "")
|
||||||
|
|
||||||
if (event.shiftKey) {
|
if (event.shiftKey) {
|
||||||
await navigator.clipboard.writeText(src)
|
await navigator.clipboard.writeText(src)
|
||||||
|
|
||||||
@@ -25,46 +29,53 @@ const IconListItem = ({ name, onClick, src: svg }: IconListItemProps) => {
|
|||||||
status: 'success',
|
status: 'success',
|
||||||
duration: 1500,
|
duration: 1500,
|
||||||
});
|
});
|
||||||
|
return
|
||||||
}
|
}
|
||||||
if (event.altKey) {
|
if (event.altKey) {
|
||||||
download(src, `${name}.svg`, 'image/svg+xml');
|
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 (
|
return (
|
||||||
<Button
|
<Tooltip label={name}>
|
||||||
variant="ghost"
|
<Button
|
||||||
borderWidth="1px"
|
as="a"
|
||||||
rounded="lg"
|
variant="ghost"
|
||||||
padding={2}
|
borderWidth="1px"
|
||||||
height={32}
|
rounded="lg"
|
||||||
position="relative"
|
padding={2}
|
||||||
whiteSpace="normal"
|
height={20}
|
||||||
onClick={handleClick}
|
width={20}
|
||||||
key={name}
|
position="relative"
|
||||||
alignItems="center"
|
whiteSpace="normal"
|
||||||
>
|
alignItems="center"
|
||||||
<Flex direction="column" align="center" justify="stretch" width="100%" gap={4}>
|
sx={{cursor: 'pointer'}}
|
||||||
<Flex flex={2} flexBasis="100%" minHeight={10} align="flex-end">
|
onClick={handleClick}
|
||||||
<IconWrapper
|
>
|
||||||
src={svg}
|
<Icon
|
||||||
stroke={color}
|
ref={iconEl => (iconsRef.current[name] = iconEl)}
|
||||||
strokeWidth={strokeWidth}
|
size={size}
|
||||||
height={size}
|
stroke={color}
|
||||||
width={size}
|
strokeWidth={strokeWidth}
|
||||||
ref={iconEl => (iconsRef.current[name] = iconEl)}
|
/>
|
||||||
/>
|
</Button>
|
||||||
</Flex>
|
</Tooltip>
|
||||||
<Flex flex={1} minHeight={10} align="center">
|
|
||||||
<Text wordBreak="break-word" maxWidth="100%">
|
|
||||||
{name}
|
|
||||||
</Text>
|
|
||||||
</Flex>
|
|
||||||
</Flex>
|
|
||||||
</Button>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1,16 +1,38 @@
|
|||||||
import { Box, Text } from '@chakra-ui/react';
|
import { Box, Text, IconButton, HStack } from '@chakra-ui/react';
|
||||||
import React, { useState } from 'react';
|
import React, { memo, useEffect, useState } from 'react';
|
||||||
import useSearch from '../lib/useSearch';
|
import useSearch from '../lib/useSearch';
|
||||||
import IconList from './IconList';
|
import IconList from './IconList';
|
||||||
import { SearchInput } from './SearchInput';
|
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 {
|
interface IconOverviewProps {
|
||||||
data: IconEntity[];
|
data: IconEntity[];
|
||||||
|
categories: Category[]
|
||||||
}
|
}
|
||||||
|
|
||||||
const IconOverview = ({ data }: IconOverviewProps) => {
|
const IconOverview = ({ data, categories }: IconOverviewProps): JSX.Element => {
|
||||||
const [query, setQuery] = useState('');
|
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, [
|
const searchResults = useSearch(query, data, [
|
||||||
{ name: 'name', weight: 2 },
|
{ name: 'name', weight: 2 },
|
||||||
@@ -18,20 +40,49 @@ const IconOverview = ({ data }: IconOverviewProps) => {
|
|||||||
]);
|
]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<Box>
|
||||||
<SearchInput onChange={setQuery} count={data.length} />
|
<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}>
|
return !currentView
|
||||||
{searchResults.length > 0 ? (
|
})}
|
||||||
<IconList icons={searchResults} />
|
icon={<SidebarIcon />}
|
||||||
) : (
|
/>
|
||||||
<Text fontSize="2xl" fontWeight="bold" textAlign="center" wordBreak="break-word">
|
<SearchInput onChange={setQuery} count={data.length} />
|
||||||
No results found for "{query}"
|
<IconCustomizerDrawer size="md" paddingX={6} />
|
||||||
</Text>
|
</HStack>
|
||||||
)}
|
|
||||||
</Box>
|
<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 { Moon, Sun, Menu, X } from 'lucide-react';
|
||||||
import { useMobileNavigationContext, useMobileNavigationValue } from './MobileNavigationProvider';
|
import { useMobileNavigationContext, useMobileNavigationValue } from './MobileNavigationProvider';
|
||||||
import Logo from './Logo';
|
import Logo from './Logo';
|
||||||
|
import menuItems from '../static/menuItems';
|
||||||
|
|
||||||
interface LayoutProps extends BoxProps {
|
interface LayoutProps extends BoxProps {
|
||||||
aside?: BoxProps['children'];
|
aside?: BoxProps['children'];
|
||||||
@@ -37,12 +38,14 @@ const Layout = ({ aside, children }: LayoutProps) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
function setQuery(query) {
|
function setQuery(query) {
|
||||||
router.push({
|
router.push(
|
||||||
|
{
|
||||||
pathname: '/',
|
pathname: '/',
|
||||||
query: { query: query },
|
query: { query: query },
|
||||||
},
|
},
|
||||||
undefined,
|
undefined,
|
||||||
{ shallow: true })
|
{ shallow: true },
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
useKeyBindings({
|
useKeyBindings({
|
||||||
@@ -56,7 +59,7 @@ const Layout = ({ aside, children }: LayoutProps) => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Box h="100vh">
|
<Box h="100vh">
|
||||||
<Flex mb={16} w="full">
|
<Flex w="full">
|
||||||
<Flex
|
<Flex
|
||||||
alignItems="center"
|
alignItems="center"
|
||||||
justifyContent="space-between"
|
justifyContent="space-between"
|
||||||
@@ -72,29 +75,28 @@ const Layout = ({ aside, children }: LayoutProps) => {
|
|||||||
<Flex justifyContent="center" alignItems="center">
|
<Flex justifyContent="center" alignItems="center">
|
||||||
{showBaseNavigation ? (
|
{showBaseNavigation ? (
|
||||||
<>
|
<>
|
||||||
<NextLink href="/docs" passHref>
|
{menuItems.map(menuItem => {
|
||||||
<Link marginRight={12} fontSize="xl">
|
if (menuItem.isExternal) {
|
||||||
Documentation
|
return (
|
||||||
</Link>
|
<Link
|
||||||
</NextLink>
|
href={menuItem.href}
|
||||||
<NextLink href="/packages" passHref>
|
isExternal
|
||||||
<Link marginRight={12} fontSize="xl">
|
marginRight={6}
|
||||||
Packages
|
fontSize="lg"
|
||||||
</Link>
|
key={menuItem.name}
|
||||||
</NextLink>
|
>
|
||||||
<NextLink href="/license" passHref>
|
{menuItem.name}
|
||||||
<Link marginRight={12} fontSize="xl">
|
</Link>
|
||||||
License
|
);
|
||||||
</Link>
|
}
|
||||||
</NextLink>
|
return (
|
||||||
<Link
|
<NextLink href={menuItem.href} passHref key={menuItem.name}>
|
||||||
href="https://github.com/lucide-icons/lucide"
|
<Link marginRight={8} fontSize="lg">
|
||||||
isExternal
|
{menuItem.name}
|
||||||
marginRight={6}
|
</Link>
|
||||||
fontSize="xl"
|
</NextLink>
|
||||||
>
|
);
|
||||||
Github
|
})}
|
||||||
</Link>
|
|
||||||
</>
|
</>
|
||||||
) : null}
|
) : null}
|
||||||
<IconButton
|
<IconButton
|
||||||
@@ -115,8 +117,12 @@ const Layout = ({ aside, children }: LayoutProps) => {
|
|||||||
</Flex>
|
</Flex>
|
||||||
</Flex>
|
</Flex>
|
||||||
<Flex>
|
<Flex>
|
||||||
{aside ? <Box as="aside" marginRight={{ base: 0, lg: -240, }}>{aside}</Box> : null}
|
{aside ? (
|
||||||
<Flex margin="0 auto" direction="column" maxW="1250px" px={5} width="100%">
|
<Box as="aside" marginRight={{ base: 0, lg: -240 }}>
|
||||||
|
{aside}
|
||||||
|
</Box>
|
||||||
|
) : null}
|
||||||
|
<Flex margin="0 auto" direction="column" width="100%">
|
||||||
{children}
|
{children}
|
||||||
<Divider mb={6} mt={12} />
|
<Divider mb={6} mt={12} />
|
||||||
<p style={{ alignSelf: 'center' }}>
|
<p style={{ alignSelf: 'center' }}>
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import {
|
|||||||
import NextLink from 'next/link';
|
import NextLink from 'next/link';
|
||||||
import { useRouter } from 'next/router';
|
import { useRouter } from 'next/router';
|
||||||
import { ReactNode, useEffect } from 'react';
|
import { ReactNode, useEffect } from 'react';
|
||||||
|
import menuItems from '../static/menuItems';
|
||||||
import Logo from './Logo';
|
import Logo from './Logo';
|
||||||
import { useMobileNavigationContext } from './MobileNavigationProvider';
|
import { useMobileNavigationContext } from './MobileNavigationProvider';
|
||||||
|
|
||||||
@@ -35,31 +36,30 @@ const MobileMenu = ({ children }: { children?: ReactNode }): JSX.Element => {
|
|||||||
</DrawerHeader>
|
</DrawerHeader>
|
||||||
<DrawerBody>
|
<DrawerBody>
|
||||||
<Box mb={4}>
|
<Box mb={4}>
|
||||||
<NextLink href="/docs" passHref>
|
{menuItems.map(menuItem => {
|
||||||
<Link fontSize="lg" fontWeight="bold" display="block" mb={2}>
|
if (menuItem.isExternal) {
|
||||||
Documentation
|
return (
|
||||||
</Link>
|
<Link
|
||||||
</NextLink>
|
href="https://github.com/lucide-icons/lucide"
|
||||||
<NextLink href="/packages" passHref>
|
isExternal
|
||||||
<Link marginRight={12} fontSize="lg" fontWeight="bold" display="block" mb={2}>
|
fontSize="lg"
|
||||||
Packages
|
fontWeight="bold"
|
||||||
</Link>
|
display="block"
|
||||||
</NextLink>
|
mb={2}
|
||||||
<NextLink href="/license" passHref>
|
key={menuItem.name}
|
||||||
<Link marginRight={12} fontSize="xl">
|
>
|
||||||
License
|
{menuItem.name}
|
||||||
</Link>
|
</Link>
|
||||||
</NextLink>
|
);
|
||||||
<Link
|
}
|
||||||
href="https://github.com/lucide-icons/lucide"
|
return (
|
||||||
isExternal
|
<NextLink href={menuItem.href} passHref>
|
||||||
fontSize="lg"
|
<Link fontSize="lg" fontWeight="bold" display="block" mb={2}>
|
||||||
fontWeight="bold"
|
{menuItem.name}
|
||||||
display="block"
|
</Link>
|
||||||
mb={2}
|
</NextLink>
|
||||||
>
|
);
|
||||||
Github
|
})}
|
||||||
</Link>
|
|
||||||
</Box>
|
</Box>
|
||||||
<Divider mt={2} />
|
<Divider mt={2} />
|
||||||
{children}
|
{children}
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import {
|
|||||||
InputGroup,
|
InputGroup,
|
||||||
InputLeftElement,
|
InputLeftElement,
|
||||||
useColorMode,
|
useColorMode,
|
||||||
useUpdateEffect
|
useUpdateEffect,
|
||||||
} from '@chakra-ui/react';
|
} from '@chakra-ui/react';
|
||||||
import { Search as SearchIcon } from 'lucide-react';
|
import { Search as SearchIcon } from 'lucide-react';
|
||||||
import React, { useEffect, useRef, useState } from 'react';
|
import React, { useEffect, useRef, useState } from 'react';
|
||||||
@@ -17,59 +17,57 @@ interface SearchInputProps {
|
|||||||
count: number;
|
count: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const SearchInput = (
|
export const SearchInput = ({ onChange, count }: SearchInputProps) => {
|
||||||
({ onChange, count }: SearchInputProps) => {
|
const { colorMode } = useColorMode();
|
||||||
const { colorMode } = useColorMode();
|
|
||||||
|
|
||||||
const [urlValue, setUrlValue] = useRouterParam('search');
|
const [urlValue, setUrlValue] = useRouterParam('search');
|
||||||
|
|
||||||
const [inputValue, setInputValue] = useState('');
|
const [inputValue, setInputValue] = useState('');
|
||||||
const debouncedValue = useDebounce(inputValue.trim(), 300);
|
const debouncedValue = useDebounce(inputValue.trim(), 300);
|
||||||
|
|
||||||
useUpdateEffect(() => {
|
useUpdateEffect(() => {
|
||||||
onChange(debouncedValue);
|
onChange(debouncedValue);
|
||||||
setUrlValue(debouncedValue);
|
setUrlValue(debouncedValue);
|
||||||
}, [debouncedValue]);
|
}, [debouncedValue]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (urlValue && !inputValue) {
|
if (urlValue && !inputValue) {
|
||||||
setInputValue(urlValue);
|
setInputValue(urlValue);
|
||||||
onChange(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 (
|
window.addEventListener('keydown', handleKeyDown);
|
||||||
<InputGroup position="sticky" top={4} zIndex={1}>
|
return () => window.removeEventListener('keydown', handleKeyDown);
|
||||||
<InputLeftElement
|
}, []);
|
||||||
children={
|
|
||||||
<Icon>
|
return (
|
||||||
<SearchIcon />
|
<InputGroup>
|
||||||
</Icon>
|
<InputLeftElement
|
||||||
}
|
children={
|
||||||
/>
|
<Icon>
|
||||||
<Input
|
<SearchIcon />
|
||||||
ref={ref}
|
</Icon>
|
||||||
placeholder={`Search ${count} icons (Press "/" to focus)`}
|
}
|
||||||
onChange={(event) => setInputValue(event.target.value)}
|
/>
|
||||||
value={inputValue}
|
<Input
|
||||||
bg={colorMode == 'light' ? theme.colors.white : theme.colors.gray[700]}
|
ref={ref}
|
||||||
/>
|
placeholder={`Search ${count} icons (Press "/" to focus)`}
|
||||||
</InputGroup>
|
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 NextCache from './nextCache';
|
||||||
import {parseSync} from 'svgson';
|
import {getAllData, GetDataOptions} from './icons';
|
||||||
import {getAllData} from './icons';
|
|
||||||
|
|
||||||
export type IconNode = [string, object, IconNode[]];
|
export type IconNode = [string, object, IconNode[]];
|
||||||
export type IconNodes = {[iconName: string]: 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 NextCache.resolve('api-icon-nodes', async () => {
|
||||||
return (await getAllData()).reduce((acc, icon) => {
|
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;
|
return acc;
|
||||||
}, {});
|
}, {});
|
||||||
}, writeCache);
|
}, 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 fs from "fs";
|
||||||
import path from "path";
|
import path from "path";
|
||||||
|
import { parseSync } from "svgson";
|
||||||
|
import { IconNode } from "../../../packages/lucide-react/src/createLucideIcon";
|
||||||
import { IconEntity } from "../types";
|
import { IconEntity } from "../types";
|
||||||
import { getContributors } from "./fetchAllContributors";
|
import { getContributors } from "./fetchAllContributors";
|
||||||
|
import { generateHashedKey } from "./helpers";
|
||||||
|
|
||||||
const directory = path.join(process.cwd(), "../icons");
|
const directory = path.join(process.cwd(), "../icons");
|
||||||
|
|
||||||
@@ -13,25 +16,42 @@ export function getAllNames() {
|
|||||||
.map((fileName) => path.basename(fileName, '.json'));
|
.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 svgPath = path.join(directory, `${name}.svg`);
|
||||||
const svgContent = fs.readFileSync(svgPath, "utf8");
|
const svgContent = fs.readFileSync(svgPath, "utf8");
|
||||||
const jsonPath = path.join(directory, `${name}.json`);
|
const jsonPath = path.join(directory, `${name}.json`);
|
||||||
const jsonContent = fs.readFileSync(jsonPath, "utf8");
|
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);
|
const contributors = await getContributors(name);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...iconJson,
|
|
||||||
name,
|
name,
|
||||||
|
tags,
|
||||||
|
categories,
|
||||||
contributors,
|
contributors,
|
||||||
src: svgContent
|
iconNode,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getAllData(): Promise<IconEntity[]> {
|
export async function getAllData(options?: GetDataOptions): Promise<IconEntity[]> {
|
||||||
const names = getAllNames();
|
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',
|
'800': '#A70B0B',
|
||||||
'900': '#720707'
|
'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';
|
import {fetchIconNodes} from '../../../lib/fetchIconNodes';
|
||||||
|
|
||||||
export default async function handler(req, res) {
|
export default async function handler(request: NextApiRequest, response: NextApiResponse) {
|
||||||
res.setHeader(
|
const params = request.query
|
||||||
'Cache-Control',
|
|
||||||
'public, max-age=86400'
|
return response
|
||||||
).status(200).json(await fetchIconNodes(false));
|
.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 { useRouter } from 'next/router';
|
||||||
import IconDetailOverlay from '../../components/IconDetailOverlay';
|
import IconDetailOverlay from '../../components/IconDetailOverlay';
|
||||||
import { getAllData, getData } from '../../lib/icons';
|
import { getAllData, getData } from '../../lib/icons';
|
||||||
import IconOverview from '../../components/IconOverview';
|
import IconOverview from '../../components/IconOverview';
|
||||||
import Layout from '../../components/Layout';
|
import Layout from '../../components/Layout';
|
||||||
import Header from '../../components/Header';
|
import { GetStaticPaths, GetStaticProps } from 'next';
|
||||||
import { useMemo } from 'react';
|
import { getAllCategories } from 'src/lib/categories';
|
||||||
|
|
||||||
const IconPage = ({ icon, data }) => {
|
const IconPage = ({ icon, data, categories }): JSX.Element => {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const getIcon = iconName => data.find(({ name }) => name === iconName) || {};
|
const getIcon = iconName => data.find(({ name }) => name === iconName) || {};
|
||||||
|
|
||||||
@@ -21,11 +22,11 @@ const IconPage = ({ icon, data }) => {
|
|||||||
|
|
||||||
router.push(
|
router.push(
|
||||||
{
|
{
|
||||||
pathname: '/',
|
pathname: '/icons',
|
||||||
query,
|
query,
|
||||||
},
|
},
|
||||||
undefined,
|
undefined,
|
||||||
{ scroll: false },
|
{ scroll: false, shallow: true },
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -39,21 +40,22 @@ const IconPage = ({ icon, data }) => {
|
|||||||
return (
|
return (
|
||||||
<Layout>
|
<Layout>
|
||||||
<IconDetailOverlay key={currentIcon.name} icon={currentIcon} close={onClose} open />
|
<IconDetailOverlay key={currentIcon.name} icon={currentIcon} close={onClose} open />
|
||||||
<Header {...{ data }} />
|
<IconOverview {...{ data, categories }} key="icon-overview" />
|
||||||
<IconOverview {...{ data }} />
|
|
||||||
</Layout>
|
</Layout>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default IconPage;
|
export default IconPage;
|
||||||
|
|
||||||
export async function getStaticProps({ params: { iconName } }) {
|
export const getStaticProps: GetStaticProps = async ({ params: { iconName } }) => {
|
||||||
const data = await getAllData();
|
const data = await getAllData({ withChildKeys: true });
|
||||||
const icon = await getData(iconName);
|
const icon = await getData(iconName as string, { withChildKeys: true });
|
||||||
return { props: { icon, data } };
|
const categories = await getAllCategories()
|
||||||
}
|
|
||||||
|
|
||||||
export async function getStaticPaths() {
|
return { props: { icon, data, categories } };
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getStaticPaths: GetStaticPaths = async () => {
|
||||||
const data = await getAllData();
|
const data = await getAllData();
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@@ -62,4 +64,4 @@ export async function getStaticPaths() {
|
|||||||
})),
|
})),
|
||||||
fallback: false,
|
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 MobileMenu from '../components/MobileMenu';
|
||||||
import { useMemo } from 'react';
|
import { useMemo } from 'react';
|
||||||
import { GetStaticPropsResult, NextPage } from 'next';
|
import { GetStaticPropsResult, NextPage } from 'next';
|
||||||
import { IconEntity } from '../types';
|
import { IconEntity, Category } from '../types';
|
||||||
|
import { getAllCategories } from 'src/lib/categories';
|
||||||
|
|
||||||
interface HomePageProps {
|
interface HomePageProps {
|
||||||
data: IconEntity[]
|
data: IconEntity[]
|
||||||
|
categories: Category[]
|
||||||
}
|
}
|
||||||
|
|
||||||
const HomePage: NextPage<HomePageProps> = ({ data }) => {
|
const HomePage: NextPage<HomePageProps> = ({ data, categories }) => {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const getIcon = iconName => data.find(({ name }) => name === iconName);
|
const getIcon = iconName => data.find(({ name }) => name === iconName);
|
||||||
|
|
||||||
@@ -28,20 +30,28 @@ const HomePage: NextPage<HomePageProps> = ({ data }) => {
|
|||||||
<IconDetailOverlay
|
<IconDetailOverlay
|
||||||
open={!!currentIcon?.name}
|
open={!!currentIcon?.name}
|
||||||
icon={currentIcon}
|
icon={currentIcon}
|
||||||
close={() => router.push('/', undefined, { shallow: true })}
|
close={() => router.push({
|
||||||
|
pathname: '/icon/[iconName]',
|
||||||
|
query: {
|
||||||
|
...router.query,
|
||||||
|
iconName: '',
|
||||||
|
},
|
||||||
|
}, undefined, { shallow: true })}
|
||||||
/>
|
/>
|
||||||
<Header {...{ data }} />
|
<Header {...{ data }} />
|
||||||
<IconOverview {...{ data }} />
|
<IconOverview {...{ data, categories }} />
|
||||||
</Layout>
|
</Layout>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export async function getStaticProps(): Promise<GetStaticPropsResult<HomePageProps>> {
|
export async function getStaticProps(): Promise<GetStaticPropsResult<HomePageProps>> {
|
||||||
const data = await getAllData();
|
const data = await getAllData({ withChildKeys: true });
|
||||||
|
const categories = await getAllCategories()
|
||||||
|
|
||||||
return {
|
return {
|
||||||
props: {
|
props: {
|
||||||
data,
|
data,
|
||||||
|
categories,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -48,7 +48,12 @@ export const getStaticProps: GetStaticProps = async () => {
|
|||||||
|
|
||||||
const licenseText = doc
|
const licenseText = doc
|
||||||
.split(/\n{2,}/)
|
.split(/\n{2,}/)
|
||||||
.map(paragraph => paragraph.split('\n').join(' ').trim())
|
.map(paragraph =>
|
||||||
|
paragraph
|
||||||
|
.split('\n')
|
||||||
|
.join(' ')
|
||||||
|
.trim(),
|
||||||
|
)
|
||||||
.filter(Boolean);
|
.filter(Boolean);
|
||||||
|
|
||||||
return { props: { licenseText } };
|
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 {
|
export interface IconEntity {
|
||||||
contributors: Contributor[];
|
|
||||||
name: string;
|
name: string;
|
||||||
src: string;
|
|
||||||
tags: string[];
|
tags: string[];
|
||||||
|
categories: string[];
|
||||||
|
contributors: Contributor[];
|
||||||
|
iconNode: IconNode;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Contributor {
|
export interface Contributor {
|
||||||
author: string;
|
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