-
Notifications
You must be signed in to change notification settings - Fork 0
Add AI YouTube thumbnail generator UI #1
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
linus145
wants to merge
1
commit into
main
Choose a base branch
from
codex/create-ai-youtube-thumbnail-generator-app
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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(); | ||
| 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(); | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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> |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Calling
shuffle()inside the reset handler overrides the defaultstyle,palette, andalignmentvalues 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 👍 / 👎.