| | <!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.id.split('/')[0], |
| | 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> |