Feat: Main entry point with Alpine app shell

This commit is contained in:
2026-03-20 21:54:09 -07:00
parent 325e2aa947
commit 7e25c0bb1b

View File

@@ -3,159 +3,184 @@
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>vibegif.lol</title>
<!-- Tailwind CSS -->
<script src="https://cdn.tailwindcss.com"></script>
<!-- Custom Styles -->
<link rel="stylesheet" href="style.css">
<!-- Lucide Icons -->
<title>vibegif.lol — AI Generated Gifs</title>
<link rel="stylesheet" href="/styles.css">
<script src="https://cdn.jsdelivr.net/npm/alpinejs@3/dist/cdn.min.js" defer></script>
<script src="https://unpkg.com/lucide@latest"></script>
<!-- Alpine.js -->
<script defer src="https://cdn.jsdelivr.net/npm/alpinejs@3.x.x/dist/cdn.min.js"></script>
<!-- Gifshot for Client-Side GIF Generation -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/gifshot/0.3.2/gifshot.min.js"></script>
<!-- App Logic -->
<script src="app.js" defer></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/gif.js/0.2.0/gif.js"></script>
</head>
<body class="bg-gray-50 text-gray-900 min-h-screen flex flex-col" x-data="gifApp()">
<body class="min-h-screen bg-white text-neutral-800" x-data="app()" x-init="init()">
<!-- Header -->
<header class="p-4 border-b border-gray-200 bg-white flex items-center justify-between">
<div class="flex items-center space-x-4">
<button @click="isSettingsOpen = true" class="text-gray-400 hover:text-gray-800 transition" title="Account Settings">
<i data-lucide="panel-left"></i>
<header class="fixed top-0 inset-x-0 z-50 bg-white/80 backdrop-blur border-b border-neutral-200">
<div class="max-w-2xl mx-auto px-4 h-14 flex items-center justify-between">
<h1 class="text-2xl tracking-tight">vibegif<span class="text-neutral-400">.lol</span></h1>
<button @click="showSettings = true" class="p-2 rounded-lg hover:bg-neutral-100 transition">
<i data-lucide="panel-left" class="w-5 h-5 text-neutral-500"></i>
</button>
<h1 class="text-2xl font-bold tracking-tight">vibegif.lol</h1>
</div>
<div class="text-sm text-gray-500">AI Generated Gifs</div>
</header>
<!-- Main Workspace -->
<main class="flex-grow p-4 md:p-8 max-w-4xl mx-auto w-full">
<!-- Settings Modal -->
<template x-if="showSettings">
<div class="fixed inset-0 z-[60] flex items-center justify-center bg-black/30 backdrop-blur-sm" @click.self="showSettings = false">
<div class="bg-white rounded-2xl shadow-xl w-full max-w-md mx-4 p-6" @click.stop>
<div class="flex items-center justify-between mb-6">
<h2 class="text-xl">account settings</h2>
<button @click="showSettings = false" class="p-1 rounded-lg hover:bg-neutral-100">
<i data-lucide="x" class="w-5 h-5"></i>
</button>
</div>
<label class="block text-sm text-neutral-500 mb-2">openrouter api key</label>
<input type="password" x-model="apiKey" @input="saveApiKey()"
placeholder="sk-or-..."
class="w-full px-4 py-3 rounded-xl border border-neutral-200 bg-neutral-50 focus:outline-none focus:ring-2 focus:ring-neutral-300 font-mono text-sm">
<p class="mt-3 text-xs text-neutral-400">your key is stored locally and never sent anywhere except openrouter.</p>
</div>
</div>
</template>
<!-- Controls Panel -->
<div class="bg-white p-6 rounded-lg shadow-sm border border-gray-200 mb-8 space-y-6">
<!-- Main Content -->
<main class="pt-20 pb-32 px-4 max-w-2xl mx-auto">
<div x-show="error" class="bg-red-50 text-red-600 p-3 rounded text-sm mb-4" x-text="error" x-cloak></div>
<!-- Onboarding / Key Prompt -->
<template x-if="!apiKey">
<div class="text-center py-20">
<div class="text-6xl mb-4">✏️</div>
<h2 class="text-2xl mb-2">welcome to vibegif</h2>
<p class="text-neutral-500 mb-6">ai-generated gifs from simple prompts</p>
<label class="block text-sm text-neutral-500 mb-2">enter your openrouter api key to start</label>
<input type="password" x-model="apiKey" @input="saveApiKey()"
placeholder="sk-or-..."
class="w-full max-w-sm mx-auto px-4 py-3 rounded-xl border border-neutral-200 bg-neutral-50 focus:outline-none focus:ring-2 focus:ring-neutral-300 font-mono text-sm">
<p class="mt-3 text-xs text-neutral-400">get one at <a href="https://openrouter.ai/keys" target="_blank" class="underline">openrouter.ai/keys</a></p>
</div>
</template>
<!-- Generator -->
<template x-if="apiKey">
<div>
<label class="block text-sm text-gray-600 mb-1">Simple Prompt</label>
<input type="text" x-model="userPrompt" placeholder="e.g. rolling cat" class="w-full border-gray-300 rounded p-3 border focus:outline-none focus:ring-2 focus:ring-gray-400 text-lg bg-gray-50">
<p class="text-xs text-gray-400 mt-2">Keep it very simple! We automatically append the 'kawaii doodle' style prompts for you.</p>
<!-- Prompt Input -->
<div class="space-y-4 mb-8">
<div>
<label class="block text-sm text-neutral-500 mb-2">prompt</label>
<input type="text" x-model="userPrompt"
placeholder="rolling cat"
class="w-full px-4 py-3 rounded-xl border border-neutral-200 bg-neutral-50 focus:outline-none focus:ring-2 focus:ring-neutral-300"
:disabled="generating">
<p class="mt-1 text-xs text-neutral-400">keep it simple — e.g. "dancing star", "bouncing ball", "waving hand"</p>
</div>
<div class="grid grid-cols-2 md:grid-cols-5 gap-4">
<!-- Options Row -->
<div class="grid grid-cols-2 sm:grid-cols-4 gap-3">
<!-- Model -->
<div>
<label class="block text-xs text-gray-600 mb-1">Frame Count</label>
<input type="number" x-model.number="frameCount" min="2" max="20" class="w-full border border-gray-300 rounded p-2 focus:outline-none focus:ring-2 focus:ring-gray-400 bg-gray-50">
</div>
<div>
<label class="block text-xs text-gray-600 mb-1">FPS</label>
<input type="number" x-model.number="fps" min="1" max="15" class="w-full border border-gray-300 rounded p-2 focus:outline-none focus:ring-2 focus:ring-gray-400 bg-gray-50">
</div>
<div>
<label class="block text-xs text-gray-600 mb-1">Model</label>
<select x-model="model" class="w-full border border-gray-300 rounded p-2 focus:outline-none focus:ring-2 focus:ring-gray-400 bg-gray-50">
<option value="google/gemini-3.1-flash-image-preview">Gemini 3.1 Preview</option>
<option value="bytedance-seed/seedream-4.5">Seedream 4.5</option>
<label class="block text-xs text-neutral-500 mb-1">model</label>
<select x-model="model" class="w-full px-3 py-2 rounded-lg border border-neutral-200 bg-neutral-50 text-sm focus:outline-none" :disabled="generating">
<option value="google/gemini-3.1-flash-image-preview">gemini flash</option>
<option value="bytedance-seed/seedream-4.5">seedream 4.5</option>
</select>
</div>
<!-- Frames -->
<div>
<label class="block text-xs text-gray-600 mb-1">Image Size</label>
<select x-model="imageSize" class="w-full border border-gray-300 rounded p-2 focus:outline-none focus:ring-2 focus:ring-gray-400 bg-gray-50">
<label class="block text-xs text-neutral-500 mb-1">frames</label>
<select x-model.number="frameCount" class="w-full px-3 py-2 rounded-lg border border-neutral-200 bg-neutral-50 text-sm focus:outline-none" :disabled="generating">
<template x-for="n in [2,3,4,5,6,7,8]" :key="n">
<option :value="n" x-text="n"></option>
</template>
</select>
</div>
<!-- FPS -->
<div>
<label class="block text-xs text-neutral-500 mb-1">fps</label>
<select x-model.number="fps" class="w-full px-3 py-2 rounded-lg border border-neutral-200 bg-neutral-50 text-sm focus:outline-none" :disabled="generating">
<template x-for="f in [2,4,6,8,10,12]" :key="f">
<option :value="f" x-text="f"></option>
</template>
</select>
</div>
<!-- Size -->
<div>
<label class="block text-xs text-neutral-500 mb-1">size</label>
<select x-model="imageSize" class="w-full px-3 py-2 rounded-lg border border-neutral-200 bg-neutral-50 text-sm focus:outline-none" :disabled="generating">
<option value="1K">1K</option>
<option value="0.5K" x-show="model === 'google/gemini-3.1-flash-image-preview'">0.5K</option>
</select>
</div>
</div>
<!-- Aspect Ratio -->
<div>
<label class="block text-xs text-gray-600 mb-1">Aspect Ratio</label>
<select x-model="aspectRatio" class="w-full border border-gray-300 rounded p-2 focus:outline-none focus:ring-2 focus:ring-gray-400 bg-gray-50">
<option value="1:1">1:1</option>
<option value="16:9">16:9</option>
<option value="4:3">4:3</option>
<option value="3:4">3:4</option>
<option value="9:16">9:16</option>
</select>
<label class="block text-xs text-neutral-500 mb-1">aspect ratio</label>
<div class="flex flex-wrap gap-2">
<template x-for="ar in ['1:1','16:9','9:16','4:3','3:4']" :key="ar">
<button
@click="aspectRatio = ar"
:class="aspectRatio === ar ? 'bg-neutral-800 text-white' : 'bg-neutral-100 text-neutral-600 hover:bg-neutral-200'"
class="px-3 py-1.5 rounded-lg text-xs font-medium transition"
:disabled="generating"
x-text="ar">
</button>
</template>
</div>
</div>
<button @click="generateGif()" :disabled="isGenerating" class="w-full bg-gray-800 text-white font-bold py-3 px-4 rounded hover:bg-gray-900 disabled:opacity-50 disabled:cursor-not-allowed transition duration-200">
<span x-show="!isGenerating">Generate GIF</span>
<span x-show="isGenerating" x-cloak>
<i data-lucide="loader-2" class="inline animate-spin mr-2 w-5 h-5 align-middle"></i>
Generating Frame <span x-text="currentFrameIndex"></span> of <span x-text="frameCount"></span>...
<!-- Generate Button -->
<button @click="generate()"
:disabled="generating || !userPrompt.trim()"
class="w-full py-3 rounded-xl font-medium transition"
:class="generating || !userPrompt.trim() ? 'bg-neutral-100 text-neutral-400 cursor-not-allowed' : 'bg-neutral-800 text-white hover:bg-neutral-700'">
<span x-show="!generating">generate gif</span>
<span x-show="generating" class="flex items-center justify-center gap-2">
<svg class="animate-spin h-4 w-4" viewBox="0 0 24 24"><circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4" fill="none"/><path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z"/></svg>
<span x-text="statusText">generating...</span>
</span>
</button>
</div>
<!-- Output Display -->
<div x-show="frames.length > 0" class="bg-white p-6 rounded-lg shadow-sm border border-gray-200" x-cloak>
<h3 class="text-lg font-bold mb-4 text-gray-800">Frames Pipeline</h3>
<div class="grid grid-cols-2 md:grid-cols-4 gap-4 mb-8">
<template x-for="(frame, index) in frames" :key="index">
<div class="relative border border-gray-200 rounded overflow-hidden aspect-square bg-gray-100 flex items-center justify-center">
<!-- Frames Preview -->
<div x-show="frames.length > 0" class="mb-8">
<h3 class="text-sm text-neutral-500 mb-3">frames <span x-text="frames.length + '/' + frameCount"></span></h3>
<div class="grid grid-cols-4 gap-2">
<template x-for="(frame, i) in frames" :key="i">
<div class="aspect-square rounded-lg overflow-hidden border border-neutral-200 bg-neutral-50">
<img :src="frame" class="w-full h-full object-cover">
<div class="absolute top-2 left-2 bg-black bg-opacity-60 text-white text-xs px-2 py-1 rounded" x-text="'#' + (index + 1)"></div>
</div>
</template>
<!-- Generating Placeholder -->
<div x-show="isGenerating && currentFrameIndex > frames.length" class="border border-dashed border-gray-300 rounded aspect-square flex items-center justify-center bg-gray-50">
<i data-lucide="loader-2" class="animate-spin text-gray-300 w-8 h-8"></i>
<template x-for="i in Math.max(0, frameCount - frames.length)" :key="'p'+i">
<div class="aspect-square rounded-lg border border-dashed border-neutral-200 bg-neutral-50 flex items-center justify-center">
<div x-show="generating && i === 1" class="w-5 h-5 border-2 border-neutral-300 border-t-neutral-600 rounded-full animate-spin"></div>
</div>
</template>
</div>
</div>
<!-- Final GIF Result -->
<div x-show="finalGifUrl" class="border-t border-gray-100 pt-8" x-cloak>
<h3 class="text-lg font-bold mb-4 text-center text-gray-800">Your Vibe GIF</h3>
<div class="flex flex-col items-center">
<img :src="finalGifUrl" class="rounded shadow-lg max-w-full mb-6 border border-gray-200">
<button @click="downloadGif()" class="flex items-center space-x-2 bg-gray-800 text-white px-6 py-3 rounded hover:bg-gray-900 transition duration-200 shadow-md">
<i data-lucide="download" class="w-5 h-5"></i>
<span>Download GIF</span>
</button>
<!-- Result -->
<div x-show="gifUrl" class="text-center">
<h3 class="text-sm text-neutral-500 mb-3">your gif</h3>
<div class="inline-block rounded-2xl overflow-hidden border border-neutral-200 shadow-lg">
<img :src="gifUrl" class="max-w-full">
</div>
<div class="mt-4">
<a :href="gifUrl" :download="'vibegif-' + Date.now() + '.gif'"
class="inline-flex items-center gap-2 px-6 py-3 bg-neutral-800 text-white rounded-xl hover:bg-neutral-700 transition">
<i data-lucide="download" class="w-4 h-4"></i>
download gif
</a>
</div>
</div>
<!-- Error -->
<div x-show="errorMsg" class="mt-4 p-4 rounded-xl bg-red-50 text-red-600 text-sm" x-text="errorMsg"></div>
</div>
</template>
</main>
<!-- Account Settings Modal -->
<div x-show="isSettingsOpen" class="fixed inset-0 bg-gray-900 bg-opacity-50 flex items-center justify-center z-50 backdrop-blur-sm" x-cloak>
<div class="bg-white p-8 rounded-lg shadow-xl w-full max-w-md border border-gray-200" @click.away="isSettingsOpen = false">
<h2 class="text-xl mb-4 font-bold text-gray-800 flex items-center">
<i data-lucide="key" class="mr-2 w-5 h-5 text-gray-500"></i> Account Settings
</h2>
<label class="block text-sm text-gray-600 mb-2">OpenRouter API Key</label>
<input type="password" x-model="apiKey" placeholder="sk-or-v1-..." class="w-full border border-gray-300 bg-gray-50 rounded p-3 mb-6 focus:outline-none focus:ring-2 focus:ring-gray-400">
<div class="flex justify-end space-x-3">
<button @click="isSettingsOpen = false" class="px-5 py-2 text-gray-600 hover:bg-gray-100 rounded transition font-medium">Cancel</button>
<button @click="saveApiKey()" class="px-5 py-2 bg-gray-800 text-white rounded hover:bg-gray-900 transition font-medium shadow">Save</button>
</div>
</div>
</div>
<!-- Footer -->
<footer class="fixed bottom-0 inset-x-0 bg-white/80 backdrop-blur border-t border-neutral-100 py-3 text-center text-xs text-neutral-400">
byok · your key never leaves your browser
</footer>
<script src="/app.js"></script>
</body>
</html>
```[vibegif/vibegif.lol@main/style.css](https://github.com/vibegif/vibegif.lol/blob/main/style.css "Feat: Add custom font Stain and base styling")
```css
@font-face {
font-family: "Stain";
src: url("https://cdn.jsdelivr.net/gh/multipleof4/stain.otf@master/dist/Stain.otf") format("opentype");
font-weight: normal;
font-style: normal;
}
body {
font-family: "Stain", sans-serif;
}
/* Hide Alpine elements before load to prevent flicker */
[x-cloak] {
display: none !important;
}