DEV Community

DONG GYUN SEO
DONG GYUN SEO

Posted on

I Built a Tool That Generates 160 Different Code Combinations from a Single Drag & Drop — Here's How

TL;DR: I was sick of writing the same event landing page code over and over. So I built PromoKit — drag buttons onto an image, pick your framework, get production-ready code. 10 frameworks × 16 styling options. Let me walk you through how I built this beast.

The Origin Story: "Can You Move That Button 3 Pixels Up?"
Picture this: You're a frontend developer. Marketing sends you an event landing page mockup. Simple enough, right? Just some buttons over a promotional image.

So you write the HTML. CSS for positioning. Button styles. Hover effects. Done.

Then comes the email: "Can we move the CTA button slightly to the right?"

You adjust the CSS. Push. Done.

"Actually, can we try it 20 pixels higher?"
"The button color should match our brand guide..."
"We're using Vue now, can you rewrite it?"
Repeat this 47 times across 12 different campaigns.
I snapped. There had to be a better way.

What I Built: PromoKit
PromoKit is a visual editor that lets you:

  • Upload your promotional image
  • Drag & drop buttons, text, and image overlays
  • Pick your framework and styling method
  • Copy production-ready code

That's it. No more manual coordinate calculations. No more rewriting the same thing for React, Vue, and Vanilla JS.

Live Demo: https://promotion-page-editor.netlify.app/
GitHub: https://github.com/seadonggyun4/promo-kit

The Numbers That Make Me Proud

  • 10 Frameworks: React, Vue, Svelte, Angular, Solid.js, Preact, Astro, Qwik, Lit, Vanilla HTML
  • 16 Styling Options: CSS, SCSS, Tailwind, Styled Components, Emotion, CSS Modules, UnoCSS, Panda CSS, and more 44 Presets: 27 button styles + 5 text styles + 12 image overlays
  • 160 Combinations: Any framework × any styling = your stack, covered

Technical Deep Dive: How It's Built
Architecture: Feature-Sliced Design
I went with a modular, feature-driven architecture:

src/
├── features/
│   ├── button-editor/     # Button creation & customization
│   ├── text-editor/       # Text element management
│   ├── image-overlay-editor/
│   ├── download/          # The 2200+ line code generator
│   └── version-history/   # Undo/redo system
├── shared/
│   └── store/             # Zustand state management
Enter fullscreen mode Exit fullscreen mode

Each feature is completely self-contained. Want to add a new element type? Create a new feature folder. Done. No touching existing code.

State Management: Zustand with Superpowers
I chose Zustand over Redux for its simplicity, but I pushed it to do some sophisticated things:

// Every mutation automatically logs to history
const updateElement = (id, data) => {
  set((state) => {
    const updated = state.elements.map(el => 
      el.id === id ? { ...el, ...data } : el
    );

    // Automatic history tracking!
    pushToHistory(updated, 'Style Changed', 'element_style');

    return { elements: updated };
  });
};
Enter fullscreen mode Exit fullscreen mode

The magic: Users never think about saving state. Every action is automatically reversible. Ctrl+Z just works.

The History System: Command Pattern Done Right
This is where I'm particularly proud. The undo/redo system uses a 3-stack architecture:

interface HistoryStore {
  past: EditorSnapshot[];      // Previous states
  present: EditorSnapshot;     // Current state
  future: EditorSnapshot[];    // Redo states
}
Enter fullscreen mode Exit fullscreen mode

Each snapshot captures:

  • All elements with their positions and styles
  • Background image (compressed)
  • Currently selected element
  • Timestamp and action description
  • Action type for visual distinction

But here's the clever part — debouncing:

// During drag operations, we don't want 60 snapshots per second
debouncedPushToHistory(elements, 'Position Changed', 'element_move', 500);
Enter fullscreen mode Exit fullscreen mode

When you drag a button, it waits 500ms of inactivity before saving. Smooth UX, no performance hit.

The Code Generation Engine: 2200+ Lines of Framework Wizardry
This is the heart of PromoKit. When you click "Export", this happens:

