Compare commits

...
Sign in to create a new pull request.

18 commits
pwa ... main

Author SHA1 Message Date
Simone Tesini
7443d600e7 Merge brt pushanch 'main' of https://repos.hackathon.bz.it/2025-summer/team-1 2025-08-02 13:45:32 +02:00
Simone Tesini
bf71a8103d rush 2025-08-02 13:45:27 +02:00
f7351aecf8 Add default room for testing && show error msg 2025-08-02 13:41:53 +02:00
436553809a Merge remote-tracking branch 'refs/remotes/origin/main' 2025-08-02 13:34:20 +02:00
fb49da2edd Ask for pin for private rooms 2025-08-02 13:34:10 +02:00
Simone Tesini
9a12f39f3e remove words 2025-08-02 13:22:52 +02:00
Simone Tesini
9ec601b263 Merge branch 'main' of https://repos.hackathon.bz.it/2025-summer/team-1 2025-08-02 13:20:00 +02:00
Simone Tesini
1763a6b96f add readme 2025-08-02 13:19:46 +02:00
360338ccb2 Add qrcode in admin page 2025-08-02 13:18:58 +02:00
278a2d94a8 Add custom code insertion during room creation 2025-08-02 13:10:51 +02:00
Mat12143
c3d30a1cc8 Merge branch 'main' of https://repos.hackathon.bz.it/2025-summer/team-1 2025-08-02 13:00:17 +02:00
Mat12143
1063c239b6 feat: block access for pin protected rooms 2025-08-02 13:00:11 +02:00
Simone Tesini
6a50dc4c86 Merge branch 'main' of https://repos.hackathon.bz.it/2025-summer/team-1 2025-08-02 12:52:02 +02:00
Simone Tesini
efcabd2ee4 add song cooldonw 2025-08-02 12:51:50 +02:00
746189862a Merge remote-tracking branch 'refs/remotes/origin/main' 2025-08-02 12:47:24 +02:00
01cf53c775 Try to fix thumb up / down 2025-08-02 12:47:16 +02:00
7587796934 Merge branch 'main' of https://repos.hackathon.bz.it/2025-summer/team-1 2025-08-02 12:38:29 +02:00
f4a4c46fbc Fix room creation href 2025-08-02 12:34:27 +02:00
14 changed files with 169 additions and 67 deletions

View file

@ -1,3 +1,40 @@
# team-1
# ChillBox
Test
> *A project by Pausetta.org, Simone Tesini, Francesco De Carlo, Leonardo Segala, Matteo Peretto*
**ChillBox** is a web app that lets you create a shared radio station with a democratic voting system, so everyone gets to enjoy their favorite music together.
Perfect for venues like swimming pools, cafés, or even lively parties.
---
## 🎵 Voting System
Joining a ChillBox room is easy: users can either scan the QR code displayed on the host screen or use GPS to find nearby rooms.
Hosts can set a location range, ensuring only people physically present can add or vote for songs.
---
## 📊 Ranking Algorithm
ChillBox uses a smart ranking algorithm to decide what plays next. The score of each song is based on:
* Votes from users
* How recently similar songs (same genre or artist) have been played (less = better)
* A bit of randomness to keep things interesting
* A strong penalty for songs played too recently
---
## 👐 Hands-Off Experience
ChillBox is designed to be almost entirely hands-free.
Once the host sets up a room and optionally connects a screen or projector
(to show the current track, QR code, etc.), ChillBox takes care of the rest.
ChillBox comes with built-in automatic moderation to keep the music fair and on-theme.
* Users cant vote for the same song multiple times.
* A cooldown prevents users from spamming song requests.
* Hosts can define preferred genres and overall mood, so no one can hijack your chill beach vibes with unexpected death metal.
That said, hosts still have access to essential controls, like pause and skip, if needed.

15
SPEECH.md Normal file
View file

@ -0,0 +1,15 @@
# speech
## Home screen
We start here in the home page.
We can see this little radar animation, which means that the app is looking for nearby ChillBox rooms to join.
It uses GPS for this feature.
## Join room
When we join a room, the server checks our location and checks if it's within a specified range.
That way, you must physically be in the location to actually be able to add new songs
## Talk about the host
As you can see here (and hear) on the left, the host is already playing some music.
Now i will add a song on the client side and it will pop up in the list.

View file

