In this tutorial, we’ll develop a basic app for creating logos using Next.js, shadcn/ui, html2canvas-pro, and Lucide React icons. We’ll assume you have a basic understanding of Next.js and shadcn/ui components, as detailed explanations can be found in their respective documentation. Our focus will be on implementing the core features of the logo builder app. Setting Up We will create a new Next.js project with TypeScript and /src folder and Shadcn/ui, you can use the configuration of your choice. pnpm create next-app@latest my-app --typescript --tailwind --eslint We install the dependencies that we will need in the project: pnpm install html2canvas-pro react-best-gradient-color-picker lucide-react html2canvas-pro: to take “screenshots” of webpages or parts of it. react-best-gradient-color-picker: to use color and gradient picker for React.js lucide-react: a list of icons. Now, let's install Shadcn/ui components: pnpm dlx shadcn-ui@latest add button dialog resizable scroll-area slider tooltip After that, we will create the following structure for the project: ... └── 📁src └── 📁app └── favicon.ico └── globals.css └── layout.tsx └── page.tsx └── 📁components └── 📁background-controller └── color-picker.tsx └── index.tsx └── edit-panel.tsx └── header.tsx └── 📁icon-controller └── color-picker.tsx └── custom-icon.tsx └── icon-list.tsx └── icons-modal.tsx └── index.tsx └── mobile-nav.tsx └── preview.tsx └── side-nav.tsx └── 📁ui └── button.tsx └── dialog.tsx └── resizable.tsx └── scroll-area.tsx └── slider.tsx └── tooltip.tsx └── 📁constants └── icons.ts └── 📁context └── index.tsx └── 📁lib └── utils.ts └── 📁styles └── patterns.module.css └── types.d.tsx ... Building the App Creating the context First, let’s create a context to manage the state related to icon properties and the background. types.d.ts : export interface ContextApp { icon: string iconSize: number iconColor: string iconBorderWidth: number iconRotate: number bgRounded: number bgPadding: number bgColor: string } context/index.ts : 'use client' import { ReactNode, createContext, useContext, useEffect, useState } from 'react' // Import necessary dependencies from React // Import the ContextApp type from the types file import { ContextApp } from '@/types.d' // Initial state with default values const initialState = { icon: 'Shell', iconSize: 420, iconColor: '#000000', iconBorderWidth: 1, iconRotate: 0, bgRounded: 250, bgPadding: 0, bgColor: '#f59e0b', } // Create the context with an empty object and the setValues function typed const AppContext = createContext({} as { values: ContextApp; setValues: (values: ContextApp) => void }) // Context provider component export function AppWrapper({ children }: { children: ReactNode }) { // State to store the context values const [values, setValues] = useState({} as ContextApp) useEffect(() => { // Try to get saved values from localStorage and set them to state const saved = localStorage.getItem('logobuilder') if (saved) { setValues(JSON.parse(saved)) } else { setValues(initialState) // If no saved values, set the initial state } }, []) // Provide the values and setValues function to the children components return <AppContext.Provider value={{ values, setValues }}>{children}</AppContext.Provider> } // Custom hook to use the app context export function useAppContext() { return useContext(AppContext) } This component establishes a global context using React’s Context API to manage state related to icon properties and background settings. User configurations are saved in localStorage to persist their progress. Building the layout app/page.tsx : import { cookies } from 'next/headers' // Import cookies from Next.js headers ... export default function HomePage() { // Retrieve the layout and collapsed state from cookies const layout = cookies().get('react-resizable-panels:layout') const collapsed = cookies().get('react-resizable-panels:collapsed') // Parse the layout and collapsed values from cookies, default to undefined if not present const defaultLayout = layout ? JSON.parse(layout.value) : undefined const defaultCollapsed = collapsed ? JSON.parse(collapsed.value) : undefined return ( <div className='flex flex-col'> <EditPanel defaultLayout={defaultLayout} defaultCollapsed={defaultCollapsed} navCollapsedSize={4} /> </div> ) } The HomePage component utilizes cookies to persist the layout state and collapsed state of the resizable panels in the EditPanel. The component retrieves cookies for the layout (react-resizable-panels:layout) and the collapsed state (react-resizable-panels:collapsed) using cookies().get() from Next.js. components/edit-panel.tsx : 'use client' /* all imports */ interface Props { defaultLayout: number[] | undefined defaultCollapsed?: boolean navCollapsedSize: number } export default function EditPanel({ defaultLayout = [20, 80], defaultCollapsed = false, navCollapsedSize }: Props) { // State to track if the side navigation is collapsed const [isCollapsed, setIsCollapsed] = useState(defaultCollapsed) // State to track the currently selected option (Icon or Background) const [selectedOption, setSelectedOption] = useState('Icon') // Access global context values const { values } = useAppContext() // Effect to save the current values to localStorage whenever they change useEffect(() => { if (Object.keys(values).length) { localStorage.setItem('logobuilder', JSON.stringify({ ...values })) } }, [values]) return ( <div className='hidden md:block'> <TooltipProvider delayDuration={0}> <ResizablePanelGroup direction='horizontal' onLayout={(sizes: number[]) => { // Save layout sizes to cookies for persistence document.cookie = `react-resizable-panels:layout=${JSON.stringify(sizes)}` }} className='h-screen fixed items-stretch' > <ResizablePanel defaultSize={defaultLayout[0]} collapsedSize={navCollapsedSize} collapsible={true} minSize={8} maxSize={12} onCollapse={() => { setIsCollapsed(true) // Save collapsed state to cookies for persistence document.cookie = `react-resizable-panels:collapsed=${JSON.stringify(true)}` }} onExpand={() => { setIsCollapsed(false) // Save expanded state to cookies for persistence document.cookie = `react-resizable-panels:collapsed=${JSON.stringify(false)}` }} className={cn(isCollapsed && 'min-w-[72px] transition-all duration-300 ease-in-out')} > <SideNav isCollapsed={isCollapsed} setSelectedOption={setSelectedOption} options={[ { id: 1, title: 'Icon', icon: PencilRuler, }, { id: 2, title: 'Background', icon: Image, }, ]} /> </ResizablePanel> <ResizableHandle withHandle className='border-[1px]' /> <ResizablePanel defaultSize={defaultLayout[1]}> <main className='w-full flex flex-col md:flex-row'> {selectedOption === 'Icon' && <IconController />} {selectedOption === 'Background' && <BackgroundController />} <Preview /> </main> </ResizablePanel> </ResizablePanelGroup> </TooltipProvider> </div> ) } The EditPanel uses the ResizablePanel and ResizablePanelGroup components from shadcn/ui to create resizable and collapsible panels. When the layout or collapsed state changes, these new states are saved back to cookies, ensuring persistence across page reloads and sessions. Icon controller components/icon-controller.tsx export default function IconControls() { const { values, setValues } = useAppContext() // FallbackSlider component to be used when values are not loaded yet const FallbackSlider = () => { return ( <Slider defaultValue={[0]} min={40} max={500} step={1} /> ) } return ( <ScrollArea className='h-[calc(100vh-80px)]'> <section className='p-4 w-full md:w-[340px] border-r-2 space-y-6'> <h2 className='font-semibold tracking-tight text-xl'>Customize your Icon</h2> {/* Icon Selection */} <div className='space-y-2'> <div className='w-full flex justify-between gap-4'> <label className='font-semibold leading-none tracking-tight'>Icon</label> <span className='text-sm text-muted-foreground'>{values.icon}</span> </div> <IconsModal /> </div> {/* Icon Size Control */} <div className='space-y-2'> <div className='w-full flex justify-between gap-4'> <label className='font-semibold leading-none tracking-tight'>Size</label> <span className='text-sm text-muted-foreground'>{values.iconSize} px</span> </div> {Object.keys(values).length ? ( <Slider defaultValue={[values.iconSize]} min={40} max={500} step={1} onValueChange={(value: any[]) => setValues({ ...values, iconSize: value[0] })} /> ) : ( <FallbackSlider /> )} </div> {/* Icon Border Width Control */} <div className='space-y-2'> <div className='w-full flex justify-between gap-4'> <label className='font-semibold leading-none tracking-tight'>Border Width</label> <span className='text-sm text-muted-foreground'>{values.iconBorderWidth} px</span> </div> {Object.keys(values).length ? ( <Slider defaultValue={[values.iconBorderWidth]} min={1} max={4} step={1} onValueChange={(value: any[]) => setValues({ ...values, iconBorderWidth: value[0] })} /> ) : ( <FallbackSlider /> )} </div> {/* Icon Rotation Control */} <div className='space-y-2'> <div className='w-full flex justify-between gap-4'> <label className='font-semibold leading-none tracking-tight'>Rotate</label> <span className='text-sm text-muted-foreground'>{values.iconRotate} °</span> </div> {Object.keys(values).length ? ( <Slider defaultValue={[values.iconRotate]} min={-180} max={180} step={1} onValueChange={(value: any[]) => setValues({ ...values, iconRotate: value[0] })} /> ) : ( <FallbackSlider /> )} </div> {/* Icon Color Control */} <div className='space-y-2'> <div className='w-full flex justify-between gap-4'> <label className='font-semibold leading-none tracking-tight'>Color</label> </div> <ColorPickerController /> </div> </section> </ScrollArea> ) } The IconControls component provides a user interface for customizing various properties of an icon. It uses a combination of sliders, modals, and color pickers to allow users to adjust the icon's size, border width, rotation, and color. The component also leverages a global context to manage and persist these customization values. icon-controller/icon-list.tsx : /* all imports */ import { iconNames } from '@/constants/icons' interface Props { setOpenDialog: (value: boolean) => void } export default function IconList({ setOpenDialog }: Props) { const [visibleIcons, setVisibleIcons] = useState<string[]>([]) // Reference for the IntersectionObserver const observer = useRef<IntersectionObserver | null>(null) const { values, setValues } = useAppContext() // Callback for observing the last icon element const lastIconElementRef = useCallback( (node: HTMLElement | null) => { // Disconnect any existing observer if (observer.current) observer.current.disconnect() // Create a new IntersectionObserver observer.current = new IntersectionObserver(entries => { // If the last icon is visible and there are more icons to load, update the visible icons if (entries[0].isIntersecting && visibleIcons.length < iconNames.length) { setVisibleIcons(prevIcons => [ ...prevIcons, ...iconNames.slice(prevIcons.length, prevIcons.length + 20) ]) } }) // Observe the node if it exists if (node) observer.current.observe(node) }, [visibleIcons.length, iconNames, setVisibleIcons] ) useEffect(() => { // Load initial set of icons if (iconNames.length) setVisibleIcons(iconNames.slice(0, 20)) }, [iconNames]) return ( <div className='grid grid-cols-6 gap-4 py-4'> {visibleIcons.map((icon, index) => { if (visibleIcons.length === index + 1) { return ( <div ref={lastIconElementRef} key={icon} > <Button title={icon} aria-label={icon} variant='ghost' className='h-9 w-9 p-1 border shadow-lg' onClick={() => { setValues({ ...values, icon }) setOpenDialog(false) }} > <CustomIcon name={icon} /> </Button> </div> ) } else { // Render button without observer reference return ( <Button key={icon} title={icon} aria-label={icon} variant='ghost' className='h-9 w-9 p-1 border shadow-lg' onClick={() => { setValues({ ...values, icon }) setOpenDialog(false) }} > <CustomIcon name={icon} /> </Button> ) } })} </div> ) } This component, IconList, allows users to select an icon from a list and load more icons as they scroll. It uses context to manage and update the selected icon and employs an Intersection Observer to load more icons when the user reaches the end of the list. To display all the icons, I extracted their names directly from the Lucide React library and created the following component to render each icon. icon-controller/custom-icon.tsx : import { icons } from 'lucide-react' interface Props { name: string color?: string size?: number strokeWidth?: number } export default function CustomIcon({ name, color = 'currentColor', size = 24, strokeWidth = 1 }: Props) { const LucideIcon = icons[name as keyof typeof icons] if (!LucideIcon) return null return ( <LucideIcon size={size} color={color} strokeWidth={strokeWidth} /> ) } The customIcon component allows you to render a specific icon from the lucide-react library with customizable properties such as name, color, size, and stroke width. Background controller background-controller/index.tsx : /* all imports */ export default function BackgroundController() { // Use the context to get and set background values const { values, setValues } = useAppContext() return ( <ScrollArea className='h-[calc(100vh-80px)]'> <section className='p-4 w-full md:w-[340px] border-r-2 space-y-6'> <h2 className='font-semibold tracking-tight text-xl'>Customize your Background</h2> {/* Rounded corner slider */} <div className='space-y-2'> <div className='w-full flex justify-between gap-4'> <label className='font-semibold leading-none tracking-tight'>Rounded</label> <span className='text-sm text-muted-foreground'>{values.bgRounded} px</span> </div> {/* Slider to adjust the rounded corners of the background */} <Slider defaultValue={[values.bgRounded]} min={0} max={250} step={1} onValueChange={(value: any[]) => setValues({ ...values, bgRounded: value[0] })} /> </div> {/* Padding slider */} <div className='space-y-2'> <div className='w-full flex justify-between gap-4'> <label className='font-semibold leading-none tracking-tight'>Padding</label> <span className='text-sm text-muted-foreground'>{values.bgPadding} px</span> </div> {/* Slider to adjust the padding of the background */} <Slider defaultValue={[values.bgPadding]} min={0} max={100} step={1} onValueChange={(value: any[]) => setValues({ ...values, bgPadding: value[0] })} /> </div> {/* Color picker */} <div className='space-y-2'> <div className='w-full flex justify-between gap-4'> <label className='font-semibold leading-none tracking-tight'>Color</label> </div> {/* Component to pick a background color */} <ColorPickerController hidecontrols={false} hidepresets={false} /> </div> </section> </ScrollArea> ) } The BackgroundController component allows users to customize background properties, including rounded corners, padding, and color. Sliders are used to adjust these properties, and a color picker is provided for background color. background-controller/color-picker.tsx : /* all imports */ interface Props { hidecontrols?: boolean hidepresets?: boolean } export default function ColorPickerController({ hidecontrols = true, hidepresets = true }: Props) { // Use context to get and set the background color value const { values, setValues } = useAppContext() const [color, setColor] = useState(values.bgColor) // Update context value whenever the local color state changes useEffect(() => { setValues({ ...values, bgColor: color }) }, [color]) return ( // Render the color picker component with current color value and change handler <ColorPicker value={color} onChange={setColor} hideControls={hidecontrols} // Optional prop to hide/show controls hidePresets={hidepresets} // Optional prop to hide/show presets /> ) } Preview Section components/preview.tsx : /* all imports */ export default function Preview() { const { values } = useAppContext() const { icon, iconSize, iconColor, iconBorderWidth, iconRotate, bgColor, bgRounded, bgPadding } = values const divRef = useRef<HTMLDivElement | null>(null) // Function to handle the download of the image const handleDownloadImage = () => { const downloadArea = divRef.current // Use html2canvas to capture the screenshot of the referenced div html2canvas(downloadArea as HTMLDivElement, { backgroundColor: null, }).then(canvas => { const pngImage = canvas.toDataURL('image/png') // Convert the canvas to a PNG image const downloadLink = document.createElement('a') // Create a new anchor element downloadLink.href = pngImage // Set the href to the PNG image URL downloadLink.download = 'logobuilder.png' // Set the download attribute to the desired file name downloadLink.click() // Programmatically click the anchor element to trigger the download }) } return ( <div className={`flex-1 p-4 ${Styles.patternDotsMd}`}> {/* Download button section */} <div className='w-full flex justify-end'> <Button size={'sm'} className='flex gap-2 items-center' onClick={() => handleDownloadImage()} > <Download strokeWidth={2} className='w-4 h-4' /> Download </Button> </div> {/* Preview section */} <div className='grid place-content-center h-[calc(100%-52px)]'> <div className='bg-white border-2 hover:border-gray-300'> <div ref={divRef} // Attach the ref to this div id='download-area' className='h-[500px] w-[500px]' style={{ padding: `${bgPadding}px`, }} > {/* Conditional rendering based on whether values are populated */} {!Object.keys(values).length ? ( <div className='animate-pulse flex justify-center items-center h-full w-full bg-gray-200'> <div className='animate-pulse h-4/5 w-4/5 rounded-full bg-gray-300' /> </div> ) : ( <div className='w-full h-full grid place-content-center' style={{ background: bgColor, borderRadius: `${bgRounded}px`, padding: `${bgPadding}px`, }} > <span style={{ transform: `rotate(${iconRotate}deg)` }}> <CustomIcon name={icon} size={iconSize} color={iconColor} strokeWidth={iconBorderWidth} /> </span> </div> )} </div> </div> </div> </div> ) } The Preview component displays a customizable icon within a styled area and allows downloading the preview as an image. It uses useRef to reference the DOM element of the preview area and html2canvas to capture an image of this area. The handleDownloadImage function converts the captured content into a PNG image and downloads the file when the download button is clicked. That’s it! 🎉 💎**Demo here** 🧑💻 Repo here Conclusion You’ve now built the basic features of a logo builder application. From here, you can expand and enhance the app with features like: Adding multiple icons Including image uploads Providing pre-built logo templates Adding text elements Allowing draggable elements within the canvas Adding premium features Have fun improving and adapting this project to suit your needs! Reference: https://www.youtube.com/watch?v=XdsZA8wSNDg&pp=ygUObG9nbyBtYWtlciBhcHA%3D Read more: Enhancing Password Security and Recovery with Next.js 14 and NextAuth.js Want to connect with the Author? Love connecting with friends all around the world on 𝕏. In this tutorial, we’ll develop a basic app for creating logos using Next.js, shadcn/ui, html2canvas-pro, and Lucide React icons. We’ll assume you have a basic understanding of Next.js and shadcn/ui components, as detailed explanations can be found in their respective documentation. Our focus will be on implementing the core features of the logo builder app. Setting Up We will create a new Next.js project with TypeScript and /src folder and Shadcn/ui, you can use the configuration of your choice. /src pnpm create next-app@latest my-app --typescript --tailwind --eslint pnpm create next-app@latest my-app --typescript --tailwind --eslint We install the dependencies that we will need in the project: pnpm install html2canvas-pro react-best-gradient-color-picker lucide-react pnpm install html2canvas-pro react-best-gradient-color-picker lucide-react html2canvas-pro: to take “screenshots” of webpages or parts of it. react-best-gradient-color-picker: to use color and gradient picker for React.js lucide-react: a list of icons. html2canvas-pro: to take “screenshots” of webpages or parts of it. html2canvas-pro : html2canvas-pro html2canvas-pro react-best-gradient-color-picker: to use color and gradient picker for React.js react-best-gradient-color-picker : react-best-gradient-color-picker react-best-gradient-color-picker lucide-react: a list of icons. lucide-react : lucide-react lucide-react Now, let's install Shadcn/ui components: pnpm dlx shadcn-ui@latest add button dialog resizable scroll-area slider tooltip pnpm dlx shadcn-ui@latest add button dialog resizable scroll-area slider tooltip After that, we will create the following structure for the project: ... └── 📁src └── 📁app └── favicon.ico └── globals.css └── layout.tsx └── page.tsx └── 📁components └── 📁background-controller └── color-picker.tsx └── index.tsx └── edit-panel.tsx └── header.tsx └── 📁icon-controller └── color-picker.tsx └── custom-icon.tsx └── icon-list.tsx └── icons-modal.tsx └── index.tsx └── mobile-nav.tsx └── preview.tsx └── side-nav.tsx └── 📁ui └── button.tsx └── dialog.tsx └── resizable.tsx └── scroll-area.tsx └── slider.tsx └── tooltip.tsx └── 📁constants └── icons.ts └── 📁context └── index.tsx └── 📁lib └── utils.ts └── 📁styles └── patterns.module.css └── types.d.tsx ... ... └── 📁src └── 📁app └── favicon.ico └── globals.css └── layout.tsx └── page.tsx └── 📁components └── 📁background-controller └── color-picker.tsx └── index.tsx └── edit-panel.tsx └── header.tsx └── 📁icon-controller └── color-picker.tsx └── custom-icon.tsx └── icon-list.tsx └── icons-modal.tsx └── index.tsx └── mobile-nav.tsx └── preview.tsx └── side-nav.tsx └── 📁ui └── button.tsx └── dialog.tsx └── resizable.tsx └── scroll-area.tsx └── slider.tsx └── tooltip.tsx └── 📁constants └── icons.ts └── 📁context └── index.tsx └── 📁lib └── utils.ts └── 📁styles └── patterns.module.css └── types.d.tsx ... Building the App Creating the context First, let’s create a context to manage the state related to icon properties and the background. types.d.ts : types.d.ts export interface ContextApp { icon: string iconSize: number iconColor: string iconBorderWidth: number iconRotate: number bgRounded: number bgPadding: number bgColor: string } export interface ContextApp { icon: string iconSize: number iconColor: string iconBorderWidth: number iconRotate: number bgRounded: number bgPadding: number bgColor: string } context/index.ts : context/index.ts 'use client' import { ReactNode, createContext, useContext, useEffect, useState } from 'react' // Import necessary dependencies from React // Import the ContextApp type from the types file import { ContextApp } from '@/types.d' // Initial state with default values const initialState = { icon: 'Shell', iconSize: 420, iconColor: '#000000', iconBorderWidth: 1, iconRotate: 0, bgRounded: 250, bgPadding: 0, bgColor: '#f59e0b', } // Create the context with an empty object and the setValues function typed const AppContext = createContext({} as { values: ContextApp; setValues: (values: ContextApp) => void }) // Context provider component export function AppWrapper({ children }: { children: ReactNode }) { // State to store the context values const [values, setValues] = useState({} as ContextApp) useEffect(() => { // Try to get saved values from localStorage and set them to state const saved = localStorage.getItem('logobuilder') if (saved) { setValues(JSON.parse(saved)) } else { setValues(initialState) // If no saved values, set the initial state } }, []) // Provide the values and setValues function to the children components return <AppContext.Provider value={{ values, setValues }}>{children}</AppContext.Provider> } // Custom hook to use the app context export function useAppContext() { return useContext(AppContext) } 'use client' import { ReactNode, createContext, useContext, useEffect, useState } from 'react' // Import necessary dependencies from React // Import the ContextApp type from the types file import { ContextApp } from '@/types.d' // Initial state with default values const initialState = { icon: 'Shell', iconSize: 420, iconColor: '#000000', iconBorderWidth: 1, iconRotate: 0, bgRounded: 250, bgPadding: 0, bgColor: '#f59e0b', } // Create the context with an empty object and the setValues function typed const AppContext = createContext({} as { values: ContextApp; setValues: (values: ContextApp) => void }) // Context provider component export function AppWrapper({ children }: { children: ReactNode }) { // State to store the context values const [values, setValues] = useState({} as ContextApp) useEffect(() => { // Try to get saved values from localStorage and set them to state const saved = localStorage.getItem('logobuilder') if (saved) { setValues(JSON.parse(saved)) } else { setValues(initialState) // If no saved values, set the initial state } }, []) // Provide the values and setValues function to the children components return <AppContext.Provider value={{ values, setValues }}>{children}</AppContext.Provider> } // Custom hook to use the app context export function useAppContext() { return useContext(AppContext) } This component establishes a global context using React’s Context API to manage state related to icon properties and background settings. User configurations are saved in localStorage to persist their progress. React’s Context API React’s Context API Building the layout app/page.tsx : app/page.tsx import { cookies } from 'next/headers' // Import cookies from Next.js headers ... export default function HomePage() { // Retrieve the layout and collapsed state from cookies const layout = cookies().get('react-resizable-panels:layout') const collapsed = cookies().get('react-resizable-panels:collapsed') // Parse the layout and collapsed values from cookies, default to undefined if not present const defaultLayout = layout ? JSON.parse(layout.value) : undefined const defaultCollapsed = collapsed ? JSON.parse(collapsed.value) : undefined return ( <div className='flex flex-col'> <EditPanel defaultLayout={defaultLayout} defaultCollapsed={defaultCollapsed} navCollapsedSize={4} /> </div> ) } import { cookies } from 'next/headers' // Import cookies from Next.js headers ... export default function HomePage() { // Retrieve the layout and collapsed state from cookies const layout = cookies().get('react-resizable-panels:layout') const collapsed = cookies().get('react-resizable-panels:collapsed') // Parse the layout and collapsed values from cookies, default to undefined if not present const defaultLayout = layout ? JSON.parse(layout.value) : undefined const defaultCollapsed = collapsed ? JSON.parse(collapsed.value) : undefined return ( <div className='flex flex-col'> <EditPanel defaultLayout={defaultLayout} defaultCollapsed={defaultCollapsed} navCollapsedSize={4} /> </div> ) } The HomePage component utilizes cookies to persist the layout state and collapsed state of the resizable panels in the EditPanel . HomePage EditPanel The component retrieves cookies for the layout ( react-resizable-panels:layout ) and the collapsed state ( react-resizable-panels:collapsed ) using cookies().get() from Next.js. react-resizable-panels:layout react-resizable-panels:collapsed cookies().get() components/edit-panel.tsx : components/edit-panel.tsx 'use client' /* all imports */ interface Props { defaultLayout: number[] | undefined defaultCollapsed?: boolean navCollapsedSize: number } export default function EditPanel({ defaultLayout = [20, 80], defaultCollapsed = false, navCollapsedSize }: Props) { // State to track if the side navigation is collapsed const [isCollapsed, setIsCollapsed] = useState(defaultCollapsed) // State to track the currently selected option (Icon or Background) const [selectedOption, setSelectedOption] = useState('Icon') // Access global context values const { values } = useAppContext() // Effect to save the current values to localStorage whenever they change useEffect(() => { if (Object.keys(values).length) { localStorage.setItem('logobuilder', JSON.stringify({ ...values })) } }, [values]) return ( <div className='hidden md:block'> <TooltipProvider delayDuration={0}> <ResizablePanelGroup direction='horizontal' onLayout={(sizes: number[]) => { // Save layout sizes to cookies for persistence document.cookie = `react-resizable-panels:layout=${JSON.stringify(sizes)}` }} className='h-screen fixed items-stretch' > <ResizablePanel defaultSize={defaultLayout[0]} collapsedSize={navCollapsedSize} collapsible={true} minSize={8} maxSize={12} onCollapse={() => { setIsCollapsed(true) // Save collapsed state to cookies for persistence document.cookie = `react-resizable-panels:collapsed=${JSON.stringify(true)}` }} onExpand={() => { setIsCollapsed(false) // Save expanded state to cookies for persistence document.cookie = `react-resizable-panels:collapsed=${JSON.stringify(false)}` }} className={cn(isCollapsed && 'min-w-[72px] transition-all duration-300 ease-in-out')} > <SideNav isCollapsed={isCollapsed} setSelectedOption={setSelectedOption} options={[ { id: 1, title: 'Icon', icon: PencilRuler, }, { id: 2, title: 'Background', icon: Image, }, ]} /> </ResizablePanel> <ResizableHandle withHandle className='border-[1px]' /> <ResizablePanel defaultSize={defaultLayout[1]}> <main className='w-full flex flex-col md:flex-row'> {selectedOption === 'Icon' && <IconController />} {selectedOption === 'Background' && <BackgroundController />} <Preview /> </main> </ResizablePanel> </ResizablePanelGroup> </TooltipProvider> </div> ) } 'use client' /* all imports */ interface Props { defaultLayout: number[] | undefined defaultCollapsed?: boolean navCollapsedSize: number } export default function EditPanel({ defaultLayout = [20, 80], defaultCollapsed = false, navCollapsedSize }: Props) { // State to track if the side navigation is collapsed const [isCollapsed, setIsCollapsed] = useState(defaultCollapsed) // State to track the currently selected option (Icon or Background) const [selectedOption, setSelectedOption] = useState('Icon') // Access global context values const { values } = useAppContext() // Effect to save the current values to localStorage whenever they change useEffect(() => { if (Object.keys(values).length) { localStorage.setItem('logobuilder', JSON.stringify({ ...values })) } }, [values]) return ( <div className='hidden md:block'> <TooltipProvider delayDuration={0}> <ResizablePanelGroup direction='horizontal' onLayout={(sizes: number[]) => { // Save layout sizes to cookies for persistence document.cookie = `react-resizable-panels:layout=${JSON.stringify(sizes)}` }} className='h-screen fixed items-stretch' > <ResizablePanel defaultSize={defaultLayout[0]} collapsedSize={navCollapsedSize} collapsible={true} minSize={8} maxSize={12} onCollapse={() => { setIsCollapsed(true) // Save collapsed state to cookies for persistence document.cookie = `react-resizable-panels:collapsed=${JSON.stringify(true)}` }} onExpand={() => { setIsCollapsed(false) // Save expanded state to cookies for persistence document.cookie = `react-resizable-panels:collapsed=${JSON.stringify(false)}` }} className={cn(isCollapsed && 'min-w-[72px] transition-all duration-300 ease-in-out')} > <SideNav isCollapsed={isCollapsed} setSelectedOption={setSelectedOption} options={[ { id: 1, title: 'Icon', icon: PencilRuler, }, { id: 2, title: 'Background', icon: Image, }, ]} /> </ResizablePanel> <ResizableHandle withHandle className='border-[1px]' /> <ResizablePanel defaultSize={defaultLayout[1]}> <main className='w-full flex flex-col md:flex-row'> {selectedOption === 'Icon' && <IconController />} {selectedOption === 'Background' && <BackgroundController />} <Preview /> </main> </ResizablePanel> </ResizablePanelGroup> </TooltipProvider> </div> ) } The EditPanel uses the ResizablePanel and ResizablePanelGroup components from shadcn/ui to create resizable and collapsible panels. When the layout or collapsed state changes, these new states are saved back to cookies, ensuring persistence across page reloads and sessions. The EditPanel uses the ResizablePanel and ResizablePanelGroup components from shadcn/ui to create resizable and collapsible panels. EditPanel ResizablePanel ResizablePanelGroup shadcn/ui When the layout or collapsed state changes, these new states are saved back to cookies, ensuring persistence across page reloads and sessions. Icon controller components/icon-controller.tsx components/icon-controller.tsx export default function IconControls() { const { values, setValues } = useAppContext() // FallbackSlider component to be used when values are not loaded yet const FallbackSlider = () => { return ( <Slider defaultValue={[0]} min={40} max={500} step={1} /> ) } return ( <ScrollArea className='h-[calc(100vh-80px)]'> <section className='p-4 w-full md:w-[340px] border-r-2 space-y-6'> <h2 className='font-semibold tracking-tight text-xl'>Customize your Icon</h2> {/* Icon Selection */} <div className='space-y-2'> <div className='w-full flex justify-between gap-4'> <label className='font-semibold leading-none tracking-tight'>Icon</label> <span className='text-sm text-muted-foreground'>{values.icon}</span> </div> <IconsModal /> </div> {/* Icon Size Control */} <div className='space-y-2'> <div className='w-full flex justify-between gap-4'> <label className='font-semibold leading-none tracking-tight'>Size</label> <span className='text-sm text-muted-foreground'>{values.iconSize} px</span> </div> {Object.keys(values).length ? ( <Slider defaultValue={[values.iconSize]} min={40} max={500} step={1} onValueChange={(value: any[]) => setValues({ ...values, iconSize: value[0] })} /> ) : ( <FallbackSlider /> )} </div> {/* Icon Border Width Control */} <div className='space-y-2'> <div className='w-full flex justify-between gap-4'> <label className='font-semibold leading-none tracking-tight'>Border Width</label> <span className='text-sm text-muted-foreground'>{values.iconBorderWidth} px</span> </div> {Object.keys(values).length ? ( <Slider defaultValue={[values.iconBorderWidth]} min={1} max={4} step={1} onValueChange={(value: any[]) => setValues({ ...values, iconBorderWidth: value[0] })} /> ) : ( <FallbackSlider /> )} </div> {/* Icon Rotation Control */} <div className='space-y-2'> <div className='w-full flex justify-between gap-4'> <label className='font-semibold leading-none tracking-tight'>Rotate</label> <span className='text-sm text-muted-foreground'>{values.iconRotate} °</span> </div> {Object.keys(values).length ? ( <Slider defaultValue={[values.iconRotate]} min={-180} max={180} step={1} onValueChange={(value: any[]) => setValues({ ...values, iconRotate: value[0] })} /> ) : ( <FallbackSlider /> )} </div> {/* Icon Color Control */} <div className='space-y-2'> <div className='w-full flex justify-between gap-4'> <label className='font-semibold leading-none tracking-tight'>Color</label> </div> <ColorPickerController /> </div> </section> </ScrollArea> ) } export default function IconControls() { const { values, setValues } = useAppContext() // FallbackSlider component to be used when values are not loaded yet const FallbackSlider = () => { return ( <Slider defaultValue={[0]} min={40} max={500} step={1} /> ) } return ( <ScrollArea className='h-[calc(100vh-80px)]'> <section className='p-4 w-full md:w-[340px] border-r-2 space-y-6'> <h2 className='font-semibold tracking-tight text-xl'>Customize your Icon</h2> {/* Icon Selection */} <div className='space-y-2'> <div className='w-full flex justify-between gap-4'> <label className='font-semibold leading-none tracking-tight'>Icon</label> <span className='text-sm text-muted-foreground'>{values.icon}</span> </div> <IconsModal /> </div> {/* Icon Size Control */} <div className='space-y-2'> <div className='w-full flex justify-between gap-4'> <label className='font-semibold leading-none tracking-tight'>Size</label> <span className='text-sm text-muted-foreground'>{values.iconSize} px</span> </div> {Object.keys(values).length ? ( <Slider defaultValue={[values.iconSize]} min={40} max={500} step={1} onValueChange={(value: any[]) => setValues({ ...values, iconSize: value[0] })} /> ) : ( <FallbackSlider /> )} </div> {/* Icon Border Width Control */} <div className='space-y-2'> <div className='w-full flex justify-between gap-4'> <label className='font-semibold leading-none tracking-tight'>Border Width</label> <span className='text-sm text-muted-foreground'>{values.iconBorderWidth} px</span> </div> {Object.keys(values).length ? ( <Slider defaultValue={[values.iconBorderWidth]} min={1} max={4} step={1} onValueChange={(value: any[]) => setValues({ ...values, iconBorderWidth: value[0] })} /> ) : ( <FallbackSlider /> )} </div> {/* Icon Rotation Control */} <div className='space-y-2'> <div className='w-full flex justify-between gap-4'> <label className='font-semibold leading-none tracking-tight'>Rotate</label> <span className='text-sm text-muted-foreground'>{values.iconRotate} °</span> </div> {Object.keys(values).length ? ( <Slider defaultValue={[values.iconRotate]} min={-180} max={180} step={1} onValueChange={(value: any[]) => setValues({ ...values, iconRotate: value[0] })} /> ) : ( <FallbackSlider /> )} </div> {/* Icon Color Control */} <div className='space-y-2'> <div className='w-full flex justify-between gap-4'> <label className='font-semibold leading-none tracking-tight'>Color</label> </div> <ColorPickerController /> </div> </section> </ScrollArea> ) } The IconControls component provides a user interface for customizing various properties of an icon. It uses a combination of sliders, modals, and color pickers to allow users to adjust the icon's size, border width, rotation, and color. IconControls The component also leverages a global context to manage and persist these customization values. icon-controller/icon-list.tsx : icon-controller/icon-list.tsx /* all imports */ import { iconNames } from '@/constants/icons' interface Props { setOpenDialog: (value: boolean) => void } export default function IconList({ setOpenDialog }: Props) { const [visibleIcons, setVisibleIcons] = useState<string[]>([]) // Reference for the IntersectionObserver const observer = useRef<IntersectionObserver | null>(null) const { values, setValues } = useAppContext() // Callback for observing the last icon element const lastIconElementRef = useCallback( (node: HTMLElement | null) => { // Disconnect any existing observer if (observer.current) observer.current.disconnect() // Create a new IntersectionObserver observer.current = new IntersectionObserver(entries => { // If the last icon is visible and there are more icons to load, update the visible icons if (entries[0].isIntersecting && visibleIcons.length < iconNames.length) { setVisibleIcons(prevIcons => [ ...prevIcons, ...iconNames.slice(prevIcons.length, prevIcons.length + 20) ]) } }) // Observe the node if it exists if (node) observer.current.observe(node) }, [visibleIcons.length, iconNames, setVisibleIcons] ) useEffect(() => { // Load initial set of icons if (iconNames.length) setVisibleIcons(iconNames.slice(0, 20)) }, [iconNames]) return ( <div className='grid grid-cols-6 gap-4 py-4'> {visibleIcons.map((icon, index) => { if (visibleIcons.length === index + 1) { return ( <div ref={lastIconElementRef} key={icon} > <Button title={icon} aria-label={icon} variant='ghost' className='h-9 w-9 p-1 border shadow-lg' onClick={() => { setValues({ ...values, icon }) setOpenDialog(false) }} > <CustomIcon name={icon} /> </Button> </div> ) } else { // Render button without observer reference return ( <Button key={icon} title={icon} aria-label={icon} variant='ghost' className='h-9 w-9 p-1 border shadow-lg' onClick={() => { setValues({ ...values, icon }) setOpenDialog(false) }} > <CustomIcon name={icon} /> </Button> ) } })} </div> ) } /* all imports */ import { iconNames } from '@/constants/icons' interface Props { setOpenDialog: (value: boolean) => void } export default function IconList({ setOpenDialog }: Props) { const [visibleIcons, setVisibleIcons] = useState<string[]>([]) // Reference for the IntersectionObserver const observer = useRef<IntersectionObserver | null>(null) const { values, setValues } = useAppContext() // Callback for observing the last icon element const lastIconElementRef = useCallback( (node: HTMLElement | null) => { // Disconnect any existing observer if (observer.current) observer.current.disconnect() // Create a new IntersectionObserver observer.current = new IntersectionObserver(entries => { // If the last icon is visible and there are more icons to load, update the visible icons if (entries[0].isIntersecting && visibleIcons.length < iconNames.length) { setVisibleIcons(prevIcons => [ ...prevIcons, ...iconNames.slice(prevIcons.length, prevIcons.length + 20) ]) } }) // Observe the node if it exists if (node) observer.current.observe(node) }, [visibleIcons.length, iconNames, setVisibleIcons] ) useEffect(() => { // Load initial set of icons if (iconNames.length) setVisibleIcons(iconNames.slice(0, 20)) }, [iconNames]) return ( <div className='grid grid-cols-6 gap-4 py-4'> {visibleIcons.map((icon, index) => { if (visibleIcons.length === index + 1) { return ( <div ref={lastIconElementRef} key={icon} > <Button title={icon} aria-label={icon} variant='ghost' className='h-9 w-9 p-1 border shadow-lg' onClick={() => { setValues({ ...values, icon }) setOpenDialog(false) }} > <CustomIcon name={icon} /> </Button> </div> ) } else { // Render button without observer reference return ( <Button key={icon} title={icon} aria-label={icon} variant='ghost' className='h-9 w-9 p-1 border shadow-lg' onClick={() => { setValues({ ...values, icon }) setOpenDialog(false) }} > <CustomIcon name={icon} /> </Button> ) } })} </div> ) } This component, IconList , allows users to select an icon from a list and load more icons as they scroll. It uses context to manage and update the selected icon and employs an Intersection Observer to load more icons when the user reaches the end of the list. IconList Intersection Observer Intersection Observer To display all the icons, I extracted their names directly from the Lucide React library and created the following component to render each icon. icon-controller/custom-icon.tsx : icon-controller/custom-icon.tsx import { icons } from 'lucide-react' interface Props { name: string color?: string size?: number strokeWidth?: number } export default function CustomIcon({ name, color = 'currentColor', size = 24, strokeWidth = 1 }: Props) { const LucideIcon = icons[name as keyof typeof icons] if (!LucideIcon) return null return ( <LucideIcon size={size} color={color} strokeWidth={strokeWidth} /> ) } import { icons } from 'lucide-react' interface Props { name: string color?: string size?: number strokeWidth?: number } export default function CustomIcon({ name, color = 'currentColor', size = 24, strokeWidth = 1 }: Props) { const LucideIcon = icons[name as keyof typeof icons] if (!LucideIcon) return null return ( <LucideIcon size={size} color={color} strokeWidth={strokeWidth} /> ) } The customIcon component allows you to render a specific icon from the lucide-react library with customizable properties such as name, color, size, and stroke width. customIcon lucide-react Background controller background-controller/index.tsx : background-controller/index.tsx /* all imports */ export default function BackgroundController() { // Use the context to get and set background values const { values, setValues } = useAppContext() return ( <ScrollArea className='h-[calc(100vh-80px)]'> <section className='p-4 w-full md:w-[340px] border-r-2 space-y-6'> <h2 className='font-semibold tracking-tight text-xl'>Customize your Background</h2> {/* Rounded corner slider */} <div className='space-y-2'> <div className='w-full flex justify-between gap-4'> <label className='font-semibold leading-none tracking-tight'>Rounded</label> <span className='text-sm text-muted-foreground'>{values.bgRounded} px</span> </div> {/* Slider to adjust the rounded corners of the background */} <Slider defaultValue={[values.bgRounded]} min={0} max={250} step={1} onValueChange={(value: any[]) => setValues({ ...values, bgRounded: value[0] })} /> </div> {/* Padding slider */} <div className='space-y-2'> <div className='w-full flex justify-between gap-4'> <label className='font-semibold leading-none tracking-tight'>Padding</label> <span className='text-sm text-muted-foreground'>{values.bgPadding} px</span> </div> {/* Slider to adjust the padding of the background */} <Slider defaultValue={[values.bgPadding]} min={0} max={100} step={1} onValueChange={(value: any[]) => setValues({ ...values, bgPadding: value[0] })} /> </div> {/* Color picker */} <div className='space-y-2'> <div className='w-full flex justify-between gap-4'> <label className='font-semibold leading-none tracking-tight'>Color</label> </div> {/* Component to pick a background color */} <ColorPickerController hidecontrols={false} hidepresets={false} /> </div> </section> </ScrollArea> ) } /* all imports */ export default function BackgroundController() { // Use the context to get and set background values const { values, setValues } = useAppContext() return ( <ScrollArea className='h-[calc(100vh-80px)]'> <section className='p-4 w-full md:w-[340px] border-r-2 space-y-6'> <h2 className='font-semibold tracking-tight text-xl'>Customize your Background</h2> {/* Rounded corner slider */} <div className='space-y-2'> <div className='w-full flex justify-between gap-4'> <label className='font-semibold leading-none tracking-tight'>Rounded</label> <span className='text-sm text-muted-foreground'>{values.bgRounded} px</span> </div> {/* Slider to adjust the rounded corners of the background */} <Slider defaultValue={[values.bgRounded]} min={0} max={250} step={1} onValueChange={(value: any[]) => setValues({ ...values, bgRounded: value[0] })} /> </div> {/* Padding slider */} <div className='space-y-2'> <div className='w-full flex justify-between gap-4'> <label className='font-semibold leading-none tracking-tight'>Padding</label> <span className='text-sm text-muted-foreground'>{values.bgPadding} px</span> </div> {/* Slider to adjust the padding of the background */} <Slider defaultValue={[values.bgPadding]} min={0} max={100} step={1} onValueChange={(value: any[]) => setValues({ ...values, bgPadding: value[0] })} /> </div> {/* Color picker */} <div className='space-y-2'> <div className='w-full flex justify-between gap-4'> <label className='font-semibold leading-none tracking-tight'>Color</label> </div> {/* Component to pick a background color */} <ColorPickerController hidecontrols={false} hidepresets={false} /> </div> </section> </ScrollArea> ) } The BackgroundController component allows users to customize background properties, including rounded corners, padding, and color. Sliders are used to adjust these properties, and a color picker is provided for background color. BackgroundController background-controller/color-picker.tsx : background-controller/color-picker.tsx /* all imports */ interface Props { hidecontrols?: boolean hidepresets?: boolean } export default function ColorPickerController({ hidecontrols = true, hidepresets = true }: Props) { // Use context to get and set the background color value const { values, setValues } = useAppContext() const [color, setColor] = useState(values.bgColor) // Update context value whenever the local color state changes useEffect(() => { setValues({ ...values, bgColor: color }) }, [color]) return ( // Render the color picker component with current color value and change handler <ColorPicker value={color} onChange={setColor} hideControls={hidecontrols} // Optional prop to hide/show controls hidePresets={hidepresets} // Optional prop to hide/show presets /> ) } /* all imports */ interface Props { hidecontrols?: boolean hidepresets?: boolean } export default function ColorPickerController({ hidecontrols = true, hidepresets = true }: Props) { // Use context to get and set the background color value const { values, setValues } = useAppContext() const [color, setColor] = useState(values.bgColor) // Update context value whenever the local color state changes useEffect(() => { setValues({ ...values, bgColor: color }) }, [color]) return ( // Render the color picker component with current color value and change handler <ColorPicker value={color} onChange={setColor} hideControls={hidecontrols} // Optional prop to hide/show controls hidePresets={hidepresets} // Optional prop to hide/show presets /> ) } Preview Section components/preview.tsx : components/preview.tsx /* all imports */ export default function Preview() { const { values } = useAppContext() const { icon, iconSize, iconColor, iconBorderWidth, iconRotate, bgColor, bgRounded, bgPadding } = values const divRef = useRef<HTMLDivElement | null>(null) // Function to handle the download of the image const handleDownloadImage = () => { const downloadArea = divRef.current // Use html2canvas to capture the screenshot of the referenced div html2canvas(downloadArea as HTMLDivElement, { backgroundColor: null, }).then(canvas => { const pngImage = canvas.toDataURL('image/png') // Convert the canvas to a PNG image const downloadLink = document.createElement('a') // Create a new anchor element downloadLink.href = pngImage // Set the href to the PNG image URL downloadLink.download = 'logobuilder.png' // Set the download attribute to the desired file name downloadLink.click() // Programmatically click the anchor element to trigger the download }) } return ( <div className={`flex-1 p-4 ${Styles.patternDotsMd}`}> {/* Download button section */} <div className='w-full flex justify-end'> <Button size={'sm'} className='flex gap-2 items-center' onClick={() => handleDownloadImage()} > <Download strokeWidth={2} className='w-4 h-4' /> Download </Button> </div> {/* Preview section */} <div className='grid place-content-center h-[calc(100%-52px)]'> <div className='bg-white border-2 hover:border-gray-300'> <div ref={divRef} // Attach the ref to this div id='download-area' className='h-[500px] w-[500px]' style={{ padding: `${bgPadding}px`, }} > {/* Conditional rendering based on whether values are populated */} {!Object.keys(values).length ? ( <div className='animate-pulse flex justify-center items-center h-full w-full bg-gray-200'> <div className='animate-pulse h-4/5 w-4/5 rounded-full bg-gray-300' /> </div> ) : ( <div className='w-full h-full grid place-content-center' style={{ background: bgColor, borderRadius: `${bgRounded}px`, padding: `${bgPadding}px`, }} > <span style={{ transform: `rotate(${iconRotate}deg)` }}> <CustomIcon name={icon} size={iconSize} color={iconColor} strokeWidth={iconBorderWidth} /> </span> </div> )} </div> </div> </div> </div> ) } /* all imports */ export default function Preview() { const { values } = useAppContext() const { icon, iconSize, iconColor, iconBorderWidth, iconRotate, bgColor, bgRounded, bgPadding } = values const divRef = useRef<HTMLDivElement | null>(null) // Function to handle the download of the image const handleDownloadImage = () => { const downloadArea = divRef.current // Use html2canvas to capture the screenshot of the referenced div html2canvas(downloadArea as HTMLDivElement, { backgroundColor: null, }).then(canvas => { const pngImage = canvas.toDataURL('image/png') // Convert the canvas to a PNG image const downloadLink = document.createElement('a') // Create a new anchor element downloadLink.href = pngImage // Set the href to the PNG image URL downloadLink.download = 'logobuilder.png' // Set the download attribute to the desired file name downloadLink.click() // Programmatically click the anchor element to trigger the download }) } return ( <div className={`flex-1 p-4 ${Styles.patternDotsMd}`}> {/* Download button section */} <div className='w-full flex justify-end'> <Button size={'sm'} className='flex gap-2 items-center' onClick={() => handleDownloadImage()} > <Download strokeWidth={2} className='w-4 h-4' /> Download </Button> </div> {/* Preview section */} <div className='grid place-content-center h-[calc(100%-52px)]'> <div className='bg-white border-2 hover:border-gray-300'> <div ref={divRef} // Attach the ref to this div id='download-area' className='h-[500px] w-[500px]' style={{ padding: `${bgPadding}px`, }} > {/* Conditional rendering based on whether values are populated */} {!Object.keys(values).length ? ( <div className='animate-pulse flex justify-center items-center h-full w-full bg-gray-200'> <div className='animate-pulse h-4/5 w-4/5 rounded-full bg-gray-300' /> </div> ) : ( <div className='w-full h-full grid place-content-center' style={{ background: bgColor, borderRadius: `${bgRounded}px`, padding: `${bgPadding}px`, }} > <span style={{ transform: `rotate(${iconRotate}deg)` }}> <CustomIcon name={icon} size={iconSize} color={iconColor} strokeWidth={iconBorderWidth} /> </span> </div> )} </div> </div> </div> </div> ) } The Preview component displays a customizable icon within a styled area and allows downloading the preview as an image. It uses useRef to reference the DOM element of the preview area and html2canvas to capture an image of this area. Preview useRef html2canvas The handleDownloadImage function converts the captured content into a PNG image and downloads the file when the download button is clicked. handleDownloadImage That’s it! 🎉 💎** Demo here ** Demo here Demo here 🧑💻 Repo here Repo here Repo here Repo here Conclusion You’ve now built the basic features of a logo builder application. From here, you can expand and enhance the app with features like: Adding multiple icons Including image uploads Providing pre-built logo templates Adding text elements Allowing draggable elements within the canvas Adding premium features Adding multiple icons Adding multiple icons Including image uploads Including image uploads Providing pre-built logo templates Providing pre-built logo templates Adding text elements Adding text elements Allowing draggable elements within the canvas Allowing draggable elements within the canvas Adding premium features Adding premium features Have fun improving and adapting this project to suit your needs! Reference: https://www.youtube.com/watch?v=XdsZA8wSNDg&pp=ygUObG9nbyBtYWtlciBhcHA%3D https://www.youtube.com/watch?v=XdsZA8wSNDg&pp=ygUObG9nbyBtYWtlciBhcHA%3D https://www.youtube.com/watch?v=XdsZA8wSNDg&pp=ygUObG9nbyBtYWtlciBhcHA%3D Read more: Read more: Enhancing Password Security and Recovery with Next.js 14 and NextAuth.js Enhancing Password Security and Recovery with Next.js 14 and NextAuth.js Want to connect with the Author? Love connecting with friends all around the world on 𝕏 . 𝕏