function generateCode(
  framework: Framework,
  styling: StylingMethod,
  elements: Element[],
  options: CodeOptions
): string {
  // 1. Detect element types (gradient buttons need special handling)
  const hasGradients = elements.some(el => isGradientButton(el.style));

  // 2. Generate framework-specific imports
  const imports = generateImports(framework, styling, hasGradients);

  // 3. Generate element markup per framework
  const markup = elements.map(el => 
    generateElementMarkup(el, framework, styling)
  );

  // 4. Generate styles per styling method
  const styles = generateStyles(elements, styling, options);

  // 5. Assemble into framework structure
  return assembleComponent(framework, imports, markup, styles);
}
Enter fullscreen mode Exit fullscreen mode

The output adapts to everything:

  • React gets onClick, Vue gets @click, Svelte gets on:click
  • Styled Components get tagged template literals
  • Tailwind gets utility classes
  • Angular gets ngClass bindings

And it's not just syntax translation. Each framework follows its actual conventions:

// React output
export const PromoPage: React.FC = () => {
  return (
    <Container>
      <StyledButton onClick={() => window.location.href = '/deal'}>
        Get 50% Off
      </StyledButton>
    </Container>
  );
};

// Vue output
<template>
  <div class="container">
    <button class="styled-button" @click="handleClick">
      Get 50% Off
    </button>
  </div>
</template>

<script setup lang="ts">
const handleClick = () => {
  window.location.href = '/deal';
};
</script>
Enter fullscreen mode Exit fullscreen mode

UX Features That I'm Proud Of

  1. Visual History Timeline Not just undo/redo buttons. A full visual timeline where you can:
  • See what each action was (with icons!)
  • Click any point to jump there
  • See how many past/future states exist
  • It's like Git for your design.
  1. Smart Presets 44 presets organized by category:
  • Simple Buttons (11): Clean, minimal designs
  • Gradient Buttons (10): Eye-catching gradients
  • Animated Buttons (6): Bounce, glow, pulse, shake, slide, ripple
  • Each preset is fully customizable after placement.
  1. Real-Time Preview
    What you see is exactly what you get. Drag a button, see it move. Change a color, see it update. No mental translation needed.

  2. Accessibility Built-In
    Toggle a switch and your generated code includes:

  • Semantic tags
  • aria-label attributes
  • aria-describedby for extended descriptions
  • role attributes
  • :focus-visible styles for keyboard users
  • Screen-reader-only content
  1. Responsive Code Generation Another toggle, and your code includes:
@media (max-width: 768px) {
  .button {
    transform: scale(0.85);
    font-size: 0.9rem;
  }
}

@media (max-width: 640px) {
  .button {
    transform: scale(0.7);
    font-size: 0.8rem;
  }
}
Enter fullscreen mode Exit fullscreen mode

Your landing page works everywhere, automatically.

  1. SEO Meta Tags Need Open Graph tags? Twitter Cards? Canonical URLs? One toggle. Done.

Technical Decisions I'd Make Again

  1. Zustand over Redux
    For an app like this, Zustand's minimal boilerplate was perfect. I can create a new store in 10 lines and integrate it with history tracking in 2 more.

  2. Feature-Sliced Design
    Adding the image overlay feature took me an afternoon. I just copied the button-editor structure and modified it. No spaghetti, no regression.

  3. Composition over Inheritance for Hooks

// Base form logic
const baseForm = useButtonForm();

// Extended for animated buttons
const animatedForm = useAnimatedBtn(); // Internally uses useButtonForm
Enter fullscreen mode Exit fullscreen mode

Clean, testable, and DRY.

  1. Dynamic Imports for Circular Dependencies
const getBackgroundImage = () => {
  const { useUploadImageStore } = require('./uploadImageStore');
  return useUploadImageStore.getState().uploadedImage;
};
Enter fullscreen mode Exit fullscreen mode

Not the prettiest, but it solved a real problem elegantly.

The Stack

  • React 18 with TypeScript
  • Vite for blazing fast dev server
  • Zustand for state management
  • styled-components for styling
  • dnd-kit for drag and drop
  • react-i18next for Korean, English, Japanese support
  • JSZip + FileSaver for ZIP downloads

Try It Out!

Drop a star if you think this is cool. Open issues if you want a framework I haven't added yet. PRs are very welcome!

Built with mass frustration, mass coffee, and the firm belief that developers shouldn't write the same CSS twice.

Top comments (0)