Spaces:
Build error
Build error
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>Attractiveness Rating App</title> | |
| <!-- Google Fonts --> | |
| <link rel="preconnect" href="https://fonts.googleapis.com"> | |
| <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin> | |
| <link href="https://fonts.googleapis.com/css2?family=Poppins:wght@300;400;600;700&display=swap" rel="stylesheet"> | |
| <!-- Font Awesome for Icons --> | |
| <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css"> | |
| <style> | |
| :root { | |
| --primary-color: #ff4b6e; | |
| --secondary-color: #ff8fa3; | |
| --bg-gradient: linear-gradient(135deg, #fdfbfb 0%, #ebedee 100%); | |
| --card-shadow: 0 10px 20px rgba(0,0,0,0.15); | |
| --text-color: #333; | |
| --success-color: #2ecc71; | |
| --danger-color: #e74c3c; | |
| } | |
| * { | |
| box-sizing: border-box; | |
| margin: 0; | |
| padding: 0; | |
| } | |
| body { | |
| font-family: 'Poppins', sans-serif; | |
| background: var(--bg-gradient); | |
| min-height: 100vh; | |
| display: flex; | |
| flex-direction: column; | |
| align-items: center; | |
| color: var(--text-color); | |
| overflow-x: hidden; | |
| } | |
| /* Header Section */ | |
| header { | |
| width: 100%; | |
| padding: 20px; | |
| text-align: center; | |
| background: white; | |
| box-shadow: 0 2px 10px rgba(0,0,0,0.05); | |
| position: sticky; | |
| top: 0; | |
| z-index: 100; | |
| } | |
| .header-content { | |
| max-width: 600px; | |
| margin: 0 auto; | |
| display: flex; | |
| justify-content: space-between; | |
| align-items: center; | |
| } | |
| h1 { | |
| font-size: 1.5rem; | |
| color: var(--primary-color); | |
| font-weight: 700; | |
| letter-spacing: 1px; | |
| } | |
| .attribution { | |
| font-size: 0.75rem; | |
| color: #888; | |
| text-decoration: none; | |
| } | |
| .attribution:hover { | |
| color: var(--primary-color); | |
| } | |
| /* Main Container */ | |
| main { | |
| flex: 1; | |
| width: 100%; | |
| max-width: 450px; | |
| padding: 20px; | |
| display: flex; | |
| flex-direction: column; | |
| align-items: center; | |
| } | |
| /* Card Stack Area */ | |
| .card-container { | |
| position: relative; | |
| width: 100%; | |
| height: 500px; | |
| perspective: 1000px; | |
| margin-bottom: 30px; | |
| } | |
| .card { | |
| position: absolute; | |
| width: 100%; | |
| height: 100%; | |
| background: white; | |
| border-radius: 20px; | |
| box-shadow: var(--card-shadow); | |
| overflow: hidden; | |
| transition: all 0.4s cubic-bezier(0.25, 0.8, 0.25, 1); | |
| transform-origin: center bottom; | |
| cursor: grab; | |
| display: flex; | |
| flex-direction: column; | |
| } | |
| .card-image { | |
| width: 100%; | |
| height: 65%; | |
| object-fit: cover; | |
| pointer-events: none; /* Prevents image drag interfering with card drag */ | |
| } | |
| .card-info { | |
| padding: 25px; | |
| background: white; | |
| text-align: center; | |
| } | |
| .card-info h2 { | |
| font-size: 2rem; | |
| margin-bottom: 5px; | |
| color: #2d3436; | |
| } | |
| .card-info p { | |
| color: #636e72; | |
| font-size: 1rem; | |
| line-height: 1.5; | |
| } | |
| .tags { | |
| display: flex; | |
| justify-content: center; | |
| gap: 10px; | |
| margin-top: 15px; | |
| flex-wrap: wrap; | |
| } | |
| .tag { | |
| background: #f1f2f6; | |
| padding: 5px 12px; | |
| border-radius: 50px; | |
| font-size: 0.8rem; | |
| color: #576574; | |
| font-weight: 600; | |
| } | |
| /* Rating Controls */ | |
| .controls { | |
| display: flex; | |
| gap: 20px; | |
| align-items: center; | |
| justify-content: center; | |
| width: 100%; | |
| } | |
| .btn { | |
| width: 70px; | |
| height: 70px; | |
| border-radius: 50%; | |
| border: none; | |
| cursor: pointer; | |
| font-size: 2rem; | |
| transition: transform 0.2s, box-shadow 0.2s; | |
| display: flex; | |
| align-items: center; | |
| justify-content: center; | |
| box-shadow: 0 5px 15px rgba(0,0,0,0.1); | |
| background: white; | |
| color: #555; | |
| } | |
| .btn:hover { | |
| transform: scale(1.1); | |
| box-shadow: 0 8px 20px rgba(0,0,0,0.15); | |
| } | |
| .btn-reject { | |
| color: var(--danger-color); | |
| } | |
| .btn-reject:hover { | |
| background-color: #fadbd8; | |
| } | |
| .btn-like { | |
| color: var(--success-color); | |
| } | |
| .btn-like:hover { | |
| background-color: #d4efdf; | |
| } | |
| .btn-pass { | |
| color: var(--primary-color); | |
| font-size: 1.2rem; | |
| } | |
| .btn-pass:hover { | |
| background-color: #fadbd8; | |
| } | |
| /* Progress Bar */ | |
| .progress-container { | |
| width: 100%; | |
| max-width: 450px; | |
| margin-bottom: 20px; | |
| text-align: center; | |
| } | |
| .progress-text { | |
| font-size: 0.9rem; | |
| margin-bottom: 5px; | |
| color: #636e72; | |
| display: flex; | |
| justify-content: space-between; | |
| } | |
| .progress-bar-bg { | |
| width: 100%; | |
| height: 8px; | |
| background: #dfe6e9; | |
| border-radius: 10px; | |
| overflow: hidden; | |
| } | |
| .progress-bar-fill { | |
| height: 100%; | |
| background: linear-gradient(90deg, var(--primary-color), var(--secondary-color)); | |
| width: 0%; | |
| transition: width 0.5s ease; | |
| } | |
| /* Results Modal */ | |
| .modal-overlay { | |
| position: fixed; | |
| top: 0; | |
| left: 0; | |
| width: 100%; | |
| height: 100%; | |
| background: rgba(0,0,0,0.8); | |
| display: flex; | |
| justify-content: center; | |
| align-items: center; | |
| z-index: 200; | |
| opacity: 0; | |
| pointer-events: none; | |
| transition: opacity 0.3s ease; | |
| backdrop-filter: blur(5px); | |
| } | |
| .modal-overlay.active { | |
| opacity: 1; | |
| pointer-events: all; | |
| } | |
| .modal-content { | |
| background: white; | |
| padding: 40px; | |
| border-radius: 25px; | |
| text-align: center; | |
| max-width: 90%; | |
| width: 400px; | |
| transform: translateY(50px); | |
| transition: transform 0.3s ease; | |
| box-shadow: 0 20px 40px rgba(0,0,0,0.3); | |
| } | |
| .modal-overlay.active .modal-content { | |
| transform: translateY(0); | |
| } | |
| .score-circle { | |
| width: 120px; | |
| height: 120px; | |
| border-radius: 50%; | |
| border: 8px solid var(--primary-color); | |
| display: flex; | |
| align-items: center; | |
| justify-content: center; | |
| margin: 0 auto 25px; | |
| font-size: 3rem; | |
| font-weight: 700; | |
| color: var(--primary-color); | |
| background: #fff; | |
| } | |
| .stats-grid { | |
| display: grid; | |
| grid-template-columns: 1fr 1fr; | |
| gap: 15px; | |
| margin-bottom: 30px; | |
| } | |
| .stat-box { | |
| background: #f8f9fa; | |
| padding: 15px; | |
| border-radius: 10px; | |
| } | |
| .stat-value { | |
| font-size: 1.5rem; | |
| font-weight: 700; | |
| color: #2d3436; | |
| } | |
| .stat-label { | |
| font-size: 0.85rem; | |
| color: #636e72; | |
| } | |
| .btn-primary { | |
| background: var(--primary-color); | |
| color: white; | |
| border: none; | |
| padding: 15px 40px; | |
| border-radius: 50px; | |
| font-size: 1rem; | |
| font-weight: 600; | |
| cursor: pointer; | |
| transition: background 0.3s; | |
| width: 100%; | |
| } | |
| .btn-primary:hover { | |
| background: #e03e5b; | |
| } | |
| /* Animations for swiping */ | |
| .animating-out-left { | |
| transform: translateX(-150%) rotate(-30deg) ; | |
| opacity: 0; | |
| } | |
| .animating-out-right { | |
| transform: translateX(150%) rotate(30deg) ; | |
| opacity: 0; | |
| } | |
| /* Responsive adjustments */ | |
| @media (max-height: 700px) { | |
| .card-container { height: 400px; } | |
| .card-image { height: 60%; } | |
| } | |
| </style> | |
| </head> | |
| <body> | |
| <header> | |
| <div class="header-content"> | |
| <h1><i class="fa-solid fa-heart"></i> RateAttract</h1> | |
| <a href="https://huggingface.co/spaces/akhaliq/anycoder" target="_blank" class="attribution">Built with anycoder</a> | |
| </div> | |
| </header> | |
| <main> | |
| <div class="progress-container"> | |
| <div class="progress-text"> | |
| <span id="progress-text">Profile 1 of 10</span> | |
| <span id="rating-count">0 Ratings</span> | |
| </div> | |
| <div class="progress-bar-bg"> | |
| <div class="progress-bar-fill" id="progress-bar"></div> | |
| </div> | |
| </div> | |
| <div class="card-container" id="card-stack"> | |
| <!-- Cards will be injected here via JS --> | |
| </div> | |
| <div class="controls"> | |
| <button class="btn btn-reject" id="btn-reject" title="No"> | |
| <i class="fa-solid fa-xmark"></i> | |
| </button> | |
| <button class="btn btn-pass" id="btn-skip" title="Skip"> | |
| <i class="fa-solid fa-rotate-right"></i> | |
| </button> | |
| <button class="btn btn-like" id="btn-like" title="Yes"> | |
| <i class="fa-solid fa-heart"></i> | |
| </button> | |
| </div> | |
| </main> | |
| <!-- Results Modal --> | |
| <div class="modal-overlay" id="results-modal"> | |
| <div class="modal-content"> | |
| <h2 style="color: var(--primary-color); margin-bottom: 10px;">Session Complete!</h2> | |
| <p>Here is your rating summary</p> | |
| <div class="score-circle" id="final-score">0</div> | |
| <p>Average Attractiveness Score</p> | |
| <div class="stats-grid"> | |
| <div class="stat-box"> | |
| <div class="stat-value" id="total-swipes">0</div> | |
| <div class="stat-label">Total Profiles</div> | |
| </div> | |
| <div class="stat-box"> | |
| <div class="stat-value" id="total-likes">0</div> | |
| <div class="stat-label">Likes (Hearts)</div> | |
| </div> | |
| <div class="stat-box"> | |
| <div class="stat-value" id="avg-rating">0.0</div> | |
| <div class="stat-label">Avg Rating</div> | |
| </div> | |
| <div class="stat-box"> | |
| <div class="stat-value" id="highest-rating">0</div> | |
| <div class="stat-label">Highest Score</div> | |
| </div> | |
| </div> | |
| <button class="btn-primary" onclick="resetApp()">Start New Session</button> | |
| </div> | |
| </div> | |
| <script> | |
| // --- Data Generation --- | |
| const generateProfiles = (count) => { | |
| const names = ["Sarah", "James", "Elena", "Michael", "Liam", "Olivia", "Noah", "Emma", "Oliver", "Ava"]; | |
| const interests = ["Traveling", "Photography", "Cooking", "Hiking", "Music", "Art", "Fitness", "Gaming", "Reading", "Dancing"]; | |
| const profiles = []; | |
| for (let i = 0; i < count; i++) { | |
| const name = names[i % names.length] + (i >= names.length ? " " + (i + 1) : ""); | |
| // Random interests | |
| const randomInterests = interests.sort(() => 0.5 - Math.random()).slice(0, 3); | |
| // Random image from Picsum (reliable placeholder service) | |
| const randomId = Math.floor(Math.random() * 1000); | |
| profiles.push({ | |
| id: i, | |
| name: name, | |
| age: Math.floor(Math.random() * (35 - 22 + 1)) + 22, | |
| bio: "Just living my life and looking for good vibes. I love exploring new places and trying different cuisines.", | |
| interests: randomInterests, | |
| image: `https://picsum.photos/id/${randomId}/400/500` | |
| }); | |
| } | |
| return profiles; | |
| }; | |
| // --- State Management --- | |
| const TOTAL_PROFILES = 10; | |
| let profiles = []; | |
| let currentIndex = 0; | |
| let ratings = []; | |
| let sessionStats = { | |
| total: 0, | |
| likes: 0, | |
| ratingsSum: 0, | |
| highestRating: 0 | |
| }; | |
| // --- DOM Elements --- | |
| const cardStack = document.getElementById('card-stack'); | |
| const progressBar = document.getElementById('progress-bar'); | |
| const progressText = document.getElementById('progress-text'); | |
| const ratingCountText = document.getElementById('rating-count'); | |
| const btnReject = document.getElementById('btn-reject'); | |
| const btnLike = document.getElementById('btn-like'); | |
| const btnSkip = document.getElementById('btn-skip'); | |
| const modal = document.getElementById('results-modal'); | |
| const finalScoreEl = document.getElementById('final-score'); | |
| const totalSwipesEl = document.getElementById('total-swipes'); | |
| const totalLikesEl = document.getElementById('total-likes'); | |
| const avgRatingEl = document.getElementById('avg-rating'); | |
| const highestRatingEl = document.getElementById('highest-rating'); | |
| // --- Initialization --- | |
| function initApp() { | |
| profiles = generateProfiles(TOTAL_PROFILES); | |
| currentIndex = 0; | |
| ratings = []; | |
| sessionStats = { total: 0, likes: 0, ratingsSum: 0, highestRating: 0 }; | |
| updateProgress(); | |
| renderCard(); | |
| modal.classList.remove('active'); | |
| } | |
| // --- Rendering --- | |
| function renderCard() { | |
| if (currentIndex >= profiles.length) { | |
| showResults(); | |
| return; | |
| } | |
| const profile = profiles[currentIndex]; | |
| // Create HTML structure | |
| const card = document.createElement('div'); | |
| card.className = 'card'; | |
| card.id = 'current-card'; | |
| // Generate Interest Tags HTML | |
| const tagsHtml = profile.interests.map(tag => `<span class="tag"><i class="fa-solid fa-tag"></i> ${tag}</span>`).join(''); | |
| card.innerHTML = ` | |
| <img src="${profile.image}" alt="${profile.name}" class="card-image" draggable="false"> | |
| <div class="card-info"> | |
| <h2>${profile.name}, ${profile.age}</h2> | |
| <p>${profile.bio}</p> | |
| <div class="tags"> | |
| ${tagsHtml} | |
| </div> | |
| </div> | |
| `; | |
| // Clear stack and append new card | |
| cardStack.innerHTML = ''; | |
| cardStack.appendChild(card); | |
| // Attach Drag Logic | |
| attachDragLogic(card); | |
| } | |
| function updateProgress() { | |
| const percentage = (currentIndex / TOTAL_PROFILES) * 100; | |
| progressBar.style.width = `${percentage}%`; | |
| progressText.innerText = `Profile ${currentIndex + 1} of ${TOTAL_PROFILES}`; | |
| ratingCountText.innerText = `${sessionStats.total} Profiles Rated`; | |
| } | |
| // --- Drag Logic (Touch & Mouse) --- | |
| function attachDragLogic(card) { | |
| let isDragging = false; | |
| let startX = 0; | |
| let currentX = 0; | |
| const onStart = (e) => { | |
| isDragging = true; | |
| startX = e.type.includes('mouse') ? e.clientX : e.touches[0].clientX; | |
| card.style.transition = 'none'; | |
| card.style.cursor = 'grabbing'; | |
| }; | |
| const onMove = (e) => { | |
| if (!isDragging) return; | |
| e.preventDefault(); // Prevent scrolling on mobile | |
| currentX = (e.type.includes('mouse') ? e.clientX : e.touches[0].clientX); | |
| const deltaX = currentX - startX; | |
| // Rotation effect based on X movement | |
| const rotate = deltaX * 0.1; | |
| card.style.transform = `translateX(${deltaX}px) rotate(${rotate}deg)`; | |
| // Visual feedback (opacity change) | |
| const opacity = Math.max(0, 1 - Math.abs(deltaX) / 500); | |
| card.style.opacity = opacity; | |
| // Color overlay effect | |
| if (deltaX > 0) { | |
| card.style.borderRight = `5px solid var(--success-color)`; | |
| card.style.borderLeft = 'none'; | |
| } else { | |
| card.style.borderLeft = `5px solid var(--danger-color)`; | |
| card.style.borderRight = 'none'; | |
| } | |
| }; | |
| const onEnd = () => { | |
| if (!isDragging) return; | |
| isDragging = false; | |
| card.style.transition = 'all 0.3s ease'; | |
| card.style.cursor = 'grab'; | |
| card.style.border = 'none'; | |
| const deltaX = currentX - startX; | |
| const threshold = 100; // pixels to trigger swipe | |
| if (deltaX > threshold) { | |
| handleSwipe('right'); | |
| } else if (deltaX < -threshold) { | |
| handleSwipe('left'); | |
| } else { | |
| // Reset position | |
| card.style.transform = 'translateX(0) rotate(0)'; | |
| card.style.opacity = '1'; | |
| } | |
| currentX = 0; | |
| }; | |
| // Mouse Events | |
| card.addEventListener('mousedown', onStart); | |
| document.addEventListener('mousemove', onMove); | |
| document.addEventListener('mouseup', onEnd); | |
| // Touch Events | |
| card.addEventListener('touchstart', onStart); | |
| card.addEventListener('touchmove', onMove); | |
| card.addEventListener('touchend', onEnd); | |
| } | |
| // --- Actions --- | |
| function handleSwipe(direction) { | |
| const card = document.getElementById('current-card'); | |
| if (direction === 'right') { | |
| card.classList.add('animating-out-right'); | |
| setTimeout(() => { | |
| showRatingPrompt(true); | |
| }, 300); | |
| } else { | |
| card.classList.add('animating-out-left'); | |
| setTimeout(() => { | |
| nextProfile(); | |
| }, 300); | |
| } | |
| } | |
| function showRatingPrompt(liked) { | |
| // In this app, if you "Like" (Right swipe), you must rate it. | |
| // If you Reject (Left swipe), you skip rating. | |
| let rating = 0; | |
| if (liked) { | |
| // Simple prompt for rating (1-10) | |
| let validRating = false; | |
| while (!validRating) { | |
| const input = prompt(`How would you rate ${profiles[currentIndex].name}? (1-10)\n(1 = Not Attractive, 10 = Extremely Attractive)`, "7"); | |
| if (input === null) { | |
| // User cancelled, treat as a pass | |
| nextProfile(); | |
| return; | |
| } | |
| const num = parseInt(input); | |
| if (!isNaN(num) && num >= 1 && num <= 10) { | |
| rating = num; | |
| validRating = true; | |
| } else { | |
| alert("Please enter a number between 1 and 10."); | |
| } | |
| } | |
| } | |
| // Record Data | |
| sessionStats.total++; | |
| sessionStats.ratingsSum += rating; | |
| if (rating > sessionStats.highestRating) { | |
| sessionStats.highestRating = rating; | |
| } | |
| if (liked) sessionStats.likes++; | |
| ratings.push({ name: profiles[currentIndex].name, score: rating }); | |
| updateProgress(); | |
| nextProfile(); | |
| } | |
| function nextProfile() { | |
| currentIndex++; | |
| renderCard(); | |
| } | |
| function showResults() { | |
| finalScoreEl.innerText = sessionStats.total; | |
| totalSwipesEl.innerText = sessionStats.total; | |
| totalLikesEl.innerText = sessionStats.likes; | |
| const avg = sessionStats.total > 0 | |
| ? (sessionStats.ratingsSum / sessionStats.total).toFixed(1) | |
| : 0; | |
| avgRatingEl.innerText = avg; | |
| highestRatingEl.innerText = sessionStats.highestRating; | |
| modal.classList.add('active'); | |
| } | |
| function resetApp() { | |
| initApp(); | |
| } | |
| // --- Button Listeners --- | |
| btnLike.addEventListener('click', () => { | |
| if (currentIndex < profiles.length) { | |
| handleSwipe('right'); | |
| } | |
| }); | |
| btnReject.addEventListener('click', () => { | |
| if (currentIndex < profiles.length) { | |
| handleSwipe('left'); | |
| } | |
| }); | |
| btnSkip.addEventListener('click', () => { | |
| if (currentIndex < profiles.length) { | |
| nextProfile(); | |
| } | |
| }); | |
| // Start the app | |
| initApp(); | |
| </script> | |
| </body> | |
| </html> |