first commit
Some checks failed
Build / run (push) Has been cancelled

This commit is contained in:
maher
2025-10-29 11:42:25 +01:00
commit 703f50a09d
4595 changed files with 385164 additions and 0 deletions

View 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',
};

View 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>
);
}

View 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);
}}
/>
);
}

View 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>
);
}