akurapluz / index.html
mrpoddaa's picture
Upload 3 files
7c55bfb verified
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>FontShift β€” Font Converter</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link href="https://fonts.googleapis.com/css2?family=Syne:wght@400;600;700;800&family=DM+Mono:ital,wght@0,400;0,500;1,400&family=Instrument+Serif:ital@0;1&display=swap" rel="stylesheet">
<style>
:root {
--bg: #0a0a0f;
--surface: #111118;
--surface2: #1a1a25;
--border: #2a2a3a;
--accent: #7c5cfc;
--accent2: #c084fc;
--gold: #f59e0b;
--green: #10b981;
--red: #ef4444;
--text: #e8e8f0;
--text-muted: #6b6b80;
--text-dim: #9999aa;
--font-display: 'Syne', sans-serif;
--font-mono: 'DM Mono', monospace;
--font-serif: 'Instrument Serif', serif;
--glow: 0 0 40px rgba(124, 92, 252, 0.15);
}
* { box-sizing: border-box; margin: 0; padding: 0; }
body {
background: var(--bg);
color: var(--text);
font-family: var(--font-display);
min-height: 100vh;
overflow-x: hidden;
}
/* Background grid */
body::before {
content: '';
position: fixed;
inset: 0;
background-image:
linear-gradient(rgba(124,92,252,0.03) 1px, transparent 1px),
linear-gradient(90deg, rgba(124,92,252,0.03) 1px, transparent 1px);
background-size: 40px 40px;
pointer-events: none;
z-index: 0;
}
/* Glow blobs */
.blob1, .blob2 {
position: fixed;
border-radius: 50%;
filter: blur(100px);
pointer-events: none;
z-index: 0;
opacity: 0.4;
}
.blob1 {
width: 500px; height: 500px;
background: radial-gradient(circle, #7c5cfc22, transparent 70%);
top: -100px; left: -100px;
animation: drift 20s ease-in-out infinite alternate;
}
.blob2 {
width: 400px; height: 400px;
background: radial-gradient(circle, #c084fc18, transparent 70%);
bottom: 100px; right: -50px;
animation: drift 25s ease-in-out infinite alternate-reverse;
}
@keyframes drift {
from { transform: translate(0,0) scale(1); }
to { transform: translate(30px, 50px) scale(1.1); }
}
/* ── Layout ── */
.wrapper {
position: relative;
z-index: 1;
max-width: 900px;
margin: 0 auto;
padding: 0 24px;
}
/* ── Header ── */
header {
padding: 48px 0 32px;
text-align: center;
}
.logo-mark {
display: inline-flex;
align-items: center;
gap: 10px;
margin-bottom: 24px;
padding: 8px 16px;
border: 1px solid var(--border);
border-radius: 100px;
background: var(--surface);
font-size: 12px;
font-family: var(--font-mono);
color: var(--accent2);
letter-spacing: 0.1em;
text-transform: uppercase;
}
.logo-mark span {
width: 6px; height: 6px;
background: var(--green);
border-radius: 50%;
animation: pulse-dot 2s ease-in-out infinite;
}
@keyframes pulse-dot {
0%,100% { opacity: 1; box-shadow: 0 0 0 0 rgba(16,185,129,0.4); }
50% { opacity: 0.8; box-shadow: 0 0 0 6px rgba(16,185,129,0); }
}
h1 {
font-size: clamp(3rem, 7vw, 5.5rem);
font-weight: 800;
line-height: 0.95;
letter-spacing: -0.04em;
margin-bottom: 16px;
background: linear-gradient(135deg, #fff 0%, var(--accent2) 50%, var(--accent) 100%);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
}
.subtitle {
font-family: var(--font-serif);
font-style: italic;
font-size: 1.2rem;
color: var(--text-dim);
margin-bottom: 8px;
}
.formats-badge {
display: flex;
justify-content: center;
gap: 8px;
flex-wrap: wrap;
margin-top: 20px;
}
.badge {
padding: 4px 12px;
background: var(--surface2);
border: 1px solid var(--border);
border-radius: 100px;
font-family: var(--font-mono);
font-size: 11px;
color: var(--accent2);
letter-spacing: 0.05em;
}
/* ── Upload Area ── */
.upload-section {
margin: 40px 0 24px;
}
.drop-zone {
border: 2px dashed var(--border);
border-radius: 20px;
padding: 56px 40px;
text-align: center;
cursor: pointer;
transition: all 0.3s ease;
background: var(--surface);
position: relative;
overflow: hidden;
}
.drop-zone::before {
content: '';
position: absolute;
inset: 0;
background: radial-gradient(ellipse at center, rgba(124,92,252,0.06) 0%, transparent 70%);
opacity: 0;
transition: opacity 0.3s;
}
.drop-zone:hover, .drop-zone.drag-over {
border-color: var(--accent);
box-shadow: var(--glow), inset 0 0 40px rgba(124,92,252,0.05);
transform: translateY(-2px);
}
.drop-zone:hover::before, .drop-zone.drag-over::before {
opacity: 1;
}
.drop-icon {
font-size: 3rem;
margin-bottom: 16px;
display: block;
animation: float 3s ease-in-out infinite;
}
@keyframes float {
0%,100% { transform: translateY(0); }
50% { transform: translateY(-6px); }
}
.drop-title {
font-size: 1.1rem;
font-weight: 600;
margin-bottom: 8px;
color: var(--text);
}
.drop-sub {
font-family: var(--font-mono);
font-size: 12px;
color: var(--text-muted);
}
#file-input { display: none; }
/* ── Font Info ── */
.font-info-card {
background: var(--surface);
border: 1px solid var(--border);
border-radius: 16px;
padding: 20px 24px;
margin-bottom: 24px;
display: none;
animation: slideIn 0.3s ease;
}
.font-info-card.visible { display: block; }
@keyframes slideIn {
from { opacity: 0; transform: translateY(10px); }
to { opacity: 1; transform: translateY(0); }
}
.font-info-header {
display: flex;
align-items: center;
gap: 12px;
margin-bottom: 16px;
}
.file-icon {
width: 44px; height: 44px;
background: linear-gradient(135deg, var(--accent), var(--accent2));
border-radius: 10px;
display: flex;
align-items: center;
justify-content: center;
font-size: 20px;
flex-shrink: 0;
}
.file-meta h3 {
font-size: 0.95rem;
font-weight: 600;
margin-bottom: 2px;
}
.file-meta span {
font-family: var(--font-mono);
font-size: 11px;
color: var(--text-muted);
}
.info-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(140px, 1fr));
gap: 12px;
}
.info-item label {
display: block;
font-size: 10px;
font-family: var(--font-mono);
color: var(--text-muted);
text-transform: uppercase;
letter-spacing: 0.08em;
margin-bottom: 4px;
}
.info-item span {
font-size: 0.9rem;
font-weight: 600;
color: var(--text);
}
.format-tag {
display: inline-flex;
align-items: center;
padding: 2px 10px;
background: rgba(124,92,252,0.15);
border: 1px solid rgba(124,92,252,0.3);
border-radius: 100px;
font-family: var(--font-mono);
font-size: 12px;
color: var(--accent2);
font-weight: 500;
}
/* ── Format Selector ── */
.format-section {
margin-bottom: 24px;
}
.section-label {
font-size: 11px;
font-family: var(--font-mono);
text-transform: uppercase;
letter-spacing: 0.1em;
color: var(--text-muted);
margin-bottom: 12px;
}
.format-grid {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 10px;
}
@media (max-width: 480px) {
.format-grid { grid-template-columns: repeat(2, 1fr); }
}
.format-toggle {
position: relative;
cursor: pointer;
}
.format-toggle input { display: none; }
.format-box {
padding: 16px 12px;
border: 1.5px solid var(--border);
border-radius: 12px;
text-align: center;
background: var(--surface);
transition: all 0.2s ease;
user-select: none;
}
.format-box .fmt-name {
font-size: 1rem;
font-weight: 700;
font-family: var(--font-mono);
margin-bottom: 4px;
color: var(--text-dim);
transition: color 0.2s;
}
.format-box .fmt-desc {
font-size: 10px;
color: var(--text-muted);
}
.format-toggle input:checked + .format-box {
border-color: var(--accent);
background: rgba(124,92,252,0.1);
box-shadow: 0 0 20px rgba(124,92,252,0.1);
}
.format-toggle input:checked + .format-box .fmt-name {
color: var(--accent2);
}
.format-toggle.source-format .format-box {
opacity: 0.4;
cursor: not-allowed;
border-style: dashed;
}
.format-toggle.source-format .format-box .fmt-desc::after {
content: ' (source)';
}
/* ── HuggingFace Config ── */
.hf-config {
background: var(--surface);
border: 1px solid var(--border);
border-radius: 16px;
padding: 20px 24px;
margin-bottom: 24px;
}
.hf-config-header {
display: flex;
align-items: center;
gap: 8px;
margin-bottom: 16px;
font-size: 0.9rem;
font-weight: 600;
}
.hf-icon { font-size: 1.2rem; }
.hf-row {
display: grid;
grid-template-columns: 1fr auto;
gap: 10px;
align-items: end;
}
.input-group label {
display: block;
font-size: 10px;
font-family: var(--font-mono);
text-transform: uppercase;
letter-spacing: 0.08em;
color: var(--text-muted);
margin-bottom: 6px;
}
.input-group input {
width: 100%;
background: var(--bg);
border: 1.5px solid var(--border);
border-radius: 10px;
padding: 10px 14px;
color: var(--text);
font-family: var(--font-mono);
font-size: 13px;
outline: none;
transition: border-color 0.2s;
}
.input-group input:focus { border-color: var(--accent); }
.input-group input::placeholder { color: var(--text-muted); }
/* ── Convert Button ── */
.convert-btn {
width: 100%;
padding: 18px;
background: linear-gradient(135deg, var(--accent), #9c5cfc);
border: none;
border-radius: 14px;
color: white;
font-family: var(--font-display);
font-size: 1rem;
font-weight: 700;
letter-spacing: 0.02em;
cursor: pointer;
transition: all 0.3s ease;
position: relative;
overflow: hidden;
margin-bottom: 24px;
}
.convert-btn::before {
content: '';
position: absolute;
inset: 0;
background: linear-gradient(135deg, rgba(255,255,255,0.1), transparent);
opacity: 0;
transition: opacity 0.3s;
}
.convert-btn:hover:not(:disabled) {
transform: translateY(-2px);
box-shadow: 0 8px 30px rgba(124,92,252,0.4);
}
.convert-btn:hover::before { opacity: 1; }
.convert-btn:active:not(:disabled) { transform: translateY(0); }
.convert-btn:disabled {
opacity: 0.5;
cursor: not-allowed;
}
.convert-btn.loading {
pointer-events: none;
}
.btn-content {
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
}
.spinner {
width: 18px; height: 18px;
border: 2px solid rgba(255,255,255,0.3);
border-top-color: white;
border-radius: 50%;
animation: spin 0.6s linear infinite;
display: none;
}
@keyframes spin { to { transform: rotate(360deg); } }
.convert-btn.loading .spinner { display: block; }
.convert-btn.loading .btn-text::after { content: 'Converting...'; }
.convert-btn:not(.loading) .btn-text::after { content: 'Convert Font'; }
/* ── Progress ── */
.progress-bar-wrap {
height: 3px;
background: var(--surface2);
border-radius: 100px;
margin-bottom: 24px;
overflow: hidden;
display: none;
}
.progress-bar-wrap.visible { display: block; }
.progress-bar {
height: 100%;
background: linear-gradient(90deg, var(--accent), var(--accent2));
border-radius: 100px;
width: 0%;
transition: width 0.3s ease;
animation: shimmer 1.5s ease-in-out infinite;
}
@keyframes shimmer {
0% { filter: brightness(1); }
50% { filter: brightness(1.3); }
100% { filter: brightness(1); }
}
/* ── Result ── */
.result-card {
background: var(--surface);
border: 1px solid var(--border);
border-radius: 16px;
overflow: hidden;
display: none;
animation: slideIn 0.4s ease;
margin-bottom: 24px;
}
.result-card.visible { display: block; }
.result-header {
padding: 16px 24px;
background: rgba(16,185,129,0.08);
border-bottom: 1px solid rgba(16,185,129,0.15);
display: flex;
align-items: center;
gap: 10px;
color: var(--green);
font-weight: 600;
font-size: 0.9rem;
}
.result-header.error {
background: rgba(239,68,68,0.08);
border-bottom-color: rgba(239,68,68,0.15);
color: var(--red);
}
.result-files {
padding: 16px 24px;
display: flex;
flex-direction: column;
gap: 8px;
}
.result-file {
display: flex;
align-items: center;
justify-content: space-between;
padding: 10px 14px;
background: var(--surface2);
border-radius: 10px;
animation: slideIn 0.3s ease;
}
.result-file-name {
font-family: var(--font-mono);
font-size: 13px;
color: var(--text);
display: flex;
align-items: center;
gap: 8px;
}
.result-file-name span { color: var(--text-muted); }
.download-btn {
padding: 6px 14px;
background: rgba(124,92,252,0.12);
border: 1px solid rgba(124,92,252,0.25);
border-radius: 8px;
color: var(--accent2);
font-family: var(--font-mono);
font-size: 11px;
cursor: pointer;
text-decoration: none;
transition: all 0.2s;
}
.download-btn:hover {
background: rgba(124,92,252,0.2);
border-color: var(--accent);
}
.download-all-btn {
margin: 0 24px 16px;
width: calc(100% - 48px);
padding: 12px;
background: var(--green);
border: none;
border-radius: 10px;
color: white;
font-family: var(--font-display);
font-weight: 700;
font-size: 0.9rem;
cursor: pointer;
transition: all 0.2s;
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
text-decoration: none;
}
.download-all-btn:hover {
background: #059669;
transform: translateY(-1px);
box-shadow: 0 6px 20px rgba(16,185,129,0.3);
}
.error-msg {
padding: 16px 24px;
font-family: var(--font-mono);
font-size: 13px;
color: var(--red);
line-height: 1.6;
}
/* ── UptimeRobot Section ── */
.uptime-section {
margin: 40px 0;
background: var(--surface);
border: 1px solid var(--border);
border-radius: 20px;
overflow: hidden;
}
.uptime-header {
padding: 24px 28px 20px;
border-bottom: 1px solid var(--border);
}
.uptime-header h2 {
font-size: 1.2rem;
font-weight: 700;
margin-bottom: 6px;
display: flex;
align-items: center;
gap: 10px;
}
.uptime-header p {
font-family: var(--font-mono);
font-size: 12px;
color: var(--text-muted);
}
.steps-list {
padding: 24px 28px;
display: flex;
flex-direction: column;
gap: 0;
}
.step {
display: flex;
gap: 16px;
padding-bottom: 24px;
position: relative;
}
.step:last-child { padding-bottom: 0; }
.step::before {
content: '';
position: absolute;
left: 15px;
top: 36px;
bottom: 0;
width: 1px;
background: var(--border);
}
.step:last-child::before { display: none; }
.step-num {
width: 32px; height: 32px;
background: linear-gradient(135deg, var(--accent), var(--accent2));
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-size: 12px;
font-weight: 700;
flex-shrink: 0;
font-family: var(--font-mono);
position: relative;
z-index: 1;
}
.step-content h4 {
font-size: 0.9rem;
font-weight: 600;
margin-bottom: 4px;
}
.step-content p {
font-size: 0.85rem;
color: var(--text-dim);
line-height: 1.5;
}
.step-content code {
font-family: var(--font-mono);
font-size: 11px;
background: var(--bg);
padding: 2px 6px;
border-radius: 4px;
border: 1px solid var(--border);
color: var(--accent2);
cursor: pointer;
transition: all 0.2s;
display: inline-block;
margin-top: 4px;
}
.step-content code:hover {
border-color: var(--accent);
background: rgba(124,92,252,0.1);
}
/* ── Footer ── */
footer {
text-align: center;
padding: 40px 0 60px;
font-family: var(--font-mono);
font-size: 11px;
color: var(--text-muted);
border-top: 1px solid var(--border);
margin-top: 40px;
}
footer a {
color: var(--accent2);
text-decoration: none;
}
/* ── Toast ── */
.toast {
position: fixed;
bottom: 24px;
right: 24px;
background: var(--surface2);
border: 1px solid var(--border);
border-radius: 12px;
padding: 12px 20px;
font-size: 0.85rem;
font-family: var(--font-mono);
color: var(--text);
transform: translateY(80px);
opacity: 0;
transition: all 0.3s cubic-bezier(0.34, 1.56, 0.64, 1);
z-index: 100;
pointer-events: none;
}
.toast.show {
transform: translateY(0);
opacity: 1;
}
</style>
</head>
<body>
<div class="blob1"></div>
<div class="blob2"></div>
<div class="wrapper">
<!-- Header -->
<header>
<div class="logo-mark">
<span></span>
Font Converter
</div>
<h1>FontShift</h1>
<p class="subtitle">Any format, every format.</p>
<div class="formats-badge">
<span class="badge">TTF</span>
<span class="badge">OTF</span>
<span class="badge">WOFF</span>
<span class="badge">WOFF2</span>
</div>
</header>
<!-- HuggingFace Config -->
<div class="hf-config">
<div class="hf-config-header">
<span class="hf-icon">πŸ€—</span>
HuggingFace Space Configuration
</div>
<div class="hf-row">
<div class="input-group" style="flex:1">
<label>HuggingFace Space URL</label>
<input type="text" id="hf-url"
placeholder="https://YOUR_USERNAME-font-converter.hf.space"
value="">
</div>
<button onclick="testConnection()" style="
padding: 10px 16px;
background: var(--surface2);
border: 1.5px solid var(--border);
border-radius: 10px;
color: var(--text);
font-family: var(--font-mono);
font-size: 12px;
cursor: pointer;
white-space: nowrap;
transition: all 0.2s;
height: 40px;
" onmouseover="this.style.borderColor='var(--accent)'"
onmouseout="this.style.borderColor='var(--border)'">
πŸ”Œ Test
</button>
</div>
<p style="font-family:var(--font-mono);font-size:11px;color:var(--text-muted);margin-top:10px;">
Deploy the HuggingFace Space first β†’ copy the URL here β†’ start converting fonts
</p>
</div>
<!-- Upload Area -->
<div class="upload-section">
<input type="file" id="file-input" accept=".ttf,.otf,.woff,.woff2">
<div class="drop-zone" id="drop-zone" onclick="document.getElementById('file-input').click()">
<span class="drop-icon">🎨</span>
<div class="drop-title">Drop your font here or click to browse</div>
<div class="drop-sub">Supports TTF Β· OTF Β· WOFF Β· WOFF2</div>
</div>
</div>
<!-- Font Info (shown after upload) -->
<div class="font-info-card" id="font-info-card">
<div class="font-info-header">
<div class="file-icon">Aa</div>
<div class="file-meta">
<h3 id="info-filename">β€”</h3>
<span id="info-filesize">β€”</span>
</div>
</div>
<div class="info-grid">
<div class="info-item">
<label>Format</label>
<span id="info-format">β€”</span>
</div>
<div class="info-item">
<label>File Size</label>
<span id="info-size">β€”</span>
</div>
</div>
</div>
<!-- Format Selection -->
<div class="format-section">
<div class="section-label">Convert to these formats:</div>
<div class="format-grid">
<label class="format-toggle" id="toggle-ttf">
<input type="checkbox" value="TTF" checked>
<div class="format-box">
<div class="fmt-name">TTF</div>
<div class="fmt-desc">TrueType</div>
</div>
</label>
<label class="format-toggle" id="toggle-otf">
<input type="checkbox" value="OTF" checked>
<div class="format-box">
<div class="fmt-name">OTF</div>
<div class="fmt-desc">OpenType</div>
</div>
</label>
<label class="format-toggle" id="toggle-woff">
<input type="checkbox" value="WOFF" checked>
<div class="format-box">
<div class="fmt-name">WOFF</div>
<div class="fmt-desc">Web Font 1.0</div>
</div>
</label>
<label class="format-toggle" id="toggle-woff2">
<input type="checkbox" value="WOFF2" checked>
<div class="format-box">
<div class="fmt-name">WOFF2</div>
<div class="fmt-desc">Web Font 2.0</div>
</div>
</label>
</div>
</div>
<!-- Convert Button -->
<button class="convert-btn" id="convert-btn" onclick="convertFont()" disabled>
<div class="btn-content">
<div class="spinner" id="spinner"></div>
<span class="btn-text"></span>
</div>
</button>
<!-- Progress -->
<div class="progress-bar-wrap" id="progress-wrap">
<div class="progress-bar" id="progress-bar"></div>
</div>
<!-- Result -->
<div class="result-card" id="result-card">
<div class="result-header" id="result-header">
<span id="result-icon">βœ…</span>
<span id="result-msg">Conversion complete!</span>
</div>
<div class="result-files" id="result-files"></div>
<a class="download-all-btn" id="download-all-btn" href="#" style="display:none">
⬇️ Download All as ZIP
</a>
<div class="error-msg" id="error-msg" style="display:none"></div>
</div>
<!-- UptimeRobot Guide -->
<div class="uptime-section">
<div class="uptime-header">
<h2>πŸ’“ Keep Your Space Alive</h2>
<p>Use UptimeRobot to ping your HuggingFace Space every 5 minutes</p>
</div>
<div class="steps-list">
<div class="step">
<div class="step-num">1</div>
<div class="step-content">
<h4>Deploy the HuggingFace Space</h4>
<p>Upload the <code>app.py</code> and <code>requirements.txt</code> files to a new HuggingFace Space (SDK: Gradio).</p>
</div>
</div>
<div class="step">
<div class="step-num">2</div>
<div class="step-content">
<h4>Create free UptimeRobot account</h4>
<p>Go to <a href="https://uptimerobot.com" target="_blank" style="color:var(--accent2)">uptimerobot.com</a> β†’ Sign up for free (monitors up to 50 sites).</p>
</div>
</div>
<div class="step">
<div class="step-num">3</div>
<div class="step-content">
<h4>Add New Monitor</h4>
<p>Monitor type: <strong>HTTP(s)</strong> Β· Interval: <strong>5 minutes</strong></p>
<code onclick="copyText(this)">https://YOUR_USERNAME-font-converter.hf.space/</code>
</div>
</div>
<div class="step">
<div class="step-num">4</div>
<div class="step-content">
<h4>Configure alerts (optional)</h4>
<p>Set up email or Telegram alerts if the Space goes down. The 5-min ping prevents HuggingFace from sleeping the Space.</p>
</div>
</div>
<div class="step">
<div class="step-num">5</div>
<div class="step-content">
<h4>Done! βœ…</h4>
<p>Your Font Converter API will stay active 24/7. HuggingFace Spaces sleep after ~15 min of inactivity β€” UptimeRobot prevents this.</p>
</div>
</div>
</div>
</div>
</div>
<!-- Toast -->
<div class="toast" id="toast"></div>
<footer>
<div class="wrapper">
Built with πŸ€— HuggingFace + fonttools Β·
<a href="https://huggingface.co" target="_blank">HuggingFace Spaces</a> Β·
<a href="https://uptimerobot.com" target="_blank">UptimeRobot</a>
</div>
</footer>
<script>
// ── State ──────────────────────────────────────────────────────────
let currentFile = null;
let sourceFormat = null;
const HF_SPACE_URL_KEY = 'fontshift_hf_url';
// Load saved URL
window.addEventListener('DOMContentLoaded', () => {
const saved = localStorage.getItem(HF_SPACE_URL_KEY);
if (saved) document.getElementById('hf-url').value = saved;
// Save on change
document.getElementById('hf-url').addEventListener('input', (e) => {
localStorage.setItem(HF_SPACE_URL_KEY, e.target.value);
});
});
// ── File Detection ─────────────────────────────────────────────────
function detectFormat(file) {
const name = file.name.toLowerCase();
if (name.endsWith('.ttf')) return 'TTF';
if (name.endsWith('.otf')) return 'OTF';
if (name.endsWith('.woff2')) return 'WOFF2';
if (name.endsWith('.woff')) return 'WOFF';
return null;
}
function formatSize(bytes) {
if (bytes < 1024) return bytes + ' B';
if (bytes < 1024*1024) return (bytes/1024).toFixed(1) + ' KB';
return (bytes/1024/1024).toFixed(2) + ' MB';
}
function handleFileSelect(file) {
if (!file) return;
const fmt = detectFormat(file);
if (!fmt) {
showToast('❌ Unsupported format. Use TTF, OTF, WOFF, or WOFF2');
return;
}
currentFile = file;
sourceFormat = fmt;
// Show font info card
document.getElementById('font-info-card').classList.add('visible');
document.getElementById('info-filename').textContent = file.name;
document.getElementById('info-filesize').textContent = formatSize(file.size);
document.getElementById('info-format').innerHTML = `<span class="format-tag">${fmt}</span>`;
document.getElementById('info-size').textContent = formatSize(file.size);
// Update drop zone
document.querySelector('.drop-title').textContent = `βœ… ${file.name}`;
document.querySelector('.drop-sub').textContent = `${fmt} Β· ${formatSize(file.size)} Β· Click to change`;
// Mark source format
document.querySelectorAll('.format-toggle').forEach(el => {
el.classList.remove('source-format');
const val = el.querySelector('input').value;
if (val === fmt) {
el.classList.add('source-format');
el.querySelector('input').checked = false;
el.querySelector('input').disabled = true;
} else {
el.querySelector('input').disabled = false;
}
});
// Enable convert button
document.getElementById('convert-btn').disabled = false;
// Hide previous results
document.getElementById('result-card').classList.remove('visible');
}
// ── Drag & Drop ────────────────────────────────────────────────────
const dropZone = document.getElementById('drop-zone');
const fileInput = document.getElementById('file-input');
dropZone.addEventListener('dragover', e => {
e.preventDefault();
dropZone.classList.add('drag-over');
});
dropZone.addEventListener('dragleave', () => dropZone.classList.remove('drag-over'));
dropZone.addEventListener('drop', e => {
e.preventDefault();
dropZone.classList.remove('drag-over');
const file = e.dataTransfer.files[0];
if (file) handleFileSelect(file);
});
fileInput.addEventListener('change', e => {
if (e.target.files[0]) handleFileSelect(e.target.files[0]);
});
// ── Convert ────────────────────────────────────────────────────────
async function convertFont() {
const hfUrl = document.getElementById('hf-url').value.trim();
if (!hfUrl) {
showToast('⚠️ Please enter your HuggingFace Space URL first');
return;
}
if (!currentFile) {
showToast('⚠️ Please upload a font file first');
return;
}
const selectedFormats = Array.from(
document.querySelectorAll('.format-toggle input:checked:not(:disabled)')
).map(cb => cb.value);
if (selectedFormats.length === 0) {
showToast('⚠️ Select at least one target format');
return;
}
// UI: loading state
const btn = document.getElementById('convert-btn');
btn.classList.add('loading');
btn.disabled = true;
const progressWrap = document.getElementById('progress-wrap');
const progressBar = document.getElementById('progress-bar');
progressWrap.classList.add('visible');
// Animate progress
let progress = 0;
const progressInterval = setInterval(() => {
progress = Math.min(progress + Math.random() * 8, 85);
progressBar.style.width = progress + '%';
}, 200);
try {
// Build form data for Gradio API
const formData = new FormData();
formData.append('files', currentFile);
// Upload file first
const uploadUrl = hfUrl.replace(/\/$/, '') + '/upload';
const uploadResp = await fetch(uploadUrl, {
method: 'POST',
body: formData
});
if (!uploadResp.ok) throw new Error(`Upload failed: ${uploadResp.status}`);
const uploadedPaths = await uploadResp.json();
const filePath = uploadedPaths[0];
// Call the convert_font function
const apiUrl = hfUrl.replace(/\/$/, '') + '/api/predict';
const apiResp = await fetch(apiUrl, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
fn_index: 0,
data: [{ name: currentFile.name, data: null, is_file: true, orig_name: currentFile.name, tmp_path: filePath }, selectedFormats]
})
});
if (!apiResp.ok) throw new Error(`API call failed: ${apiResp.status}`);
const result = await apiResp.json();
clearInterval(progressInterval);
progressBar.style.width = '100%';
setTimeout(() => progressWrap.classList.remove('visible'), 600);
if (result.error) throw new Error(result.error);
const [zipPath, statusText] = result.data;
// Show results
showSuccess(zipPath, statusText, hfUrl, selectedFormats);
} catch (err) {
clearInterval(progressInterval);
progressWrap.classList.remove('visible');
showError(err.message);
} finally {
btn.classList.remove('loading');
btn.disabled = false;
}
}
function showSuccess(zipPath, statusText, hfUrl, formats) {
const card = document.getElementById('result-card');
const header = document.getElementById('result-header');
const resultFiles = document.getElementById('result-files');
const downloadAllBtn = document.getElementById('download-all-btn');
const errorMsg = document.getElementById('error-msg');
card.classList.add('visible');
header.className = 'result-header';
document.getElementById('result-icon').textContent = 'βœ…';
document.getElementById('result-msg').textContent = `Converted to ${formats.length} format(s) successfully!`;
errorMsg.style.display = 'none';
resultFiles.innerHTML = '';
// Build download URL for the ZIP
const baseUrl = hfUrl.replace(/\/$/, '');
const zipUrl = zipPath ? `${baseUrl}/file=${zipPath}` : null;
formats.forEach(fmt => {
const row = document.createElement('div');
row.className = 'result-file';
const baseName = currentFile.name.replace(/\.[^/.]+$/, '');
row.innerHTML = `
<div class="result-file-name">
πŸ”€ ${baseName}<span>.${fmt.toLowerCase()}</span>
</div>
${zipUrl ? `<a class="download-btn" href="${zipUrl}" download="${baseName}_converted.zip">⬇ ZIP</a>` : '<span style="font-family:var(--font-mono);font-size:11px;color:var(--green)">βœ“ Done</span>'}
`;
resultFiles.appendChild(row);
});
if (zipUrl) {
downloadAllBtn.href = zipUrl;
downloadAllBtn.download = `${currentFile.name.replace(/\.[^/.]+$/, '')}_converted.zip`;
downloadAllBtn.style.display = 'flex';
}
showToast('βœ… Conversion complete!');
}
function showError(msg) {
const card = document.getElementById('result-card');
const header = document.getElementById('result-header');
const errorMsg = document.getElementById('error-msg');
const downloadAllBtn = document.getElementById('download-all-btn');
card.classList.add('visible');
header.className = 'result-header error';
document.getElementById('result-icon').textContent = '❌';
document.getElementById('result-msg').textContent = 'Conversion failed';
document.getElementById('result-files').innerHTML = '';
downloadAllBtn.style.display = 'none';
errorMsg.style.display = 'block';
errorMsg.textContent = msg;
showToast('❌ Conversion failed β€” check the error message');
}
// ── Test Connection ────────────────────────────────────────────────
async function testConnection() {
const hfUrl = document.getElementById('hf-url').value.trim();
if (!hfUrl) {
showToast('⚠️ Enter your HuggingFace Space URL first');
return;
}
showToast('πŸ”Œ Testing connection...');
try {
const resp = await fetch(hfUrl, { method: 'HEAD', mode: 'no-cors' });
showToast('βœ… Space appears to be online!');
} catch {
showToast('⚠️ Could not verify β€” try loading the URL directly in your browser');
}
}
// ── Toast ──────────────────────────────────────────────────────────
let toastTimeout;
function showToast(msg) {
const toast = document.getElementById('toast');
toast.textContent = msg;
toast.classList.add('show');
clearTimeout(toastTimeout);
toastTimeout = setTimeout(() => toast.classList.remove('show'), 3000);
}
// ── Copy ───────────────────────────────────────────────────────────
function copyText(el) {
navigator.clipboard.writeText(el.textContent).then(() => showToast('πŸ“‹ Copied!'));
}
</script>
</body>
</html>