feat: add typing carousel with promotional messages
- 5 rotating promotional commands with typing effect - Progress bar and navigation dots - Messages: instant deploy, DDoS protection, pricing, Telegram bot, regions Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
172
index.html
172
index.html
@@ -137,22 +137,44 @@
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- Interactive Command -->
|
||||
<div class="w-full max-w-2xl mt-6 group">
|
||||
<div class="bg-terminal-bg border border-terminal-border rounded flex items-center p-3 md:p-4 gap-3 shadow-lg group-hover:border-primary/50 transition-colors">
|
||||
<span class="text-primary font-bold select-none">$</span>
|
||||
<div class="flex-1 text-sm md:text-base flex items-center gap-2 overflow-x-auto">
|
||||
<span class="text-terminal-purple">incus</span>
|
||||
<span class="text-terminal-text">launch</span>
|
||||
<span class="text-terminal-cyan">ubuntu:24.04</span>
|
||||
<span class="text-terminal-amber">tokyo-1</span>
|
||||
<span class="text-terminal-muted">--vm</span>
|
||||
<!-- Typing Carousel -->
|
||||
<div class="w-full max-w-3xl mt-6" x-data="typingCarousel()">
|
||||
<div class="bg-terminal-bg border border-terminal-border rounded-lg overflow-hidden shadow-lg">
|
||||
<!-- Terminal Header -->
|
||||
<div class="bg-[#161b22] px-4 py-2 border-b border-terminal-border flex items-center gap-2">
|
||||
<span class="text-terminal-muted text-xs">anvil-cli v2.4.0</span>
|
||||
<span class="ml-auto text-xs text-terminal-muted" x-text="(currentIndex + 1) + '/' + commands.length"></span>
|
||||
</div>
|
||||
<!-- Command Display -->
|
||||
<div class="p-4 md:p-6 min-h-[120px]">
|
||||
<div class="flex items-start gap-3">
|
||||
<span class="text-primary font-bold select-none">$</span>
|
||||
<div class="flex-1">
|
||||
<div class="text-sm md:text-base flex flex-wrap items-center gap-1">
|
||||
<span class="text-terminal-text" x-html="displayedCommand"></span>
|
||||
<span class="text-primary cursor-blink text-lg" x-show="isTyping">▋</span>
|
||||
</div>
|
||||
<!-- Result Message -->
|
||||
<div class="mt-4 text-terminal-muted text-sm border-l-2 border-primary/50 pl-3 transition-opacity duration-300"
|
||||
:class="showResult ? 'opacity-100' : 'opacity-0'">
|
||||
<span class="text-primary">✓</span>
|
||||
<span x-text="commands[currentIndex]?.result"></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Progress Bar -->
|
||||
<div class="h-1 bg-terminal-border">
|
||||
<div class="h-full bg-primary transition-all duration-100 ease-linear" :style="'width: ' + progress + '%'"></div>
|
||||
</div>
|
||||
<span class="text-xs text-terminal-muted bg-terminal-border/30 px-2 py-1 rounded hidden sm:inline-block">ENTER ↵</span>
|
||||
</div>
|
||||
<div class="pl-4 mt-2 text-xs text-terminal-muted flex items-center gap-2">
|
||||
<span class="w-2 h-2 rounded-full bg-primary animate-pulse"></span>
|
||||
Instance creating... (0.4s)
|
||||
<!-- Navigation Dots -->
|
||||
<div class="flex justify-center gap-2 mt-4">
|
||||
<template x-for="(cmd, idx) in commands" :key="idx">
|
||||
<button @click="goTo(idx)"
|
||||
class="w-2 h-2 rounded-full transition-all"
|
||||
:class="idx === currentIndex ? 'bg-primary w-6' : 'bg-terminal-border hover:bg-terminal-muted'"></button>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -353,6 +375,128 @@
|
||||
<!-- Alpine.js -->
|
||||
<script defer src="https://cdn.jsdelivr.net/npm/alpinejs@3.14.3/dist/cdn.min.js"></script>
|
||||
<script>
|
||||
function typingCarousel() {
|
||||
return {
|
||||
commands: [
|
||||
{
|
||||
text: '<span class="text-terminal-purple">anvil</span> <span class="text-terminal-text">deploy</span> <span class="text-terminal-cyan">--tokyo</span> <span class="text-terminal-amber">--instant</span>',
|
||||
plain: 'anvil deploy --tokyo --instant',
|
||||
result: '0.4초 만에 서버 생성 완료. VM 부팅 대기? 그런 건 없습니다.'
|
||||
},
|
||||
{
|
||||
text: '<span class="text-terminal-purple">anvil</span> <span class="text-terminal-text">protect</span> <span class="text-terminal-cyan">--ddos</span> <span class="text-terminal-amber">1Tbps+</span>',
|
||||
plain: 'anvil protect --ddos 1Tbps+',
|
||||
result: 'ForgeShield 무료 제공. DDoS 걱정은 이제 그만.'
|
||||
},
|
||||
{
|
||||
text: '<span class="text-terminal-purple">anvil</span> <span class="text-terminal-text">pricing</span> <span class="text-terminal-cyan">--monthly</span> <span class="text-terminal-amber">₩7,000~</span>',
|
||||
plain: 'anvil pricing --monthly ₩7,000~',
|
||||
result: '커피 두 잔 값으로 나만의 서버를. 연간 결제 시 17% 할인.'
|
||||
},
|
||||
{
|
||||
text: '<span class="text-terminal-purple">anvil</span> <span class="text-terminal-text">connect</span> <span class="text-terminal-cyan">@AnvilForgeBot</span>',
|
||||
plain: 'anvil connect @AnvilForgeBot',
|
||||
result: '텔레그램으로 서버 관리. 24시간 어디서든 컨트롤.'
|
||||
},
|
||||
{
|
||||
text: '<span class="text-terminal-purple">anvil</span> <span class="text-terminal-text">regions</span> <span class="text-terminal-cyan">--list</span>',
|
||||
plain: 'anvil regions --list',
|
||||
result: '도쿄 · 오사카 · 서울 · 싱가포르. 아시아 전역 커버.'
|
||||
},
|
||||
],
|
||||
currentIndex: 0,
|
||||
displayedCommand: '',
|
||||
isTyping: true,
|
||||
showResult: false,
|
||||
progress: 0,
|
||||
charIndex: 0,
|
||||
intervalId: null,
|
||||
progressId: null,
|
||||
|
||||
init() {
|
||||
this.startTyping();
|
||||
},
|
||||
|
||||
startTyping() {
|
||||
this.isTyping = true;
|
||||
this.showResult = false;
|
||||
this.displayedCommand = '';
|
||||
this.charIndex = 0;
|
||||
this.progress = 0;
|
||||
|
||||
const plainText = this.commands[this.currentIndex].plain;
|
||||
const htmlText = this.commands[this.currentIndex].text;
|
||||
|
||||
// Clear any existing intervals
|
||||
if (this.intervalId) clearInterval(this.intervalId);
|
||||
if (this.progressId) clearInterval(this.progressId);
|
||||
|
||||
// Typing effect
|
||||
this.intervalId = setInterval(() => {
|
||||
if (this.charIndex < plainText.length) {
|
||||
this.charIndex++;
|
||||
// Map plain text position to HTML
|
||||
this.displayedCommand = this.getPartialHtml(htmlText, this.charIndex);
|
||||
} else {
|
||||
clearInterval(this.intervalId);
|
||||
this.isTyping = false;
|
||||
this.showResult = true;
|
||||
this.startProgress();
|
||||
}
|
||||
}, 50);
|
||||
},
|
||||
|
||||
getPartialHtml(html, charCount) {
|
||||
let result = '';
|
||||
let visibleChars = 0;
|
||||
let inTag = false;
|
||||
|
||||
for (let i = 0; i < html.length && visibleChars < charCount; i++) {
|
||||
if (html[i] === '<') inTag = true;
|
||||
if (!inTag) visibleChars++;
|
||||
result += html[i];
|
||||
if (html[i] === '>') inTag = false;
|
||||
}
|
||||
|
||||
// Close any open tags
|
||||
const openTags = result.match(/<span[^>]*>/g) || [];
|
||||
const closeTags = result.match(/<\/span>/g) || [];
|
||||
const unclosed = openTags.length - closeTags.length;
|
||||
for (let i = 0; i < unclosed; i++) {
|
||||
result += '</span>';
|
||||
}
|
||||
|
||||
return result;
|
||||
},
|
||||
|
||||
startProgress() {
|
||||
const duration = 4000; // 4 seconds display time
|
||||
const interval = 50;
|
||||
const step = (interval / duration) * 100;
|
||||
|
||||
this.progressId = setInterval(() => {
|
||||
this.progress += step;
|
||||
if (this.progress >= 100) {
|
||||
clearInterval(this.progressId);
|
||||
this.next();
|
||||
}
|
||||
}, interval);
|
||||
},
|
||||
|
||||
next() {
|
||||
this.currentIndex = (this.currentIndex + 1) % this.commands.length;
|
||||
this.startTyping();
|
||||
},
|
||||
|
||||
goTo(index) {
|
||||
if (this.intervalId) clearInterval(this.intervalId);
|
||||
if (this.progressId) clearInterval(this.progressId);
|
||||
this.currentIndex = index;
|
||||
this.startTyping();
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
function pricingTerminal() {
|
||||
return {
|
||||
// State
|
||||
|
||||
Reference in New Issue
Block a user