|
| 1 | +<script lang="ts"> |
| 2 | + import BottomNavigation from "$lib/BottomNavigation.svelte"; |
| 3 | + import { onMount } from "svelte"; |
| 4 | + import ProjectCard from "./ProjectCard.svelte"; |
| 5 | + import ProjectPanel from "./ProjectPanel.svelte"; |
| 6 | + import Leaderboard from "./Leaderboard.svelte"; |
| 7 | +
|
| 8 | + /* Sounds */ |
| 9 | + const soundFiles = Array.from({ length: 7 }, (_, i) => `/sounds/sound${i + 1}.mp3`); |
| 10 | +
|
| 11 | + function playRandomSound() { |
| 12 | + const randomSound = new Audio(soundFiles[Math.floor(Math.random() * soundFiles.length)]); |
| 13 | + randomSound.volume = 0.4; |
| 14 | + randomSound.play().catch(() => {}); |
| 15 | + } |
| 16 | +
|
| 17 | + /* Panel & Drag */ |
| 18 | + let open = false; |
| 19 | + let isRandomMode = true; |
| 20 | + let pointerId: number | null = null; |
| 21 | + let dragging = false; |
| 22 | + let panelHeight = 0; |
| 23 | + let currentTranslate = 0; |
| 24 | + let startOffset = 0; |
| 25 | + let ready = false; |
| 26 | +
|
| 27 | + const allProjects = Array.from({ length: 320 }, (_, i) => ({ |
| 28 | + name: `Project ${i + 1}`, |
| 29 | + desc: `This is a description for project number description for project number ${i + 1}.`, |
| 30 | + img: `https://placehold.co/600x400?text=Project+${i + 1}` |
| 31 | + })); |
| 32 | +
|
| 33 | + let visibleCount = 32; |
| 34 | + let visibleProjects = allProjects.slice(0, visibleCount); |
| 35 | + let sentinel: HTMLElement; |
| 36 | +
|
| 37 | + function loadMore() { |
| 38 | + if (visibleCount < allProjects.length) { |
| 39 | + visibleCount += 32; |
| 40 | + visibleProjects = allProjects.slice(0, visibleCount); |
| 41 | + } |
| 42 | + } |
| 43 | +
|
| 44 | + function setupObserver() { |
| 45 | + const observer = new IntersectionObserver( |
| 46 | + (entries) => { |
| 47 | + for (const entry of entries) { |
| 48 | + if (entry.isIntersecting) loadMore(); |
| 49 | + } |
| 50 | + }, |
| 51 | + { rootMargin: "200px" } |
| 52 | + ); |
| 53 | + if (sentinel) observer.observe(sentinel); |
| 54 | + } |
| 55 | +
|
| 56 | + onMount(() => { |
| 57 | + panelHeight = window.innerHeight * 0.7; |
| 58 | + currentTranslate = -panelHeight; |
| 59 | + setupObserver(); |
| 60 | + ready = true; |
| 61 | + }); |
| 62 | +
|
| 63 | + /* Project Select */ |
| 64 | + let selectedProject = allProjects[Math.floor(Math.random() * allProjects.length)]; |
| 65 | +
|
| 66 | + function generateRandomProject() { |
| 67 | + selectedProject = allProjects[Math.floor(Math.random() * allProjects.length)]; |
| 68 | + isRandomMode = true; |
| 69 | + } |
| 70 | +
|
| 71 | + function openProjectPanel(project: any) { |
| 72 | + selectedProject = project; |
| 73 | + isRandomMode = false; |
| 74 | + open = true; |
| 75 | + currentTranslate = 0; |
| 76 | + } |
| 77 | +
|
| 78 | + /* Draging */ |
| 79 | + function handlePointerDown(e: PointerEvent) { |
| 80 | + if (e.pointerType === "mouse" && e.button !== 0) return; |
| 81 | + const target = e.currentTarget as HTMLElement; |
| 82 | + target.setPointerCapture(e.pointerId); |
| 83 | + pointerId = e.pointerId; |
| 84 | + startOffset = e.clientY - currentTranslate; |
| 85 | + dragging = true; |
| 86 | + } |
| 87 | +
|
| 88 | + function handlePointerMove(e: PointerEvent) { |
| 89 | + if (!dragging || pointerId !== e.pointerId) return; |
| 90 | + currentTranslate = e.clientY - startOffset; |
| 91 | + currentTranslate = Math.min(0, Math.max(currentTranslate, -panelHeight)); |
| 92 | + } |
| 93 | +
|
| 94 | + function handlePointerUp(e: PointerEvent) { |
| 95 | + if (pointerId !== e.pointerId) return; |
| 96 | + const target = e.currentTarget as HTMLElement; |
| 97 | + target.releasePointerCapture(e.pointerId); |
| 98 | + pointerId = null; |
| 99 | + dragging = false; |
| 100 | +
|
| 101 | + const midpoint = -panelHeight / 2; |
| 102 | + open = currentTranslate > midpoint; |
| 103 | + currentTranslate = open ? 0 : -panelHeight; |
| 104 | +
|
| 105 | + if (open && isRandomMode) generateRandomProject(); |
| 106 | + } |
| 107 | +
|
| 108 | + /* Leaderboard */ |
| 109 | + const leaderboard:any = [ |
| 110 | + { name: "Alice", hours: 120, approvedHours: 110, coins: 450 }, |
| 111 | + { name: "Bob", hours: 98, approvedHours: 90, coins: 380 }, |
| 112 | + { name: "Charlie", hours: 105, approvedHours: 103, coins: 410 }, |
| 113 | + { name: "Diana", hours: 75, approvedHours: 70, coins: 260 }, |
| 114 | + { name: "Ethan", hours: 89, approvedHours: 82, coins: 310 }, |
| 115 | + { name: "Farah", hours: 112, approvedHours: 109, coins: 420 }, |
| 116 | + { name: "George", hours: 66, approvedHours: 64, coins: 200 }, |
| 117 | + { name: "Hana", hours: 134, approvedHours: 130, coins: 500 }, |
| 118 | + { name: "Ivan", hours: 92, approvedHours: 90, coins: 330 }, |
| 119 | + { name: "Julia", hours: 101, approvedHours: 99, coins: 390 } |
| 120 | + ]; |
| 121 | +
|
| 122 | +</script> |
| 123 | + |
| 124 | +<Leaderboard {leaderboard} /> |
| 125 | + |
| 126 | +<ProjectPanel |
| 127 | + {open} |
| 128 | + {selectedProject} |
| 129 | + {isRandomMode} |
| 130 | + {panelHeight} |
| 131 | + {currentTranslate} |
| 132 | + {dragging} |
| 133 | + {ready} |
| 134 | + onClose={() => { |
| 135 | + open = false; |
| 136 | + currentTranslate = -panelHeight; |
| 137 | + }} |
| 138 | + onGenerateRandom={generateRandomProject} |
| 139 | + onPointerDown={handlePointerDown} |
| 140 | + onPointerMove={handlePointerMove} |
| 141 | + onPointerUp={handlePointerUp} |
| 142 | +/> |
| 143 | + |
| 144 | +<div |
| 145 | + class={`w-full fixed left-0 top-0 h-screen bg-neutral-800/20 backdrop-blur-[2px] z-[77] transition-all duration-600 ease-in-out ${ |
| 146 | + open ? "opacity-100" : "opacity-0 pointer-events-none" |
| 147 | + }`} |
| 148 | + on:click={() => { |
| 149 | + open = false; |
| 150 | + currentTranslate = -panelHeight; |
| 151 | + }} |
| 152 | +></div> |
| 153 | + |
| 154 | +<main class="relative z-[1] pt-[8vh] pb-[10vh] flex flex-col items-center text-center text-[#fee1c0] bg-[#443B61] min-h-screen font-['PT_Sans',_sans-serif]"> |
| 155 | + <div class="relative"> |
| 156 | + <h1 class="text-[15vh] leading-[12vh] font-extrabold mb-[3vh] font-['Moga',_sans-serif] text-black absolute top-[0.75vh] left-[-0.5vw] z-[-1]"> |
| 157 | + Midnight Gallery |
| 158 | + </h1> |
| 159 | + <h1 class="text-[15vh] leading-[12vh] font-extrabold mb-[3vh] font-['Moga',_sans-serif] text-[#fee1c0]"> |
| 160 | + Midnight Gallery |
| 161 | + </h1> |
| 162 | + </div> |
| 163 | + |
| 164 | + <p class="max-w-[40vw] text-[2.5vh] mb-[5vh] opacity-90 font-['PT_Serif',_sans-serif] font-extrabold"> |
| 165 | + Check out featured projects below - or click the red button to generate a new random one! |
| 166 | + </p> |
| 167 | + |
| 168 | + <div class="grid grid-cols-4 w-full px-[3vw] gap-x-[3vw] gap-y-[6vh] pb-[10vh]"> |
| 169 | + {#each visibleProjects as project (project.name)} |
| 170 | + <ProjectCard |
| 171 | + {project} |
| 172 | + onOpen={openProjectPanel} |
| 173 | + onHover={playRandomSound} |
| 174 | + /> |
| 175 | + {/each} |
| 176 | + <div bind:this={sentinel}></div> |
| 177 | + </div> |
| 178 | +</main> |
| 179 | + |
| 180 | +<BottomNavigation /> |
0 commit comments