Skip to content

Commit

Permalink
feat: upload custom svg
Browse files Browse the repository at this point in the history
  • Loading branch information
vwh committed Oct 6, 2024
1 parent 0cef9bc commit 1b0e96f
Show file tree
Hide file tree
Showing 5 changed files with 133 additions and 28 deletions.
94 changes: 73 additions & 21 deletions src/App.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { useMemo } from "react";
import { useMemo, useEffect, useState } from "react";
import { useStore } from "@/store/useStore";

import { cn } from "@/lib/utils";
Expand All @@ -11,9 +11,39 @@ import DotPattern from "@/components/magicui/dot-pattern";
import * as svgs from "lucide-react";

const App: React.FC = () => {
const { svgSettings, selectedSvgName } = useStore();
const { svgSettings, selectedSvgName, customSvg } = useStore();
const SvgComponent = svgs[selectedSvgName as Icons];

const [customSvgContent, setCustomSvgContent] = useState<string | null>(null);
const [customViewBox, setCustomViewBox] = useState<string | null>(null);

useEffect(() => {
if (customSvg) {
// Extract the SVG content from the base64 string
const svgContent = atob(customSvg.split(",")[1]);
setCustomSvgContent(svgContent);
// Extract viewBox
const viewBoxMatch = svgContent.match(/viewBox="([^"]+)"/);
if (viewBoxMatch && viewBoxMatch[1]) {
setCustomViewBox(viewBoxMatch[1]);
} else {
setCustomViewBox(null);
}
} else {
setCustomSvgContent(null);
setCustomViewBox(null);
}
}, [customSvg]);

const svgStyle = useMemo(
() => ({
position: "absolute" as const,
transform: `rotate(${svgSettings.rotation}deg) scale(${svgSettings.scale})`,
filter: `drop-shadow(${svgSettings.innerShadowX}px ${svgSettings.innerShadowY}px ${svgSettings.innerShadowBlur}px ${svgSettings.innerShadowColor})`
}),
[svgSettings]
);

