Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
197 changes: 197 additions & 0 deletions app.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,197 @@
const stylePresets = [
{
name: "Vibrant",
gradient: ["#4c5cff", "#ff5b7c"],
textShadow: "0 10px 20px rgba(15, 23, 42, 0.4)",
},
{
name: "Neon",
gradient: ["#00f5d4", "#9b5cff"],
textShadow: "0 8px 18px rgba(0, 0, 0, 0.35)",
},
{
name: "Cinematic",
gradient: ["#111827", "#ef4444"],
textShadow: "0 6px 20px rgba(0, 0, 0, 0.55)",
},
];

const palettes = [
{ name: "Cosmic", colors: ["#4c5cff", "#ff5b7c", "#ffffff"] },
{ name: "Sunset", colors: ["#ff8c42", "#ff3c6f", "#ffd66b"] },
{ name: "Ocean", colors: ["#0077ff", "#00c2ff", "#e0f7ff"] },
];

const alignments = ["Left", "Center", "Right"];

const state = {
style: stylePresets[0],
palette: palettes[0],
alignment: alignments[0],
};

const previewCanvas = document.getElementById("previewCanvas");
const previewText = document.getElementById("previewText");
const previewTitle = document.getElementById("previewTitle");
const previewSubtitle = document.getElementById("previewSubtitle");
const previewKicker = document.getElementById("previewKicker");
const styleBadge = document.getElementById("styleBadge");
const swatches = document.getElementById("swatches");

const styleChips = document.getElementById("styleChips");
const alignChips = document.getElementById("alignChips");
const paletteOptions = document.getElementById("paletteOptions");

const videoTitle = document.getElementById("videoTitle");
const videoHook = document.getElementById("videoHook");
const videoSubtitle = document.getElementById("videoSubtitle");

const generateBtn = document.getElementById("generateBtn");
const shuffleBtn = document.getElementById("shuffleBtn");
const resetBtn = document.getElementById("resetBtn");

const createChip = (label, group, onSelect) => {
const chip = document.createElement("button");
chip.type = "button";
chip.className = "chip";
chip.textContent = label;
chip.addEventListener("click", () => onSelect(label, chip));
group.appendChild(chip);
return chip;
};

const updatePreview = () => {
const [first, second] = state.style.gradient;
previewCanvas.style.background = `linear-gradient(135deg, ${first}, ${second})`;
previewText.style.textAlign = state.alignment.toLowerCase();
previewText.style.textShadow = state.style.textShadow;
styleBadge.textContent = state.style.name;

swatches.innerHTML = "";
state.palette.colors.forEach((color) => {
const swatch = document.createElement("span");
swatch.className = "swatch";
swatch.style.background = color;
swatches.appendChild(swatch);
});
};

const setActiveChip = (group, activeChip) => {
group.querySelectorAll(".chip").forEach((chip) => chip.classList.remove("active"));
activeChip.classList.add("active");
};

stylePresets.forEach((preset, index) => {
const chip = createChip(preset.name, styleChips, (label, clicked) => {
state.style = stylePresets.find((item) => item.name === label);
setActiveChip(styleChips, clicked);
updatePreview();
});
if (index === 0) {
chip.classList.add("active");
}
});

alignments.forEach((alignment, index) => {
const chip = createChip(alignment, alignChips, (label, clicked) => {
state.alignment = label;
setActiveChip(alignChips, clicked);
updatePreview();
});
if (index === 0) {
chip.classList.add("active");
}
});

palettes.forEach((palette, index) => {
const card = document.createElement("button");
card.type = "button";
card.className = "palette-option";

const samples = document.createElement("div");
samples.className = "palette-samples";
palette.colors.forEach((color) => {
const dot = document.createElement("span");
dot.style.background = color;
samples.appendChild(dot);
});

const label = document.createElement("span");
label.textContent = palette.name;

card.appendChild(samples);
card.appendChild(label);
card.addEventListener("click", () => {
state.palette = palette;
paletteOptions
.querySelectorAll(".palette-option")
.forEach((option) => option.classList.remove("active"));
card.classList.add("active");
updatePreview();
});

if (index === 0) {
card.classList.add("active");
}

paletteOptions.appendChild(card);
});

const syncInputs = () => {
previewTitle.textContent = videoTitle.value.trim() || "Your video title";
previewKicker.textContent = videoHook.value.trim() || "Hook";
previewSubtitle.textContent = videoSubtitle.value.trim() || "Supporting line";
};

const randomItem = (items) => items[Math.floor(Math.random() * items.length)];

