Skip to content

Commit 035f99d

Browse files
feat: add explore page (#41)
1 parent bdb4d6e commit 035f99d

File tree

4 files changed

+466
-0
lines changed

4 files changed

+466
-0
lines changed
Lines changed: 180 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,180 @@
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 />
Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
<script lang="ts">
2+
export let leaderboard:any = [];
3+
4+
$: sorted = [...leaderboard].sort((a, b) => b.hours - a.hours);
5+
6+
7+
// Local state (no need for parent)
8+
let open = false;
9+
10+
function toggle() {
11+
open = !open;
12+
}
13+
</script>
14+
15+
<style>
16+
.leaderboard {
17+
transition:
18+
width 0.6s cubic-bezier(0.25, 0.8, 0.3, 1),
19+
height 0.6s cubic-bezier(0.25, 0.8, 0.3, 1),
20+
top 0.6s cubic-bezier(0.25, 0.8, 0.3, 1),
21+
right 0.6s cubic-bezier(0.25, 0.8, 0.3, 1),
22+
border-radius 0.6s cubic-bezier(0.25, 0.8, 0.3, 1);
23+
font-family: 'PT Serif', serif;
24+
}
25+
26+
.leaderboard.small {
27+
width: 18vw;
28+
height: 22vh;
29+
cursor: pointer;
30+
}
31+
.leaderboard.full {
32+
width: 100vw;
33+
height: 100vh;
34+
top: 0;
35+
right: 0;
36+
border-radius: 0;
37+
}
38+
39+
.entry {
40+
display: flex;
41+
justify-content: space-between;
42+
padding: 1.2vh 2vw;
43+
border-bottom: 1px solid rgba(0,0,0,0.15);
44+
font-weight: 700;
45+
color: #2a2746;
46+
}
47+
48+
.entry:nth-child(1) { background: #f24b4b; color: #fee1c0; }
49+
.entry:nth-child(2) { background: #f56d6d; }
50+
.entry:nth-child(3) { background: #f88e8e; }
51+
52+
.lb-header {
53+
font-weight: 900;
54+
text-align: center;
55+
padding: 1.5vh 0;
56+
font-size: 3vh;
57+
background: #2a2746;
58+
color: #fee1c0;
59+
border-bottom: 3px solid black;
60+
}
61+
</style>
62+
63+
<div
64+
class="leaderboard fixed rounded-[2vh] z-10 top-[2vh] right-[2vw] overflow-hidden border-2 border-black/0 {open ? 'full' : 'small'}"
65+
on:click={() => {
66+
if (!open) toggle();
67+
}}
68+
>
69+
{#if open}
70+
<!-- FULL LEADERBOARD -->
71+
<div class="w-[98vw] ml-[1vw] mt-[2vh] rounded-[2vh] overflow-hidden">
72+
73+
<div class="lb-header flex justify-between items-center px-[4vw] z-50">
74+
<p class="pt-[5vh] text-center mx-auto text-[5vh] font-extrabold">Leaderboard</p>
75+
76+
<button
77+
on:click|stopPropagation={toggle}
78+
class="bg-[#fee1c0] text-[#2a2746] border-2 mr-[2vh] border-black rounded-full px-[1vw] py-[0.5vh] text-[2vh] font-bold hover:bg-[#2a2746] hover:text-[#fee1c0] transition-all"
79+
>
80+
Close
81+
</button>
82+
</div>
83+
84+
<div class="flex flex-col w-full h-[70vh] overflow-y-auto bg-[#fee1c0]">
85+
{#each sorted as { name, hours, approvedHours, coins }, i}
86+
<div class="entry text-[2.2vh] flex flex-col">
87+
<div class="flex justify-between text-[1.8vh] mt-[0.5vh] opacity-90 font-semibold">
88+
<span class="font-bold min-w-[10vw]">{i + 1}. {name}</span>
89+
<span>Hours: {hours}</span>
90+
<span>Approved: {approvedHours}</span>
91+
<span>Coins: {coins}</span>
92+
</div>
93+
</div>
94+
{/each}
95+
96+
</div>
97+
</div>
98+
99+
{:else}
100+
<!-- MINI PREVIEW -->
101+
<div class="flex flex-col w-full h-full bg-[#fee1c0] text-[#2a2746] p-[1vh]">
102+
<h3 class="text-[2.2vh] font-extrabold text-center mb-[1vh]">Top 3</h3>
103+
104+
{#each leaderboard.slice(0, 3) as item, i}
105+
<div class="flex justify-between text-[1.8vh] font-bold border-b border-black/20 py-[0.5vh]">
106+
<span>{i + 1}. {item.name}</span>
107+
<span>{item.score}</span>
108+
</div>
109+
{/each}
110+
</div>
111+
{/if}
112+
</div>
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
<script lang="ts">
2+
export let project: {
3+
name: string;
4+
desc: string;
5+
img: string;
6+
};
7+
8+
export let onOpen: (project: any) => void;
9+
export let onHover: () => void;
10+
</script>
11+
12+
<div
13+
on:click={() => onOpen(project)}
14+
on:mouseenter={onHover}
15+
class="group cursor-pointer rounded-[2vh] gap-[1vh] flex flex-col items-center text-center transition-all no-select"
16+
>
17+
<div
18+
class="bg-[#5E5087] p-[2vh] border border-black rounded-[2vh] shadow-lg transition-all duration-200 ease-out group-hover:rotate-[2deg] group-hover:shadow-2xl group-hover:scale-[1.1]"
19+
>
20+
<img
21+
src={project.img}
22+
alt={project.name}
23+
class="rounded-[1.5vh] mb-[1vh] w-full h-[25vh] object-cover border-2 border-black transition-all duration-200 ease-out no-select"
24+
draggable="false"
25+
/>
26+
<div class="flex items-center justify-center no-select">
27+
<p>4 hours</p>
28+
<div class="w-[0.5vh] h-[0.5vh] bg-[#fee1c0] rounded-full mx-[1vw]"></div>
29+
<p>2 weeks ago</p>
30+
</div>
31+
</div>
32+
33+
<div
34+
class="bg-[#5E5087] px-[0] py-[1vh] w-full border border-black rounded-[2vh] shadow-lg transition-all duration-200 ease-out group-hover:-rotate-[3deg] group-hover:shadow-2xl group-hover:scale-[1.05]"
35+
>
36+
<h3 class="text-[3vh] font-bold font-['PT_Serif',_serif] border-b border-black pb-[0.5vh]">
37+
{project.name}
38+
</h3>
39+
<p class="text-[2vh] opacity-80 pt-[0.5vh] px-[1vw]">{project.desc.slice(0,65)}...</p>
40+
</div>
41+
</div>

0 commit comments

Comments
 (0)