I Built a Tool That Generates 160 Different Code Combinations from a Single Drag & Drop — Here's How
Source: Dev.to
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?”
- Write the HTML.
- Add CSS for positioning, button styles, hover effects.
- Done.
Then the email arrives:
- “Can we move the CTA button slightly to the right?” → adjust CSS → Done.
- “Actually, can we try it 20 pixels higher?” → adjust CSS again → Done.
What I Built: PromoKit
- 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/
Source Code
GitHub – seadonggyun4/promo-kit
The Numbers That Make Me Proud
| Category | Details |
|---|---|
| Frameworks | React, Vue, Svelte, Angular, Solid.js, Preact, Astro, Qwik, Lit, Vanilla HTML |
| Styling Options | CSS, SCSS, Tailwind, Styled Components, Emotion, CSS Modules, UnoCSS, Panda CSS, … |
| Presets | 27 button styles + 5 text styles + 12 image overlays = 44 |
| Combinations | Any framework × any styling = 160 possible stacks |
Technical Deep Dive: How It’s Built
src/
├── features/
│ ├── button-editor/ # Button creation & customization
│ ├── text-editor/ # Text element management
│ ├── image-overlay-editor/ # Image overlay handling
│ ├── download/ # The 2200+‑line code generator
│ └── version-history/ # Undo/redo system
└── shared/
└── store/ # Zustand state management
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
// 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 };
});
};
The magic: Users never think about saving state. Every action is automatically reversible. Ctrl+Z just works.
The History System: Command Pattern Done Right
interface HistoryStore {
past: EditorSnapshot[]; // Previous states
present: EditorSnapshot; // Current state
future: EditorSnapshot[]; // Redo states
}
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
Debounced History
// During drag operations we don't want 60 snapshots per second
debouncedPushToHistory(
elements,
'Position Changed',
'element_move',
500 // ms of inactivity before saving
);
When you drag a button, it waits 500 ms of inactivity before saving – smooth UX, no performance hit.
Code Generation Engine: 2200+ Lines of Framework Wizardry
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);
}
The output adapts to everything:
| Framework | Event Syntax | Styling Integration |
|---|---|---|
| React | onClick={...} | Styled Components, Tailwind, etc. |
| Vue | @click="..." | Scoped CSS, CSS Modules |
| Svelte | on:click={...} | “ blocks or Tailwind |
| Angular | (click)="..." | ngClass, ngStyle |
| Solid.js, Preact, Astro, Qwik, Lit, Vanilla | analogous conventions | — |
Example Outputs
React
export const PromoPage: React.FC = () => {
return (
<button onClick={() => {
window.location.href = '/deal';
}}>
Get 50% Off
</button>
);
};
Vue (Script Setup)
<template>
<button @click="handleClick">Get 50% Off</button>
</template>
<script setup>
const handleClick = () => {
window.location.href = '/deal';
};
</script>
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
- View how many past/future states exist
It’s like Git for your design.
Smart Presets
44 presets organized by category:
| Category | Count | Examples |
|---|---|---|
| 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.
Real‑Time Preview
(Live preview pane updates as you edit.)
Accessibility Built‑In
- Semantic tags (
<button>,<a>, etc.) aria-labelattributesaria-describedbyfor extended descriptionsroleattributes where needed:focus-visiblestyles for keyboard users- Screen‑reader‑only content
Responsive Code Generation
Toggle “responsive” and the generated code includes media queries automatically:
@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;
}
}
Your landing page works everywhere, automatically.
SEO Meta Tags
Need Open Graph tags? Twitter Cards? Canonical URLs? One toggle. Done.
Architecture Choices
Zustand over Redux
- Simpler API
- Built‑in history tracking (as shown above)
Feature‑Sliced Design
- Clear separation of concerns
- Easy to scale
Composition over Inheritance for Hooks
// Base form logic
const baseForm = useButtonForm();
// Extended for animated buttons
const animatedForm = useAnimatedBtn(); // Internally uses useButtonForm
Clean, testable, and DRY.
Dynamic Imports for Circular Dependencies
const getBackgroundImage = async () => {
const module = await import('./background');
return module.default;
};
Miscellaneous Code Snippet
// Retrieve the uploaded image from the Zustand store
() => {
const { useUploadImageStore } = require('./uploadImageStore');
return useUploadImageStore.getState().uploadedImage;
};
Not the prettiest, but it solved a real problem elegantly.
Tech Stack
- React 18 with TypeScript
- Vite – blazing‑fast development server
- Zustand – state management
- styled‑components – styling
- dnd‑kit – drag‑and‑drop
- react‑i18next – Korean, English, Japanese support
- JSZip + FileSaver – ZIP downloads
Demo & Source
⭐️ Drop a star if you think this is cool.
🐞 Open issues for any missing frameworks you’d like to see.
🔧 Pull requests are very welcome!
Built with mass frustration, mass coffee, and the firm belief that developers shouldn’t write the same CSS twice.