const shuffle = () => {
state.style = randomItem(stylePresets);
state.palette = randomItem(palettes);
state.alignment = randomItem(alignments);

styleChips
.querySelectorAll(".chip")
.forEach((chip) => chip.classList.toggle("active", chip.textContent === state.style.name));
alignChips
.querySelectorAll(".chip")
.forEach((chip) => chip.classList.toggle("active", chip.textContent === state.alignment));
paletteOptions
.querySelectorAll(".palette-option")
.forEach((option) => option.classList.toggle("active", option.textContent === state.palette.name));

updatePreview();
};

const resetForm = () => {
videoTitle.value = "Create Thumbnails That Pop";
videoHook.value = "AI Hacks";
videoSubtitle.value = "Boost clicks with smart layouts";
state.style = stylePresets[0];
state.palette = palettes[0];
state.alignment = alignments[0];
shuffle();

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Avoid randomizing state during reset

Calling shuffle() inside the reset handler overrides the default style, palette, and alignment values set just above, so pressing Reset produces a random configuration instead of restoring the initial baseline. In normal UI use this makes the reset action unreliable and prevents users from getting back to the documented default preset combination.

Useful? React with 👍 / 👎.

syncInputs();
};

videoTitle.addEventListener("input", syncInputs);
videoHook.addEventListener("input", syncInputs);
videoSubtitle.addEventListener("input", syncInputs);

const form = document.getElementById("thumbnailForm");
form.addEventListener("submit", (event) => {
event.preventDefault();
syncInputs();
updatePreview();
});

generateBtn.addEventListener("click", () => {
syncInputs();
updatePreview();
});

shuffleBtn.addEventListener("click", shuffle);
resetBtn.addEventListener("click", resetForm);

syncInputs();
updatePreview();
128 changes: 128 additions & 0 deletions index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>AI YouTube Thumbnail Generator</title>
<link rel="stylesheet" href="styles.css" />
</head>
<body>
<div class="page">
<header class="hero">
<div>
<p class="eyebrow">AI YouTube Studio</p>
<h1>AI YouTube Thumbnail Generator</h1>
<p class="subtitle">
Craft attention-grabbing thumbnails in minutes with smart presets, bold typography, and
creative layouts.
</p>
<div class="hero-actions">
<button class="primary" id="generateBtn">Generate thumbnail</button>
<button class="ghost" id="shuffleBtn">Surprise me</button>
</div>
<ul class="hero-metrics">
<li>
<strong>1,280×720</strong>
<span>Optimized size</span>
</li>
<li>
<strong>3</strong>
<span>Style modes</span>
</li>
<li>
<strong>Instant</strong>
<span>Preview</span>
</li>
</ul>
</div>
<div class="preview-card">
<div class="preview-header">
<span>Live preview</span>
<span class="badge" id="styleBadge">Vibrant</span>
</div>
<div class="preview-canvas" id="previewCanvas">
<div class="preview-text" id="previewText">
<p class="preview-kicker" id="previewKicker">AI Hacks</p>
<h2 id="previewTitle">Create Thumbnails That Pop</h2>
<p class="preview-subtitle" id="previewSubtitle">Boost clicks with smart layouts</p>
</div>
</div>
<div class="preview-footer">
<span>Background</span>
<div class="swatches" id="swatches"></div>
</div>
</div>
</header>

<main class="content">
<section class="form-card">
<h2>Describe your video</h2>
<p>Tell the AI what your video is about, choose a style, and customize the layout.</p>
<form id="thumbnailForm">
<label>
Video title
<input type="text" id="videoTitle" value="Create Thumbnails That Pop" />
</label>
<label>
Hook / short phrase
<input type="text" id="videoHook" value="AI Hacks" />
</label>
<label>
Supporting line
<input type="text" id="videoSubtitle" value="Boost clicks with smart layouts" />
</label>
<label>
Style preset
<div class="chip-group" id="styleChips"></div>
</label>
<label>
Color palette
<div class="palette" id="paletteOptions"></div>
</label>
<label>
Text alignment
<div class="chip-group" id="alignChips"></div>
</label>
<label>
Mood
<select id="moodSelect">
<option>Bold</option>
<option>Playful</option>
<option>Minimal</option>
<option>Futuristic</option>
</select>
</label>
<div class="form-actions">
<button type="submit" class="primary">Generate thumbnail</button>
<button type="button" class="ghost" id="resetBtn">Reset</button>
</div>
</form>
</section>

<section class="tips">
<h2>AI creative tips</h2>
<div class="tip-grid">
<article>
<h3>Faces & focus</h3>
<p>Add a bold subject area for a face or product cut-out.</p>
</article>
<article>
<h3>Contrast</h3>
<p>Pick a background palette with strong contrast to make text pop.</p>
</article>
<article>
<h3>Short hooks</h3>
<p>Keep the hook under 3 words to create curiosity.</p>
</article>
<article>
<h3>Story cues</h3>
<p>Use arrows or rings around important focus points.</p>
</article>
</div>
</section>
</main>
</div>

<script src="app.js"></script>
</body>
</html>
Loading