const containerStyle = useMemo(
() => ({
width: "256px",
Expand All @@ -40,14 +70,25 @@ const App: React.FC = () => {
[svgSettings]
);

const svgStyle = useMemo(
() => ({
position: "absolute" as const,
transform: `rotate(${svgSettings.rotation}deg) scale(${svgSettings.scale})`,
filter: `drop-shadow(${svgSettings.innerShadowX}px ${svgSettings.innerShadowY}px ${svgSettings.innerShadowBlur}px ${svgSettings.innerShadowColor})`
}),
[svgSettings]
);
const customSvgStyle = useMemo(() => {
if (!customViewBox) return svgStyle;

const [, , vbWidth, vbHeight] = customViewBox.split(" ").map(Number);
const scale = Math.min(
svgSettings.size / vbWidth,
svgSettings.size / vbHeight
);

return {
...svgStyle,
transform: `${svgStyle.transform} scale(${scale})`,
fill: svgSettings.fillColor,
fillOpacity: svgSettings.fillOpacity,
stroke: svgSettings.svgColor,
strokeWidth: `${svgSettings.strokeWidth}px`,
opacity: svgSettings.opacity
};
}, [customViewBox, svgSettings, svgStyle]);

return (
<>
Expand All @@ -61,17 +102,28 @@ const App: React.FC = () => {
style={containerStyle}
>
<div style={svgWrapperStyle}>
<SvgComponent
fill={svgSettings.fillColor}
fillOpacity={svgSettings.fillOpacity}
stroke={svgSettings.svgColor}
strokeWidth={svgSettings.strokeWidth}
strokeLinecap="round"
strokeLinejoin="round"
width={svgSettings.size}
height={svgSettings.size}
style={svgStyle}
/>
{customSvgContent ? (
<svg
width={svgSettings.size}
height={svgSettings.size}
viewBox={customViewBox || undefined}
style={customSvgStyle}
dangerouslySetInnerHTML={{ __html: customSvgContent }}
preserveAspectRatio="xMidYMid meet"
/>
) : (
<SvgComponent
fill={svgSettings.fillColor}
fillOpacity={svgSettings.fillOpacity}
stroke={svgSettings.svgColor}
strokeWidth={svgSettings.strokeWidth}
strokeLinecap="round"
strokeLinejoin="round"
width={svgSettings.size}
height={svgSettings.size}
style={svgStyle}
/>
)}
</div>
</div>
<DotPattern
Expand Down
40 changes: 38 additions & 2 deletions src/components/editing-section.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { useState } from "react";
import { useRef, useState } from "react";
import { useStore } from "@/store/useStore";

import { Slider } from "@/components/ui/slider";
Expand All @@ -7,6 +7,8 @@ import { HexColorPicker } from "react-colorful";
import IconsDialog from "./icons-dialog";
import ColorPicker from "./color-picker";

import { UploadIcon } from "lucide-react";

type ControlProps = {
label: string;
};
Expand Down Expand Up @@ -79,11 +81,44 @@ export default function EditingSection() {
}

function IconControlGroup() {
const { svgSettings, updateSvgSetting } = useStore();
const { svgSettings, updateSvgSetting, setCustomSvg } = useStore();

const inputRef = useRef<HTMLInputElement>(null);

function handleUploadIcon(e: React.ChangeEvent<HTMLInputElement>) {
const file = e.target.files?.[0];
if (!file) return;

const reader = new FileReader();
reader.readAsDataURL(file);
reader.onload = () => {
const base64 = reader.result as string;
setCustomSvg(base64);
if (inputRef.current) {
inputRef.current.value = "";
}
};
}

return (
<ControlGroup label="Icon Customization">
<div className="mt-2">
<IconsDialog />
<Button
onClick={() => inputRef.current?.click()}
className="mt-1 flex h-12 w-full items-center justify-center gap-2"
>
<UploadIcon />
<span>Upload icon</span>
</Button>
<input
ref={inputRef}
type="file"
accept="image/svg+xml"
onChange={handleUploadIcon}
className="hidden"
style={{ display: "none" }}
/>
</div>
<ControlSlider
label="Icon Opacity"
Expand Down Expand Up @@ -270,6 +305,7 @@ function BackgroundControlGroup() {
<ColorPicker
onChange={(color) => updateSvgSetting("bgColor", color)}
value={svgSettings.bgColor}
displayColorOnly={false}
/>
</div>
<ControlSlider
Expand Down
7 changes: 5 additions & 2 deletions src/components/icons-dialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ import * as svgs from "lucide-react";
const ICONS_PER_PAGE = 35;

export default function IconsDialog() {
const { setSelectedSvgName, selectedSvgName } = useStore();
const { setSelectedSvgName, selectedSvgName, setCustomSvg } = useStore();
const [currentPage, setCurrentPage] = useState(0);
const [searchTerm, setSearchTerm] = useState("");

Expand All @@ -46,7 +46,10 @@ export default function IconsDialog() {
<DialogClose>
<Button
aria-label={`Select ${name} icon`}
onClick={() => setSelectedSvgName(name as Icons)}
onClick={() => {
setCustomSvg(null);
setSelectedSvgName(name as Icons);
}}
className="flex h-14 w-14 items-center justify-center rounded-lg transition-all hover:opacity-80 md:h-16 md:w-16"
>
<SvgComponent className="h-full w-full" />
Expand Down
11 changes: 9 additions & 2 deletions src/components/navbar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -86,11 +86,17 @@ const VariationButton: React.FC<VariationButtonProps> = React.memo(
);

const Navbar: React.FC = () => {
const { selectedSvgName, setSelectedSvgName, setSvgSettings, svgSettings } =
useStore();
const {
selectedSvgName,
setSelectedSvgName,
setSvgSettings,
svgSettings,
setCustomSvg
} = useStore();
const SvgComponent = svgs[selectedSvgName as Icons];

const handleVariationClick = (svgSettings: SvgSettings) => {
setCustomSvg(null);
setSvgSettings({
...svgSettings,
size: 190,
Expand All @@ -99,6 +105,7 @@ const Navbar: React.FC = () => {
};

const handleRandomClick = () => {
setCustomSvg(null);
setSelectedSvgName(randomIconName());
setSvgSettings({
...svgSettings,
Expand Down
9 changes: 8 additions & 1 deletion src/store/useStore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,14 @@ import type { Icons, SvgSettings } from "@/types";
interface State {
svgSettings: SvgSettings;
selectedSvgName: Icons;
customSvg: string | null;
}

interface Actions {
setSvgSettings: (svgSettings: SvgSettings) => void;
updateSvgSetting: (key: string, value: unknown) => void;
setSelectedSvgName: (name: Icons) => void;
setCustomSvg: (customSvg: string | null) => void;
}

export const defaultSvgSettings: SvgSettings = {
Expand Down Expand Up @@ -40,7 +42,8 @@ export const defaultSvgSettings: SvgSettings = {

const initialState: State = {
selectedSvgName: "Paintbrush",
svgSettings: defaultSvgSettings
svgSettings: defaultSvgSettings,
customSvg: null
};

export const useStore = create<State & Actions>((set) => ({
Expand All @@ -59,5 +62,9 @@ export const useStore = create<State & Actions>((set) => ({

setSelectedSvgName: (name: Icons) => {
set({ selectedSvgName: name });
},

setCustomSvg: (customSvg: string | null) => {
set({ customSvg });
}
}));

0 comments on commit 1b0e96f

Please sign in to comment.