5
common/resources/client/ui/icon-picker/icon-grid-style.ts
Executable file
5
common/resources/client/ui/icon-picker/icon-grid-style.ts
Executable file
@@ -0,0 +1,5 @@
|
||||
export const iconGridStyle = {
|
||||
grid: 'flex flex-wrap gap-24',
|
||||
button:
|
||||
'flex flex-col items-center rounded hover:bg-hover h-90 aspect-square',
|
||||
};
|
||||
100
common/resources/client/ui/icon-picker/icon-list.tsx
Executable file
100
common/resources/client/ui/icon-picker/icon-list.tsx
Executable file
@@ -0,0 +1,100 @@
|
||||
import React, {ComponentType, Fragment} from 'react';
|
||||
import * as Icons from '../../icons/material/all-icons';
|
||||
import {ButtonBase} from '../buttons/button-base';
|
||||
import {SvgIconProps} from '../../icons/svg-icon';
|
||||
import {elementToTree, IconTree} from '../../icons/create-svg-icon';
|
||||
import {iconGridStyle} from './icon-grid-style';
|
||||
import {useFilter} from '../../i18n/use-filter';
|
||||
import clsx from 'clsx';
|
||||
import {Trans} from '../../i18n/trans';
|
||||
import {YoutubeIcon} from '@common/icons/social/youtube';
|
||||
import {AmazonIcon} from '@common/icons/social/amazon';
|
||||
import {AppleIcon} from '@common/icons/social/apple';
|
||||
import {BandcampIcon} from '@common/icons/social/bandcamp';
|
||||
import {EnvatoIcon} from '@common/icons/social/envato';
|
||||
import {FacebookIcon} from '@common/icons/social/facebook';
|
||||
import {InstagramIcon} from '@common/icons/social/instagram';
|
||||
import {LinkedinIcon} from '@common/icons/social/linkedin';
|
||||
import {PatreonIcon} from '@common/icons/social/patreon';
|
||||
import {PinterestIcon} from '@common/icons/social/pinterest';
|
||||
import {SnapchatIcon} from '@common/icons/social/snapchat';
|
||||
import {SoundcloudIcon} from '@common/icons/social/soundcloud';
|
||||
import {SpotifyIcon} from '@common/icons/social/spotify';
|
||||
import {TelegramIcon} from '@common/icons/social/telegram';
|
||||
import {TiktokIcon} from '@common/icons/social/tiktok';
|
||||
import {TwitchIcon} from '@common/icons/social/twitch';
|
||||
import {TwitterIcon} from '@common/icons/social/twitter';
|
||||
import {WhatsappIcon} from '@common/icons/social/whatsapp';
|
||||
|
||||
const socialIcons: [string, ComponentType<SvgIconProps>][] = [
|
||||
['amazon', AmazonIcon],
|
||||
['apple', AppleIcon],
|
||||
['bandcamp', BandcampIcon],
|
||||
['envato', EnvatoIcon],
|
||||
['facebook', FacebookIcon],
|
||||
['instagram', InstagramIcon],
|
||||
['linkedin', LinkedinIcon],
|
||||
['patreon', PatreonIcon],
|
||||
['pinterest', PinterestIcon],
|
||||
['snapchat', SnapchatIcon],
|
||||
['soundcloud', SoundcloudIcon],
|
||||
['spotify', SpotifyIcon],
|
||||
['telegram', TelegramIcon],
|
||||
['tiktok', TiktokIcon],
|
||||
['twitch', TwitchIcon],
|
||||
['twitter', TwitterIcon],
|
||||
['whatsapp', WhatsappIcon],
|
||||
['youtube', YoutubeIcon],
|
||||
];
|
||||
const entries = Object.entries(Icons)
|
||||
.map(([name, cmp]) => {
|
||||
const prettyName = name
|
||||
.replace('Icon', '')
|
||||
.replace(/[A-Z]/g, letter => ` ${letter.toLowerCase()}`);
|
||||
return [prettyName, cmp] as [string, ComponentType<SvgIconProps>];
|
||||
})
|
||||
.concat(socialIcons);
|
||||
|
||||
interface IconListProps {
|
||||
onIconSelected: (icon: IconTree[] | null) => void;
|
||||
searchQuery: string;
|
||||
}
|
||||
export default function IconList({onIconSelected, searchQuery}: IconListProps) {
|
||||
const {contains} = useFilter({
|
||||
sensitivity: 'base',
|
||||
});
|
||||
const matchedEntries = entries.filter(([name]) =>
|
||||
contains(name, searchQuery)
|
||||
);
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
<ButtonBase
|
||||
className={clsx(iconGridStyle.button, 'diagonal-lines')}
|
||||
onClick={e => {
|
||||
onIconSelected(null);
|
||||
}}
|
||||
>
|
||||
<Trans message="None" />
|
||||
</ButtonBase>
|
||||
{matchedEntries.map(([name, Icon]) => (
|
||||
<ButtonBase
|
||||
key={name}
|
||||
className={iconGridStyle.button}
|
||||
onClick={e => {
|
||||
const svgTree = elementToTree(
|
||||
e.currentTarget.querySelector('svg') as SVGElement
|
||||
);
|
||||
// only emit svg children, and not svg tag itself
|
||||
onIconSelected(svgTree.child as IconTree[]);
|
||||
}}
|
||||
>
|
||||
<Icon className="block text-muted icon-lg" />
|
||||
<span className="mt-16 block whitespace-normal text-xs capitalize">
|
||||
{name}
|
||||
</span>
|
||||
</ButtonBase>
|
||||
))}
|
||||
</Fragment>
|
||||
);
|
||||
}
|
||||
31
common/resources/client/ui/icon-picker/icon-picker-dialog.tsx
Executable file
31
common/resources/client/ui/icon-picker/icon-picker-dialog.tsx
Executable file
@@ -0,0 +1,31 @@
|
||||
import React from 'react';
|
||||
import IconPicker from './icon-picker';
|
||||
import {useDialogContext} from '../overlays/dialog/dialog-context';
|
||||
import {Dialog} from '../overlays/dialog/dialog';
|
||||
import {DialogHeader} from '../overlays/dialog/dialog-header';
|
||||
import {DialogBody} from '../overlays/dialog/dialog-body';
|
||||
import {Trans} from '../../i18n/trans';
|
||||
|
||||
export function IconPickerDialog() {
|
||||
return (
|
||||
<Dialog size="w-850" className="min-h-dialog">
|
||||
<DialogHeader>
|
||||
<Trans message="Select icon" />
|
||||
</DialogHeader>
|
||||
<DialogBody>
|
||||
<IconPickerWrapper />
|
||||
</DialogBody>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
|
||||
function IconPickerWrapper() {
|
||||
const {close} = useDialogContext();
|
||||
return (
|
||||
<IconPicker
|
||||
onIconSelected={value => {
|
||||
close(value);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
50
common/resources/client/ui/icon-picker/icon-picker.tsx
Executable file
50
common/resources/client/ui/icon-picker/icon-picker.tsx
Executable file
@@ -0,0 +1,50 @@
|
||||
import React, {Suspense} from 'react';
|
||||
import {IconTree} from '../../icons/create-svg-icon';
|
||||
import {iconGridStyle} from './icon-grid-style';
|
||||
import {TextField} from '../forms/input-field/text-field/text-field';
|
||||
import {Skeleton} from '../skeleton/skeleton';
|
||||
import {useTrans} from '../../i18n/use-trans';
|
||||
import {AnimatePresence, m} from 'framer-motion';
|
||||
import {opacityAnimation} from '../animation/opacity-animation';
|
||||
|
||||
const skeletons = [...Array(60).keys()];
|
||||
|
||||
const IconList = React.lazy(() => import('./icon-list'));
|
||||
|
||||
interface IconListProps {
|
||||
onIconSelected: (icon: IconTree[] | null) => void;
|
||||
}
|
||||
export default function IconPicker({onIconSelected}: IconListProps) {
|
||||
const {trans} = useTrans();
|
||||
const [value, setValue] = React.useState('');
|
||||
|
||||
return (
|
||||
<div className="py-4">
|
||||
<TextField
|
||||
className="mb-20"
|
||||
value={value}
|
||||
onChange={e => {
|
||||
setValue(e.target.value);
|
||||
}}
|
||||
placeholder={trans({message: 'Search icons...'})}
|
||||
/>
|
||||
<AnimatePresence mode="wait">
|
||||
<Suspense
|
||||
fallback={
|
||||
<m.div {...opacityAnimation} className={iconGridStyle.grid}>
|
||||
{skeletons.map((_, index) => (
|
||||
<div className={iconGridStyle.button} key={index}>
|
||||
<Skeleton variant="rect" />
|
||||
</div>
|
||||
))}
|
||||
</m.div>
|
||||
}
|
||||
>
|
||||
<m.div {...opacityAnimation} className={iconGridStyle.grid}>
|
||||
<IconList searchQuery={value} onIconSelected={onIconSelected} />
|
||||
</m.div>
|
||||
</Suspense>
|
||||
</AnimatePresence>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user