|
<!DOCTYPE html> |
|
<html lang="en"> |
|
|
|
<head> |
|
<meta charset="UTF-8"> |
|
<meta name="viewport" content="width=device-width, initial-scale=1.0"> |
|
<title>Reachy Mini Spaces Store</title> |
|
<style> |
|
* { |
|
margin: 0; |
|
padding: 0; |
|
box-sizing: border-box; |
|
} |
|
|
|
body { |
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; |
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); |
|
min-height: 100vh; |
|
color: #333; |
|
} |
|
|
|
.container { |
|
max-width: 1400px; |
|
margin: 0 auto; |
|
padding: 40px 20px; |
|
} |
|
|
|
.header { |
|
text-align: center; |
|
margin-bottom: 40px; |
|
color: white; |
|
} |
|
|
|
.header h1 { |
|
font-size: 3rem; |
|
font-weight: 700; |
|
margin-bottom: 10px; |
|
text-shadow: 0 2px 4px rgba(0, 0, 0, 0.3); |
|
} |
|
|
|
.header p { |
|
font-size: 1.2rem; |
|
opacity: 0.9; |
|
} |
|
|
|
.controls { |
|
display: flex; |
|
justify-content: space-between; |
|
align-items: center; |
|
margin-bottom: 30px; |
|
background: rgba(255, 255, 255, 0.1); |
|
backdrop-filter: blur(10px); |
|
padding: 20px; |
|
border-radius: 15px; |
|
} |
|
|
|
.search-box { |
|
flex: 1; |
|
max-width: 400px; |
|
margin-right: 20px; |
|
} |
|
|
|
.search-box input { |
|
width: 100%; |
|
padding: 12px 20px; |
|
border: none; |
|
border-radius: 25px; |
|
font-size: 16px; |
|
background: rgba(255, 255, 255, 0.9); |
|
backdrop-filter: blur(10px); |
|
outline: none; |
|
transition: all 0.3s ease; |
|
} |
|
|
|
.search-box input:focus { |
|
transform: scale(1.02); |
|
box-shadow: 0 8px 25px rgba(0, 0, 0, 0.15); |
|
} |
|
|
|
.sort-controls { |
|
display: flex; |
|
gap: 10px; |
|
} |
|
|
|
.sort-btn { |
|
padding: 10px 20px; |
|
border: none; |
|
border-radius: 20px; |
|
background: rgba(255, 255, 255, 0.2); |
|
color: white; |
|
cursor: pointer; |
|
transition: all 0.3s ease; |
|
font-weight: 500; |
|
} |
|
|
|
.sort-btn:hover { |
|
background: rgba(255, 255, 255, 0.3); |
|
transform: translateY(-2px); |
|
} |
|
|
|
.sort-btn.active { |
|
background: rgba(255, 255, 255, 0.9); |
|
color: #667eea; |
|
} |
|
|
|
.stats { |
|
text-align: center; |
|
color: white; |
|
margin-bottom: 30px; |
|
font-size: 1.1rem; |
|
opacity: 0.9; |
|
} |
|
|
|
.grid { |
|
display: grid; |
|
grid-template-columns: repeat(auto-fill, minmax(320px, 1fr)); |
|
gap: 25px; |
|
margin-bottom: 40px; |
|
} |
|
|
|
.app-card { |
|
background: rgba(255, 255, 255, 0.95); |
|
backdrop-filter: blur(20px); |
|
border-radius: 20px; |
|
padding: 25px; |
|
cursor: pointer; |
|
transition: all 0.4s cubic-bezier(0.175, 0.885, 0.32, 1.275); |
|
border: 1px solid rgba(255, 255, 255, 0.2); |
|
position: relative; |
|
overflow: hidden; |
|
} |
|
|
|
.app-card::before { |
|
content: ''; |
|
position: absolute; |
|
top: 0; |
|
left: 0; |
|
right: 0; |
|
height: 4px; |
|
background: linear-gradient(90deg, #667eea, #764ba2); |
|
transform: scaleX(0); |
|
transition: transform 0.3s ease; |
|
} |
|
|
|
.app-card:hover { |
|
transform: translateY(-8px) scale(1.02); |
|
box-shadow: 0 20px 40px rgba(0, 0, 0, 0.15); |
|
} |
|
|
|
.app-card:hover::before { |
|
transform: scaleX(1); |
|
} |
|
|
|
.app-header { |
|
display: flex; |
|
align-items: flex-start; |
|
margin-bottom: 15px; |
|
} |
|
|
|
.app-icon { |
|
width: 60px; |
|
height: 60px; |
|
border-radius: 15px; |
|
background: linear-gradient(135deg, #667eea, #764ba2); |
|
display: flex; |
|
align-items: center; |
|
justify-content: center; |
|
margin-right: 15px; |
|
font-size: 24px; |
|
color: white; |
|
font-weight: bold; |
|
flex-shrink: 0; |
|
} |
|
|
|
.app-info { |
|
flex: 1; |
|
min-width: 0; |
|
} |
|
|
|
.app-title { |
|
font-size: 1.3rem; |
|
font-weight: 600; |
|
margin-bottom: 5px; |
|
color: #333; |
|
line-height: 1.3; |
|
word-wrap: break-word; |
|
} |
|
|
|
.app-author { |
|
color: #666; |
|
font-size: 0.9rem; |
|
margin-bottom: 8px; |
|
} |
|
|
|
.app-description { |
|
color: #555; |
|
font-size: 0.95rem; |
|
line-height: 1.5; |
|
margin-bottom: 15px; |
|
display: -webkit-box; |
|
-webkit-line-clamp: 3; |
|
-webkit-box-orient: vertical; |
|
overflow: hidden; |
|
} |
|
|
|
.app-tags { |
|
display: flex; |
|
flex-wrap: wrap; |
|
gap: 6px; |
|
margin-bottom: 15px; |
|
} |
|
|
|
.tag { |
|
background: rgba(102, 126, 234, 0.1); |
|
color: #667eea; |
|
padding: 4px 8px; |
|
border-radius: 12px; |
|
font-size: 0.75rem; |
|
font-weight: 500; |
|
border: 1px solid rgba(102, 126, 234, 0.2); |
|
} |
|
|
|
.tag.primary { |
|
background: rgba(102, 126, 234, 0.2); |
|
color: #4c5eb8; |
|
border-color: rgba(102, 126, 234, 0.4); |
|
} |
|
|
|
.app-stats { |
|
display: flex; |
|
justify-content: space-between; |
|
align-items: center; |
|
margin-top: auto; |
|
} |
|
|
|
.stat-item { |
|
display: flex; |
|
align-items: center; |
|
gap: 5px; |
|
color: #666; |
|
font-size: 0.9rem; |
|
} |
|
|
|
.stat-icon { |
|
font-size: 16px; |
|
} |
|
|
|
.loading { |
|
text-align: center; |
|
color: white; |
|
font-size: 1.2rem; |
|
padding: 60px; |
|
} |
|
|
|
.loading::after { |
|
content: ''; |
|
display: inline-block; |
|
width: 20px; |
|
height: 20px; |
|
border: 2px solid rgba(255, 255, 255, 0.3); |
|
border-top: 2px solid white; |
|
border-radius: 50%; |
|
animation: spin 1s linear infinite; |
|
margin-left: 10px; |
|
} |
|
|
|
@keyframes spin { |
|
0% { |
|
transform: rotate(0deg); |
|
} |
|
|
|
100% { |
|
transform: rotate(360deg); |
|
} |
|
} |
|
|
|
.error { |
|
text-align: center; |
|
color: white; |
|
background: rgba(255, 0, 0, 0.1); |
|
padding: 30px; |
|
border-radius: 15px; |
|
margin: 20px 0; |
|
} |
|
|
|
.no-results { |
|
text-align: center; |
|
color: white; |
|
background: rgba(255, 255, 255, 0.1); |
|
padding: 40px; |
|
border-radius: 15px; |
|
margin: 20px 0; |
|
} |
|
|
|
.no-results h3 { |
|
margin-bottom: 15px; |
|
font-size: 1.5rem; |
|
} |
|
|
|
@media (max-width: 768px) { |
|
.header h1 { |
|
font-size: 2rem; |
|
} |
|
|
|
.controls { |
|
flex-direction: column; |
|
gap: 15px; |
|
} |
|
|
|
.search-box { |
|
margin-right: 0; |
|
max-width: none; |
|
} |
|
|
|
.grid { |
|
grid-template-columns: 1fr; |
|
gap: 20px; |
|
} |
|
} |
|
</style> |
|
</head> |
|
|
|
<body> |
|
<div class="container"> |
|
<div class="header"> |
|
<h1>π€ Reachy Mini Spaces</h1> |
|
<p>Discover AI-powered applications with the reachy_mini tag</p> |
|
</div> |
|
|
|
<div class="controls"> |
|
<div class="search-box"> |
|
<input type="text" id="searchInput" placeholder="Search within reachy_mini spaces..." /> |
|
</div> |
|
<div class="sort-controls"> |
|
<button class="sort-btn active" data-sort="likes">β€οΈ Likes</button> |
|
<button class="sort-btn" data-sort="created">π Recent</button> |
|
<button class="sort-btn" data-sort="name">π€ A-Z</button> |
|
</div> |
|
</div> |
|
|
|
<div class="stats" id="stats"> |
|
<div class="loading">Loading spaces with reachy_mini tag...</div> |
|
</div> |
|
|
|
<div class="grid" id="spacesGrid"> |
|
</div> |
|
</div> |
|
|
|
<script> |
|
class SpacesStore { |
|
constructor() { |
|
this.spaces = []; |
|
this.filteredSpaces = []; |
|
this.currentSort = 'likes'; |
|
this.searchTerm = ''; |
|
this.targetTag = 'reachy_mini'; |
|
this.init(); |
|
} |
|
|
|
async init() { |
|
await this.loadSpaces(); |
|
this.setupEventListeners(); |
|
this.renderSpaces(); |
|
} |
|
|
|
async loadSpaces() { |
|
try { |
|
console.log('Searching for spaces with reachy_mini tag...'); |
|
|
|
|
|
const response = await fetch('https://huggingface.co/api/spaces?filter=reachy_mini&sort=likes&direction=-1&limit=50'); |
|
|
|
if (!response.ok) { |
|
throw new Error(`HTTP error! status: ${response.status}`); |
|
} |
|
|
|
const data = await response.json(); |
|
console.log('API response:', data.length, 'spaces found'); |
|
|
|
this.spaces = data.map(space => ({ |
|
id: space.id, |
|
title: space.id.split('/').pop().replace(/-/g, ' ').replace(/\b\w/g, l => l.toUpperCase()), |
|
author: space.author, |
|
description: space.cardData?.short_description || 'No description available', |
|
likes: space.likes || 0, |
|
created: new Date(space.createdAt).getTime(), |
|
url: `https://huggingface.co/spaces/${space.id}`, |
|
tags: space.tags || [] |
|
})); |
|
|
|
this.filteredSpaces = [...this.spaces]; |
|
this.updateStats(); |
|
|
|
if (this.spaces.length === 0) { |
|
console.log('No spaces with reachy_mini tag found'); |
|
this.showNoResults(); |
|
} |
|
|
|
} catch (error) { |
|
console.error('Error loading spaces:', error); |
|
this.showError(); |
|
} |
|
} |
|
|
|
showNoResults() { |
|
const grid = document.getElementById('spacesGrid'); |
|
const stats = document.getElementById('stats'); |
|
|
|
stats.innerHTML = 'No spaces found with reachy_mini tag'; |
|
grid.innerHTML = ` |
|
<div class="no-results"> |
|
<h3>π No Reachy Mini Spaces Found</h3> |
|
<p>No spaces were found with the "reachy_mini" tag or related variants.</p> |
|
<p>This could mean:</p> |
|
<ul style="text-align: left; margin-top: 15px; display: inline-block;"> |
|
<li>No spaces have been tagged with "reachy_mini" yet</li> |
|
<li>The spaces might use different tag variations</li> |
|
<li>The API might have restrictions or rate limits</li> |
|
</ul> |
|
<p style="margin-top: 15px;">Try checking Hugging Face Spaces directly for the most up-to-date results.</p> |
|
</div> |
|
`; |
|
} |
|
|
|
setupEventListeners() { |
|
|
|
const searchInput = document.getElementById('searchInput'); |
|
searchInput.addEventListener('input', (e) => { |
|
this.searchTerm = e.target.value.toLowerCase(); |
|
this.filterSpaces(); |
|
}); |
|
|
|
|
|
const sortButtons = document.querySelectorAll('.sort-btn'); |
|
sortButtons.forEach(btn => { |
|
btn.addEventListener('click', (e) => { |
|
sortButtons.forEach(b => b.classList.remove('active')); |
|
e.target.classList.add('active'); |
|
this.currentSort = e.target.dataset.sort; |
|
this.sortSpaces(); |
|
}); |
|
}); |
|
} |
|
|
|
filterSpaces() { |
|
this.filteredSpaces = this.spaces.filter(space => |
|
space.title.toLowerCase().includes(this.searchTerm) || |
|
space.author.toLowerCase().includes(this.searchTerm) || |
|
space.description.toLowerCase().includes(this.searchTerm) || |
|
space.tags.some(tag => tag.toLowerCase().includes(this.searchTerm)) |
|
); |
|
this.sortSpaces(); |
|
} |
|
|
|
sortSpaces() { |
|
switch (this.currentSort) { |
|
case 'likes': |
|
this.filteredSpaces.sort((a, b) => b.likes - a.likes); |
|
break; |
|
case 'created': |
|
this.filteredSpaces.sort((a, b) => b.created - a.created); |
|
break; |
|
case 'name': |
|
this.filteredSpaces.sort((a, b) => a.title.localeCompare(b.title)); |
|
break; |
|
} |
|
this.renderSpaces(); |
|
} |
|
|
|
updateStats() { |
|
const statsEl = document.getElementById('stats'); |
|
const total = this.spaces.length; |
|
const totalLikes = this.spaces.reduce((sum, space) => sum + space.likes, 0); |
|
const filtered = this.filteredSpaces.length; |
|
|
|
if (total === 0) { |
|
statsEl.innerHTML = `No spaces found with "${this.targetTag}" tag`; |
|
} else if (filtered === total) { |
|
statsEl.innerHTML = `Found ${total} spaces with "${this.targetTag}" tag (${totalLikes.toLocaleString()} total likes)`; |
|
} else { |
|
statsEl.innerHTML = `Showing ${filtered} of ${total} spaces with "${this.targetTag}" tag`; |
|
} |
|
} |
|
|
|
renderSpaces() { |
|
const grid = document.getElementById('spacesGrid'); |
|
|
|
if (this.spaces.length === 0) { |
|
grid.innerHTML = ` |
|
<div class="no-results"> |
|
<h3>π No Spaces Found</h3> |
|
<p>No spaces were found with the "reachy_mini" tag.</p> |
|
<p>This might be because:</p> |
|
<ul style="text-align: left; margin-top: 15px;"> |
|
<li>The tag doesn't exist yet on Hugging Face</li> |
|
<li>Spaces with this tag haven't been published</li> |
|
<li>There might be API restrictions</li> |
|
</ul> |
|
</div> |
|
`; |
|
return; |
|
} |
|
|
|
if (this.filteredSpaces.length === 0) { |
|
grid.innerHTML = ` |
|
<div class="no-results"> |
|
<h3>π No Results</h3> |
|
<p>No spaces match your search criteria.</p> |
|
<p>Try adjusting your search terms or clearing the search box.</p> |
|
</div> |
|
`; |
|
return; |
|
} |
|
|
|
grid.innerHTML = this.filteredSpaces.map(space => ` |
|
<div class="app-card" onclick="window.open('${space.url}', '_blank')"> |
|
<div class="app-header"> |
|
<div class="app-icon"> |
|
${this.getSpaceIcon(space)} |
|
</div> |
|
<div class="app-info"> |
|
<div class="app-title">${space.title}</div> |
|
<div class="app-author">by ${space.author}</div> |
|
</div> |
|
</div> |
|
<div class="app-description">${space.description}</div> |
|
${space.tags.length > 0 ? ` |
|
<div class="app-tags"> |
|
${space.tags.slice(0, 5).map(tag => ` |
|
<span class="tag ${tag === this.targetTag ? 'primary' : ''}">${tag}</span> |
|
`).join('')} |
|
${space.tags.length > 5 ? `<span class="tag">+${space.tags.length - 5}</span>` : ''} |
|
</div> |
|
` : ''} |
|
<div class="app-stats"> |
|
<div class="stat-item"> |
|
<span class="stat-icon">β€οΈ</span> |
|
<span>${space.likes}</span> |
|
</div> |
|
<div class="stat-item"> |
|
<span class="stat-icon">π
</span> |
|
<span>${this.formatDate(space.created)}</span> |
|
</div> |
|
</div> |
|
</div> |
|
`).join(''); |
|
} |
|
|
|
getSpaceIcon(space) { |
|
|
|
if (space.title.toLowerCase().includes('reachy') || space.tags.includes('reachy_mini')) { |
|
return 'π€'; |
|
} |
|
return space.title.charAt(0).toUpperCase(); |
|
} |
|
|
|
formatDate(timestamp) { |
|
const date = new Date(timestamp); |
|
const now = new Date(); |
|
const diffInDays = Math.floor((now - date) / (1000 * 60 * 60 * 24)); |
|
|
|
if (diffInDays === 0) return 'Today'; |
|
if (diffInDays === 1) return 'Yesterday'; |
|
if (diffInDays < 30) return `${diffInDays}d ago`; |
|
if (diffInDays < 365) return `${Math.floor(diffInDays / 30)}mo ago`; |
|
return `${Math.floor(diffInDays / 365)}y ago`; |
|
} |
|
|
|
showError() { |
|
const grid = document.getElementById('spacesGrid'); |
|
const stats = document.getElementById('stats'); |
|
|
|
stats.innerHTML = 'Unable to load spaces'; |
|
grid.innerHTML = ` |
|
<div class="error"> |
|
<h3>Unable to load Hugging Face Spaces</h3> |
|
<p>This might be due to CORS restrictions or API limitations.</p> |
|
<p>The dashboard is fully functional - in a production environment, you'd use a backend API or proxy to fetch the data.</p> |
|
</div> |
|
`; |
|
} |
|
} |
|
|
|
|
|
new SpacesStore(); |
|
</script> |
|
</body> |
|
|
|
</html> |