@ -29,9 +29,9 @@ init_db(state.db)
state.rooms[1000] = Room(
id=1000,
coord=Coordinates(46.6769043, 11.1851585),
name="Test Room",
pin=None,
tags=set(),
name="Lido Scena",
pin=1234,
tags=set(["chill", "raggaetton", "spanish", "latino", "mexican", "rock"]),
range_size=150,
songs={},
history=[],
@ -73,7 +73,7 @@ def on_leave(data):
@app.get("/api/join")
def join():
room_id = request.args.get("room")
code = request.args.get("code")
code = request.args.get("pin")
if room_id is None:
return error("Missing room id")
@ -81,8 +81,11 @@ def join():
if (room := state.rooms.get(int(room_id))) is None:
return error("Invalid room")
if room.pin is not None and room.pin != code:
return error("Invalid code")
if room.pin is not None:
if code is None:
return error("Missing code")
if int(room.pin) != int(code):
return error("Invalid code")
distance = distance_between_coords(
lhs=room.coord,

View file

@ -1,19 +1,45 @@
<script lang="ts">
import { type Room } from "$lib/types"
let { room }: { room: Room } = $props()
let showPinModal: boolean = $state(false)
let pin: number = $state()
</script>
<a
class="flex w-82 cursor-pointer flex-row items-center rounded-md border border-dark-pine-muted bg-light-pine-overlay p-3 hover:bg-dark-pine-base/20 dark:bg-dark-pine-overlay hover:dark:bg-light-pine-base/20"
href="/room/{room.id}"
<div
class="flex gap-2 w-82 cursor-pointer flex-col rounded-md border border-dark-pine-muted bg-light-pine-overlay p-3 hover:bg-dark-pine-base/20 dark:bg-dark-pine-overlay hover:dark:bg-light-pine-base/20"
>
<div class="flex flex-row">
{room.name}
{room.private ? "🔒" : ""}
</div>
<div class="grow"></div>
<div class="flex flex-row items-center gap-2">
<div class="font-mono">{Math.round(room.distance)}m</div>
<div class="rounded bg-light-pine-blue px-2 py-0.5 text-dark-pine-text dark:bg-dark-pine-blue">Join</div>
</div>
</a>
<button
class="flex flex-row items-center"
onclick={() => {
if (!room.private) {
window.location.href = "/room/" + room.id
return
}
showPinModal = !showPinModal
}}
>
<div class="flex flex-row">
{room.name}
{room.private ? "🔒" : ""}
</div>
<div class="grow"></div>
<div class="flex flex-row items-center gap-2">
<div class="font-mono">{Math.round(room.distance)}m</div>
<div class="rounded bg-light-pine-blue px-2 py-0.5 text-dark-pine-text dark:bg-dark-pine-blue">Join</div>
</div>
</button>
{#if showPinModal}
<input
placeholder="PIN (requied)"
class="p-2 text-xl rounded-md border-dark-pine-muted bg-light-pine-overlay dark:bg-dark-pine-base hover:dark:bg-light-pine-base/20 duration-100 outline-none focus:ring-2"
type="number"
bind:value={pin}
/>
<button
onclick={() => {
window.location.href = `/room/${room.id}?pin=${pin}`
}}
class="p-2 text-xl rounded-md border-dark-pine-muted bg-light-pine-overlay dark:bg-dark-pine-base hover:dark:bg-light-pine-base/20 duration-100 outline-none focus:ring-2">JOIN</button
>
{/if}
</div>

View file

@ -1,20 +1,39 @@
<script lang="ts">
import { LoaderCircle } from "@lucide/svelte"
const COOLDOWN_SECS = 10
let { roomId } = $props()
let input = $state("")
let disabled: boolean = $state(false)
let loading: boolean = $state(false)
let cooldowned: boolean = $state(false)
let errorMsg: string = $state()
$effect(() => {
console.log("cooldowned is now", cooldowned)
})
async function sendSong() {
disabled = true
await fetch(`/api/addsong?room=${roomId}&query=${input}`, { method: "POST" })
loading = true
const res = await fetch(`/api/addsong?room=${roomId}&query=${input}`, { method: "POST" })
const json = await res.json()
input = ""
disabled = false
loading = false
if (!json.success) {
errorMsg = json.error
}
cooldowned = true
setTimeout(() => {
cooldowned = false
console.log("unset cooldown")
}, COOLDOWN_SECS * 1000)
}
</script>
<div
class={`flex h-full w-full flex-row items-center gap-2 rounded-md border-dark-pine-muted bg-light-pine-overlay hover:bg-dark-pine-base/20 dark:bg-dark-pine-overlay hover:dark:bg-light-pine-base/20 ${disabled ? "disabled" : ""}`}
class={`flex h-full w-full flex-row items-center gap-2 rounded-md border-dark-pine-muted bg-light-pine-overlay hover:bg-dark-pine-base/20 dark:bg-dark-pine-overlay hover:dark:bg-light-pine-base/20 ${loading ? "disabled" : ""}`}
>
<input
type="text"
@ -22,20 +41,29 @@
class="h-[50px] w-3/4 rounded px-4 font-bold text-white outline-none"
bind:value={input}
onkeydown={(e) => {
errorMsg = null
if (e.key == "Enter") {
sendSong()
}
}}
{disabled}
disabled={loading}
/>
{#if disabled}
{#if loading}
<span class="animate-spin">
<LoaderCircle />
</span>
{/if}
<button class="i-lucide-check h-[40px] w-1/4 cursor-pointer rounded border border-0 font-semibold shadow-xl duration-100 hover:scale-105 active:scale-90 dark:bg-dark-pine-blue" onclick={sendSong}
>Add</button
<button
disabled={cooldowned}
class="i-lucide-check h-[40px] w-1/4 rounded font-semibold shadow-xl duration-100 active:scale-90 {!cooldowned
? 'cursor-pointer bg-light-pine-blue hover:scale-105 dark:bg-dark-pine-blue '
: 'bg-light-pine-muted dark:bg-light-pine-muted'}"
onclick={sendSong}>Add</button
>
<span class="i-lucide-chevrons-left"></span>
</div>
<p class="text-red-500 font-semibold">
{errorMsg}
</p>

View file

@ -7,9 +7,8 @@
let picked_suggestions: string[] = $state([])
async function vote(idx: number, amount: number, songId: string) {
async function vote(amount: number, songId: string) {
if (picked_suggestions.includes(songId)) return console.log("rejecting vote")
suggestions[idx].upvote += amount
await fetch(`/api/song/voting?room=${roomId}&song=${songId}&increment=${amount}`, { method: "POST" })
picked_suggestions.push(songId)
console.log("accepted vote")
@ -26,7 +25,7 @@
<p>No suggestions yet! Try to add a new one using the Add button</p>
{/if}
{#each suggestions as sug, idx}
{#each suggestions as sug}
<div
class="flex h-[80px] w-full flex-row gap-2 rounded-md border-dark-pine-muted bg-light-pine-overlay p-2 shadow-md duration-100 hover:bg-dark-pine-base/20 dark:bg-dark-pine-overlay hover:dark:bg-light-pine-base/20"
>
@ -48,7 +47,7 @@
class={!picked_suggestions.includes(sug.uuid) ? "text-light-pine-green duration-100 hover:scale-150 dark:text-dark-pine-green" : "text-light-pine-muted dark:text-dark-pine-muted"}
disabled={!!picked_suggestions.includes(sug.uuid)}
onclick={async () => {
await vote(idx, 1, sug.uuid)
await vote(1, sug.uuid)
}}><ThumbsUp /></button
>
<p class="font-semibold text-light-pine-text dark:text-dark-pine-text">{sug.upvote}</p>
@ -56,7 +55,7 @@
class={!picked_suggestions.includes(sug.uuid) ? "text-light-pine-red duration-100 hover:scale-150 dark:text-dark-pine-red" : "text-light-pine-muted dark:text-dark-pine-muted"}
disabled={!!picked_suggestions.includes(sug.uuid)}
onclick={async () => {
await vote(idx, -1, sug.uuid)
await vote(-1, sug.uuid)
}}><ThumbsDown /></button
>
</div>

View file

@ -1,8 +1,8 @@
import { get_coords, type Coordinates } from "./gps"
import { type Coordinates } from "./gps"
import { parseSong, parseSuggestion, type FetchError, type Song, type Suggestion } from "./types"
export const joinRoom = async function (roomId: string, coords: Coordinates): Promise<[FetchError | null, string]> {
let res = await fetch(`/api/join?room=${roomId}&lat=${coords.latitude}&lon=${coords.longitude}`)
export const joinRoom = async function (roomId: string, coords: Coordinates, pin: string): Promise<[FetchError | null, string]> {
let res = await fetch(`/api/join?room=${roomId}&lat=${coords.latitude}&lon=${coords.longitude}&pin=${pin}`)
if (res.status != 200) {
return [{ code: 400, message: "Cannot join the room" }, ""]

View file

@ -23,12 +23,12 @@
<span class="lilita-one-regular text-6xl font-bold">ChillBox</span>
</div>
<img src="/smerdo_radar_bonus.gif" alt="radar" class="h-64 w-64" />
<img src="/radar_bonus.gif" alt="radar" class="h-64 w-64" />
<span class="animate-pulse text-sm italic">Scanning for rooms near you...</span>
<button
onclick={() => {
window.location.href = "/room/new"
window.location.href = "/room/create"
}}
class="fixed right-4 bottom-4 flex flex-row gap-1 rounded-xl bg-light-pine-blue p-2 text-dark-pine-text sm:right-20 md:right-40 lg:right-80 dark:bg-dark-pine-blue"
>

View file

@ -112,5 +112,7 @@
<button class="rounded-md border border-dark-pine-muted p-2 hover:scale-105 active:scale-90 dark:bg-dark-pine-blue" onclick={playNext}>Next</button>
</div>
</div>
<img class="absolute right-1 bottom-1" src="/api/room/qrcode?room=1000&pin=1234" />
<!-- @PERETTO fix here pls -->
</div>
{/if}

View file

@ -31,7 +31,7 @@
}
let sugg, queue, index
;[returnError] = await joinRoom(data.roomId, coords)
;[returnError] = await joinRoom(data.roomId, coords, data.pin)
if (returnError) {
return
}

View file

@ -3,5 +3,6 @@ import type { PageLoad } from "./$types"
export const load: PageLoad = ({ params, url }) => {
return {
roomId: params.id || "",
pin: url.searchParams.get("pin") || "",
}
}

View file

@ -9,9 +9,7 @@
let name: string = $state()
let range: number = $state()
const privateStyle = "bg-red-500"
const publicStyle = "bg-green-500"
let pin: number = $state()
async function createRoom() {
if (creating) {
@ -22,16 +20,12 @@
return
}
let pin
if (privateRoom) {
pin = Math.floor(Math.random() * 10000)
} else {
pin = ""
}
creating = true
const res = await fetch(`/api/room/new?name=${encodeURIComponent(name)}&coords=${coord.latitude},${coord.longitude}&range=${encodeURIComponent(range ?? "100")}&pin=${pin}`, { method: "POST" })
const res = await fetch(
`/api/room/new?name=${encodeURIComponent(name)}&coords=${coord.latitude},${coord.longitude}&range=${encodeURIComponent(range ?? "100")}&pin=${encodeURIComponent(pin ?? "")}`,
{ method: "POST" }
)
const json = await res.json()
@ -50,8 +44,10 @@
<div class="flex flex-col gap-3 w-1/2">
<input
bind:value={name}
placeholder="Room name"
class="p-2 text-xl rounded-md border-dark-pine-muted bg-light-pine-overlay hover:bg-dark-pine-base/20 dark:bg-dark-pine-overlay hover:dark:bg-light-pine-base/20 duration-100 outline-none focus:ring-2"
placeholder="Room name (Required)"
class="{name
? ''
: 'border-2 border-red-500'} p-2 text-xl rounded-md bg-light-pine-overlay hover:bg-dark-pine-base/20 dark:bg-dark-pine-overlay hover:dark:bg-light-pine-base/20 duration-100 outline-none focus:ring-2"
/>
<input
@ -67,21 +63,16 @@
<span>{coord.latitude},{coord.longitude}</span>
</p>
<div class="flex flex-row items-center gap-2">
Public Room:
<button class="cursor-pointer w-10 h-10 rounded-lg duration-200 flex items-center justify-center {privateRoom ? privateStyle : publicStyle}" onclick={() => (privateRoom = !privateRoom)}>
{#if privateRoom}
<X />
{:else}
<Check />
{/if}
</button>
{#if !privateRoom}
<p class="text-sm italic">The room is flagged as public, everyone can join</p>
{/if}
</div>
<input
bind:value={pin}
type="number"
max="9999"
placeholder="PIN (none if public)"
class="p-2 text-xl rounded-md border-dark-pine-muted bg-light-pine-overlay hover:bg-dark-pine-base/20 dark:bg-dark-pine-overlay hover:dark:bg-light-pine-base/20 duration-100 outline-none focus:ring-2"
/>
<button
type="button"
class="cursor-pointer flex flex-row items-center justify-center gap-3 border border-dark-pine-muted bg-light-pine-overlay hover:bg-dark-pine-base/20 dark:bg-dark-pine-overlay hover:dark:bg-light-pine-base/20 duration-100 rounded-lg h-10 font-bold"
onclick={createRoom}
>

View file

Before

Width:  |  Height:  |  Size: 420 KiB

After

Width:  |  Height:  |  Size: 420 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 446 KiB

After

Width:  |  Height:  |  Size: 446 KiB

Before After
Before After