[READ-ONLY] Mirror of https://github.com/flo-bit/youtube-party-dj. democratic youtube player, everyone can vote and add songs, uni project, made with uix
0

Configure Feed

Select the types of activity you want to include in your feed.

Refactor: Move updateRecommendations function to backend and apply code changes

+517 -200
+16
backend/data.tsx
··· 22 22 } 23 23 24 24 const normalizeVideo = (video: any) => { 25 + console.log(video); 25 26 return { 26 27 title: video.title.text ?? 'Untitled', 27 28 // @ts-ignore - no type for thumbnails ··· 66 67 const shuffledRecommendations = shuffle(uniqueRecommendations); 67 68 68 69 const finalRecommendations = take(shuffledRecommendations, maxRecommendations); 70 + 71 + console.log("FINDAL RECOMMENDATIONS", finalRecommendations); 69 72 return finalRecommendations; 70 73 } 74 + 75 + export async function updateRecommendations(session: any, code: string) { 76 + const recommendations = await getRecommendations(session.queue); 77 + 78 + recommendations.forEach(() => { 79 + session?.recommendedQueue.pop() 80 + }) 81 + recommendations.forEach((video) => { 82 + session?.recommendedQueue.push(video) 83 + }) 84 + 85 + return session; 86 + }
+42 -14
backend/sessions.ts
··· 1 1 import { Context } from "uix/routing/context.ts"; 2 - import { ObjectRef } from "unyt_core/runtime/pointers.ts"; 2 + import { ObjectRef } from "datex-core-legacy/runtime/pointers.ts"; 3 3 4 4 export type Item = { 5 5 title: string; ··· 21 21 }; 22 22 23 23 // map of session codes to session data 24 - export const sessions = eternalVar('sessions') ?? $$(new Map<string, SessionData>()); 24 + export const sessions = eternalVar('sessions-1234') ?? $$({} as Record<string, SessionData>); 25 25 26 26 export const getAndRemoveNextVideoFromSession = (code: string) => { 27 - const session = sessions.get(code); 27 + const session = sessions[code]; 28 28 console.log(session); 29 29 if (!session) { 30 30 return; ··· 38 38 } 39 39 40 40 export const getSessionWithCode = (code: string) => { 41 - return sessions.get(code); 41 + return sessions[code]; 42 42 } 43 43 44 44 export const getSessionUserHosts = async () => { 45 + console.log('sessions', sessions) 45 46 const user = await getUserId(); 46 - for (const [_code, session] of sessions.entries()) { 47 - if (session.hostId === user.userId) { 48 - return session; 47 + for (const code of Object.keys(sessions)) { 48 + if (sessions[code].hostId === user.userId) { 49 + console.log('found session', sessions[code]); 50 + return sessions[code]; 49 51 } 50 52 } 51 53 ··· 57 59 58 60 export const addClientToSession = async (code: string) => { 59 61 const client = await getUserId(); 60 - const session = sessions.get(code); 62 + const session = sessions[code]; 61 63 if (!session) { 62 64 return; 63 65 } ··· 69 71 } 70 72 71 73 export const toggleLike = async (code: string, videoId: string) => { 72 - console.log(code, videoId); 73 74 try { 74 75 const user = await getUserId(); 75 76 76 - const session = sessions.get(code); 77 + const session = sessions[code]; 77 78 console.log(session); 78 79 if (!session) { 79 80 return; ··· 113 114 let code = null; 114 115 115 116 // check if the code is already in use 116 - while (!code || sessions.get(code)) { 117 + while (!code || sessions[code]) { 117 118 // generate a random 4 character code consisting of uppercase letters and numbers 118 119 code = Array.from({ length: 4 }, () => Math.floor(Math.random() * 36).toString(36).toUpperCase()).join(''); 119 120 } ··· 122 123 hostId: userId, 123 124 clientIds: new Set() as Set<string>, 124 125 queue: [] as Item[], 126 + recommendedQueue: [] as Item[], 125 127 currentlyPlaying: null as Item | null, 126 - recommendedQueue: [] as Item[] 127 128 }; 128 - 129 - sessions.set(code, session); 129 + sessions[code] = session; 130 130 131 131 return session; 132 132 } ··· 142 142 }); 143 143 console.log("sorted", videos); 144 144 } 145 + 146 + export const getSortedQueue = (code: string) => { 147 + const session = sessions[code]; 148 + if (!session) { 149 + console.log("no session!") 150 + return $$([]) 151 + } 152 + return always(() => { 153 + return session.queue.toSorted((a, b) => { 154 + if (a.likes.size > b.likes.size) return -1; 155 + if (a.likes.size < b.likes.size) return 1; 156 + if (a.added > b.added) return 1; 157 + if (a.added < b.added) return -1; 158 + return 0; 159 + }); 160 + }); 161 + } 162 + 163 + export const getRecommendedQueue = (code: string) => { 164 + const session = sessions[code]; 165 + if (!session) { 166 + console.log("no session!") 167 + return $$([]) 168 + } 169 + return always(() => { 170 + return session.recommendedQueue 171 + }); 172 + }
+81 -61
common/client.tsx
··· 1 - import { Queue } from "./components/Queue.tsx"; 1 + import { QueueItem } from "./components/QueueItem.tsx"; 2 2 import SearchBar from "./components/SearchBar.tsx"; 3 - import Settings from "./components/Settings.tsx"; 4 3 import { search } from "backend/data.tsx"; 5 - import { addClientToSession, Item } from "backend/sessions.ts"; 4 + import { addClientToSession, getSortedQueue, Item } from "backend/sessions.ts"; 6 5 import { Context } from "uix/routing/context.ts"; 7 6 import { loadInitialTheme } from "./components/ToggleThemeButton.tsx"; 8 - import NavMenu from "common/components/nav/NavMenu.tsx"; 7 + import NavMenu from "./components/nav/NavMenu.tsx"; 9 8 10 9 export default async function App(ctx: Context) { 11 - const code = (ctx.urlPattern?.pathname.groups[0] ?? "XXXX"); 10 + const code = ctx.urlPattern?.pathname.groups[0] ?? "XXXX"; 12 11 13 - const session = await addClientToSession(code); 12 + const session = await addClientToSession(code); 14 13 15 - if (!session) { 16 - return; 17 - } 14 + if (!session) { 15 + return; 16 + } 18 17 19 - const searchResults: Item[] = $$([]); 18 + const sorted = await getSortedQueue(code); 20 19 21 - const activeView: ('queue' | 'search' | 'settings') = $$('queue'); 20 + const searchResults = $$<Item[]>([]); 22 21 23 - const onSearch = async (value: string) => { 24 - activeView.val = 'search'; 22 + const activeView = $$<"queue" | "search" | "settings">("queue"); 25 23 26 - searchResults.splice(0, searchResults.length); 27 - searchResults.push(...await search(value)); 28 - }; 24 + const showSearch = $$(false); 29 25 30 - const view = always(() => { 31 - if (activeView == 'queue') { 32 - return <Queue items={session.queue} type={'client'} code={code} />; 33 - } else if (activeView == 'search') { 34 - return <Queue items={searchResults} type={'search'} code={code} />; 35 - } 26 + const onSearch = async (value: string) => { 27 + activeView.val = "search"; 36 28 37 - return <Settings />; 38 - }) 29 + showSearch.val = true; 39 30 40 - const menu = always(() => { 41 - if(!activeView) return; 31 + searchResults.splice(0, searchResults.length); 32 + searchResults.push(...(await search(value))); 33 + }; 42 34 43 - console.log('activeView', activeView); 35 + const menu = always(() => { 36 + if (!activeView) return; 44 37 45 - return <NavMenu active={activeView} buttons={[{ 46 - label: 'queue', onClick: () => { 47 - console.log('queue'); 48 - activeView.val = 'queue'; 49 - } 50 - }, { 51 - label: 'search', onClick: () => { 52 - console.log('search'); 53 - activeView.val = 'search'; 54 - } 55 - }, { 56 - label: 'settings', 57 - onClick: () => { 58 - console.log('settings'); 59 - activeView.val = 'settings'; 60 - } 61 - }]} /> 62 - }) 38 + console.log("activeView", activeView); 63 39 64 - loadInitialTheme(); 40 + return ( 41 + <NavMenu 42 + active={activeView} 43 + buttons={[ 44 + { 45 + label: "queue", 46 + onClick: () => { 47 + console.log("queue"); 48 + activeView.val = "queue"; 65 49 66 - return ( 67 - <main class="bg-gray-50 dark:bg-gray-950"> 68 - <div 69 - class="flex flex-col overflow-y-hidden h-[100dvh] rounded-xl mx-auto max-w-2xl" 70 - > 71 - <div class="flex px-4 my-4 "> 72 - <SearchBar onSearch={onSearch} /> 73 - </div> 74 - <div class="px-4 py-4 border-t border-black dark:border-white/20 mx-0 overflow-y-scroll flex-grow"> 75 - {view} 76 - </div> 77 - </div> 78 - {menu} 79 - </main> 80 - ); 50 + showSearch.val = false; 51 + }, 52 + }, 53 + { 54 + label: "search", 55 + onClick: () => { 56 + console.log("search"); 57 + activeView.val = "search"; 58 + 59 + showSearch.val = true; 60 + }, 61 + }, 62 + { 63 + label: "settings", 64 + onClick: () => { 65 + console.log("settings"); 66 + activeView.val = "settings"; 67 + }, 68 + }, 69 + ]} 70 + /> 71 + ); 72 + }); 73 + 74 + loadInitialTheme(); 75 + 76 + return ( 77 + <main class="bg-gray-50 dark:bg-gray-950"> 78 + <div class="flex flex-col overflow-y-hidden h-[100dvh] rounded-xl mx-auto max-w-2xl"> 79 + <div class="flex px-4 my-4 "> 80 + <SearchBar onSearch={onSearch} /> 81 + </div> 82 + <div class="px-4 py-4 border-t border-black dark:border-white/20 mx-0 overflow-y-scroll flex-grow"> 83 + 84 + {toggle(showSearch, 85 + <div class="space-y-4">{ 86 + searchResults.$.map(item => { 87 + return <QueueItem item={item} type={'search'} code={code}></QueueItem> 88 + })} 89 + </div>, 90 + <div class="space-y-4">{ 91 + sorted.$.map(item => { 92 + return <QueueItem item={item} type={'client'} code={code}></QueueItem> 93 + })} 94 + </div>)} 95 + 96 + </div> 97 + </div> 98 + {menu} 99 + </main> 100 + ); 81 101 }
+1 -1
common/components/QR.tsx
··· 1 - import { Pointer } from "unyt_core/runtime/pointers.ts"; 1 + import { Pointer } from "datex-core-legacy/runtime/pointers.ts"; 2 2 3 3 export default function QRCode({ code }: { code: Pointer<string> & string }) { 4 4 const qrCode = always(() => {
+1 -1
common/components/QRCodeOverlay.tsx
··· 1 - import { Pointer } from "unyt_core/runtime/pointers.ts"; 1 + import { Pointer } from "datex-core-legacy/runtime/pointers.ts"; 2 2 import QRCode from "./QR.tsx"; 3 3 4 4 export default function QRCodeOverlay({ code }: { code: Pointer<string> & string }) {
+6 -7
common/components/Queue.tsx
··· 1 + import { ObjectRef } from "datex-core-legacy/runtime/pointers.ts"; 1 2 import { QueueItem } from "./QueueItem.tsx"; 2 3 import { Item } from "backend/sessions.ts"; 3 4 4 - export type QueueType = 'player' | 'client' | 'search' 5 + export type QueueType = 'player' | 'client' | 'search'; 5 6 6 - export function Queue({ items, type, code }: Readonly<{ items: Item[], type: QueueType, code: string }>) { 7 + export function Queue({ items, type, code }: Readonly<{ items: ObjectRef<Item[]>, type: QueueType, code: string }>) { 7 8 return <div class="space-y-4">{ 8 - // @ts-ignore - uix stuff that doesn't work with types 9 - items.$.map((item: Item) => { 10 - // @ts-ignore - uix stuff that doesn't work with types 11 - return <QueueItem item={item.$} type={type} code={code}></QueueItem> 9 + items.$.map(item => { 10 + return <QueueItem item={item} type={type} code={code}></QueueItem> 12 11 })} 13 12 </div> 14 - } 13 + }
+116 -56
common/components/QueueItem.tsx
··· 1 - import { ObjectRef } from "unyt_core/runtime/pointers.ts"; 1 + import { ObjectRef } from "datex-core-legacy/runtime/pointers.ts"; 2 2 import { QueueType } from "./Queue.tsx"; 3 3 import { getSessionWithCode, getUserId, Item, toggleLike } from "backend/sessions.ts"; 4 - import { getRecommendations } from "backend/data.tsx" 4 + import { getRecommendations, updateRecommendations } from "backend/data.tsx" 5 + 6 + const userId = (await getUserId()).userId; 5 7 6 8 export async function QueueItem({ 7 9 item, 8 10 type, 9 11 code 10 - }: Readonly<{ item: ObjectRef<Item>; type: QueueType, code: string }>) { 11 - const userId = (await getUserId()).userId; 12 + }: Readonly<{ item: ObjectRef<Item>; type: QueueType; code: string }>) { 12 13 13 - function getAction() { 14 + async function renderIcon() { 15 + const session = await getSessionWithCode(code); 16 + if (!session) return null; 17 + 18 + if (session.queue.some((v) => v.id == item.id) || session.currentlyPlaying?.id == item.id) { 19 + return ( 20 + <div class="text-black dark:text-green-500 flex items-center justify-center"> 21 + <svg 22 + viewBox="0 0 24 24" 23 + class="size-6 items-center justify-center" 24 + id={`hook-${item.id}`} 25 + > 26 + <path 27 + fill="none" 28 + stroke="currentColor" 29 + stroke-width="2" 30 + stroke-linecap="round" 31 + stroke-linejoin="round" 32 + d="M5 12l5 5L20 7" 33 + /> 34 + </svg> 35 + </div> 36 + ); 37 + } else { 38 + return ( 39 + <svg 40 + xmlns="http://www.w3.org/2000/svg" 41 + fill="none" 42 + viewBox="0 0 24 24" 43 + stroke-width="1.5" 44 + class="size-6 dark:stroke-white stroke-black queueicon2 " 45 + id={`plus-${item.id}`} 46 + > 47 + <path 48 + stroke-linecap="round" 49 + stroke-linejoin="round" 50 + d="M12 4.5v15m7.5-7.5h-15" 51 + /> 52 + </svg> 53 + ); 54 + } 55 + } 56 + 57 + async function handleButtonClick() { 58 + const button = document.getElementById(`button-${item.id}`); 59 + const plusIcon = document.getElementById(`plus-${item.id}`); 60 + const hookIcon = ` 61 + <div class="hook text-black dark:text-green-500 flex items-center justify-center fly-in-animation"> 62 + <svg viewBox="0 0 24 24" class="size-6 items-center justify-center"> 63 + <path 64 + fill="none" 65 + stroke="currentColor" 66 + stroke-width="2" 67 + stroke-linecap="round" 68 + stroke-linejoin="round" 69 + d="M5 12l5 5L20 7" 70 + /> 71 + </svg> 72 + </div> 73 + `; 74 + 75 + if (button && plusIcon) { 76 + plusIcon.classList.add("fly-out-animation"); 77 + await sleep(220); 78 + plusIcon.classList.remove("fly-out-animation"); 79 + button.innerHTML = hookIcon; 80 + const hookElement = button.querySelector('.hook'); 81 + if (hookElement) { 82 + hookElement.classList.add("fly-in-animation"); 83 + await sleep(220); 84 + hookElement.classList.remove("fly-in-animation"); 85 + } 86 + } 87 + } 88 + 89 + async function getAction() { 14 90 if (type === "player") { 15 91 return ( 16 92 <> 17 - <div class="font-semibold">{always(() => item.likes.val.size)}</div> 93 + <div class="font-semibold">{always(() => item.likes.size)}</div> 18 94 <svg 19 95 xmlns="http://www.w3.org/2000/svg" 20 96 viewBox="0 0 24 24" ··· 33 109 }} 34 110 > 35 111 {always(() => { 36 - // this is a mindfuck and i blame uix, but it has to be this way for the always thing to work 37 - if (item.likes.val.has(userId)) 38 - return (<div class="text-accent-500 fill-accent-500 stroke-accent-500 flex"> 39 - <div class="font-semibold">{item.likes.val.size}</div><svg 40 - xmlns="http://www.w3.org/2000/svg" 41 - viewBox="0 0 24 24" 42 - fill="currentColor" 43 - class="size-6 ml-1" 44 - > 45 - <path d="m11.645 20.91-.007-.003-.022-.012a15.247 15.247 0 0 1-.383-.218 25.18 25.18 0 0 1-4.244-3.17C4.688 15.36 2.25 12.174 2.25 8.25 2.25 5.322 4.714 3 7.688 3A5.5 5.5 0 0 1 12 5.052 5.5 5.5 0 0 1 16.313 3c2.973 0 5.437 2.322 5.437 5.25 0 3.925-2.438 7.111-4.739 9.256a25.175 25.175 0 0 1-4.244 3.17 15.247 15.247 0 0 1-.383.219l-.022.012-.007.004-.003.001a.752.752 0 0 1-.704 0l-.003-.001Z" /> 46 - </svg></div>) 112 + if (item.likes.has(userId)) 113 + return ( 114 + <div class="text-accent-500 fill-accent-500 stroke-accent-500 flex"> 115 + <div class="font-semibold">{item.likes.size}</div> 116 + <svg 117 + xmlns="http://www.w3.org/2000/svg" 118 + viewBox="0 0 24 24" 119 + fill="currentColor" 120 + class="size-6 ml-1 bounce-animation" 121 + > 122 + <path d="m11.645 20.91-.007-.003-.022-.012a15.247 15.247 0 0 1-.383-.218 25.18 25.18 0 0 1-4.244-3.17C4.688 15.36 2.25 12.174 2.25 8.25 2.25 5.322 4.714 3 7.688 3A5.5 5.5 0 0 1 12 5.052 5.5 5.5 0 0 1 16.313 3c2.973 0 5.437 2.322 5.437 5.25 0 3.925-2.438 7.111-4.739 9.256a25.175 25.175 0 0 1-4.244 3.17 15.247 15.247 0 0 1-.383.219l-.022.012-.007.004-.003.001a.752.752 0 0 1-.704 0l-.003-.001Z" /> 123 + </svg> 124 + </div> 125 + ); 47 126 else 48 127 return ( 49 128 <div class="flex"> 50 - <div class="font-semibold">{item.likes.val.size}</div> 129 + <div class="font-semibold">{item.likes.size}</div> 51 130 <svg 52 131 xmlns="http://www.w3.org/2000/svg" 53 132 fill="none" ··· 61 140 d="M21 8.25c0-2.485-2.099-4.5-4.688-4.5-1.935 0-3.597 1.126-4.312 2.733-.715-1.607-2.377-2.733-4.313-2.733C5.1 3.75 3 5.765 3 8.25c0 7.22 9 12 9 12s9-4.78 9-12Z" 62 141 /> 63 142 </svg> 64 - </div>) 143 + </div> 144 + ); 65 145 })} 66 146 </button> 67 147 ); ··· 69 149 return ( 70 150 <button 71 151 onclick={async () => { 152 + await handleButtonClick(); 153 + await sleep(250); 72 154 const clickCode = code.length ? code : code.val 73 155 const session = await getSessionWithCode(clickCode); 74 - // check if session is null 75 - if (!session) { 76 - return; 77 - } 156 + if (!session) return; 78 157 79 - // check if queue already contains the video 80 - if (session.queue.some((v) => v.id == item.id)) { 158 + if ((session.queue.some((v) => v.id == item.id) || session.currentlyPlaying?.id == item.id)) { 81 159 return; 82 160 } 83 - 84 - session?.$.queue.val.push(item); 161 + session?.queue.push(item); 162 + toggleLike(code, item.id); 85 163 86 - const recommendations = await getRecommendations(session?.$.queue.val); 87 - console.log(recommendations) 88 - // remove all elements from the recommended queue 89 - recommendations.forEach(() => { 90 - session?.$.recommendedQueue.val.pop() 91 - }) 92 - recommendations.forEach((video) => { 93 - session?.$.recommendedQueue.val.push(video) 94 - }) 164 + await updateRecommendations(session, code); 95 165 }} 96 - class="queueframe bg-white dark:bg-white/5 border border-black dark:border-white/10 rounded-full w-10 h-10 flex items-center justify-center" 166 + id={`button-${item.id}`} 167 + class="queueframe bg-white dark:bg-white/5 border border-black dark:border-white/10 rounded-full w-10 h-10 flex items-center justify-center overflow-hidden " 97 168 > 98 - <svg 99 - xmlns="http://www.w3.org/2000/svg" 100 - fill="none" 101 - viewBox="0 0 24 24" 102 - stroke-width="1.5" 103 - class="size-6 dark:stroke-white stroke-black queueicon2" 104 - > 105 - <path 106 - stroke-linecap="round" 107 - stroke-linejoin="round" 108 - d="M12 4.5v15m7.5-7.5h-15" 109 - /> 110 - </svg> 169 + {await renderIcon()} 111 170 </button> 112 171 ); 113 172 } 114 173 } 115 174 116 175 return ( 117 - <div class="queueframe w-full rounded-xl bg-white dark:bg-white/5 border border-black dark:border-white/10 h-20 overflow-hidden mb-2"> 176 + <div class="queueframe w-full rounded-xl bg-white dark:bg-white/5 border border-black dark:border-white/10 h-20 overflow-hidden mb-2 "> 118 177 <div class="queueitem text-black dark:text-white flex items-left h-full"> 119 178 <img src={item.thumbnail} class="h-20 w-32 object-cover" alt=" " /> 120 179 <div class="flex flex-1 flex-grow justify-between"> 121 180 <div class="pl-4 justify-center flex flex-col h-full"> 122 - <p class="line-clamp-2 font-bold text-md leading-6">{item.title} </p> 181 + <p class="line-clamp-2 font-bold text-md leading-6">{item.title}</p> 123 182 <p class="text-xs">{item.duration} minutes</p> 124 183 </div> 125 - <div class="queueicon2 flex h-full justify-center items-center px-2 dark:stroke-white stroke-black"> 126 - {getAction()} 184 + <div class="queueicon2 flex h-full justify-center items-center stroke-black dark:stroke-white px-2"> 185 + {await getAction()} 127 186 </div> 128 187 </div> 129 188 </div> 130 - </div>) 131 - }; 189 + </div> 190 + ); 191 + }
+1 -1
common/components/Settings.tsx
··· 1 - import ToggleThemeButton from "common/components/ToggleThemeButton.tsx"; 1 + import ToggleThemeButton from "./ToggleThemeButton.tsx"; 2 2 3 3 export default function Settings() { 4 4 return (<div class="text-black dark:text-white">
+1 -1
common/components/VideoPlayer.tsx
··· 7 7 async function playNext() { 8 8 // @ts-ignore - YouTube API 9 9 if (!player || player.getPlayerState() !== YT.PlayerState.PLAYING) { 10 - const video = await getAndRemoveNextVideoFromSession(code.val); 10 + const video = await getAndRemoveNextVideoFromSession(code); 11 11 if (video) { 12 12 play(video.id); 13 13 }
+11
common/helper.tsx
··· 1 + export default function addDurations(duration1: string, duration2: string) { 2 + const duration1Partitions = duration1.split(":").map(Number); 3 + const duration2Partitions = duration2.split(":").map(Number); 4 + const duration1Seconds = duration1Partitions[0] * 60 + duration1Partitions[1]; 5 + const duration2Seconds = duration2Partitions[0] * 60 + duration2Partitions[1]; 6 + const totalSeconds = duration1Seconds + duration2Seconds; 7 + const minutes = Math.floor(totalSeconds / 60); 8 + let seconds = totalSeconds % 60; 9 + seconds = seconds < 10 ? "0" + seconds : seconds; 10 + return `${minutes}:${seconds}`; 11 + }
+1 -1
common/home.tsx
··· 21 21 </p> 22 22 </div> 23 23 <div class="min-w-full"> 24 - <a href="/player" rel="noopener noreferrer"> 24 + <a href="/player"> 25 25 <button class="bg-rose-500 hover:bg-rose-600 w-fit text-white font-medium gap-2 rounded-full font-lg py-3 px-6 flex items-center"> 26 26 <svg 27 27 xmlns="http://www.w3.org/2000/svg"
+97 -55
common/page.tsx
··· 1 1 import QRCode from "./components/QR.tsx"; 2 2 import VideoPlayer from "./components/VideoPlayer.tsx"; 3 3 import { Queue } from "./components/Queue.tsx"; 4 + import { QueueItem } from "./components/QueueItem.tsx"; 5 + 4 6 import QRCodeOverlay from "./components/QRCodeOverlay.tsx"; 5 - import { getSessionUserHosts } from "backend/sessions.ts"; 7 + import { getSessionUserHosts, getSortedQueue, getRecommendedQueue } from "backend/sessions.ts"; 6 8 import { NowPlaying } from "./components/NowPlaying.tsx"; 7 - import ToggleThemeButton, { loadInitialTheme } from "./components/ToggleThemeButton.tsx"; 9 + import addDurations from "./helper.tsx"; 10 + 11 + import ToggleThemeButton, { 12 + loadInitialTheme, 13 + } from "./components/ToggleThemeButton.tsx"; 14 + import { Item } from "../backend/sessions.ts"; 8 15 9 16 export default async function App() { 10 - const session = await getSessionUserHosts() 17 + const session = await getSessionUserHosts(); 11 18 12 - const code = $$(session.code as string); 19 + const code = $$(session.code); 13 20 14 - const current = always(() => { 15 - if (session.currentlyPlaying) { 16 - return <div class="px-4 mx-0"> 17 - <div class="text-accent-500 font-semibold text-sm mb-2">currently playing</div> 18 - <NowPlaying item={session.currentlyPlaying.$} /> 19 - </div> 20 - } else { 21 - return null; 22 - } 23 - }) 21 + const current = always(() => { 22 + if (session.currentlyPlaying) { 23 + return ( 24 + <div class="px-4 mx-0"> 25 + <div class="text-accent-500 font-semibold text-sm mb-2"> 26 + currently playing 27 + </div> 28 + <NowPlaying item={session.currentlyPlaying} /> 29 + </div> 30 + ); 31 + } else { 32 + return null; 33 + } 34 + }); 24 35 25 - loadInitialTheme(); 36 + const sorted = await getSortedQueue(code); 37 + const recommended = await getRecommendedQueue(code); 26 38 27 - const Recommendations = ({ recommendedQueue }) => { 28 - return always(() => { 29 - // console.log('session', session) 30 - if (session.$.recommendedQueue.length === 0) { 31 - return null 32 - } 39 + const timeLeft = always(() => { 40 + let timeCounter = "0:00"; 41 + session.queue.forEach((item: Item) => { 42 + timeCounter = addDurations(timeCounter, item.duration); 43 + }); 44 + return timeCounter; 45 + }); 33 46 34 - return ( 35 - <div class="flex flex-col gap-3 mt-5"> 36 - <div class="text-white">RECOMMENDED:</div> 37 - <Queue items={session.$.recommendedQueue} type={'search'} code={code} /> 38 - </div> 39 - ) 40 - }) 47 + loadInitialTheme(); 48 + 49 + const Recommendations = () => { 50 + 51 + // console.log('session', session) 52 + if (recommended.$.length === 0) { 53 + return null 54 + } 55 + 56 + return ( 57 + <div class="flex flex-col gap-3 mt-5"> 58 + <div class="text-white">RECOMMENDED:</div> 59 + <div class="space-y-4">{ 60 + recommended.$.map(item => { 61 + return <QueueItem item={item} type={'search'} code={code}></QueueItem> 62 + })} 63 + </div> 64 + </div> 65 + ) 66 + 41 67 } 42 68 43 - return ( 44 - <main class="w-screen h-screen relative bg-gray-50 dark:bg-gray-950"> 45 - <div class="mx-auto grid md:grid-cols-2 h-screen"> 46 - <div class="h-screen hidden md:flex items-center flex-col justify-center p-8"> 47 - <QRCode code={code} /> 48 - <div class="text-black dark:text-white text-3xl font-semibold mt-4">Party code: <span>{code}</span></div> 49 - </div> 50 - <div 51 - class="flex flex-col overflow-y-hidden h-screen bg-white dark:bg-white/5 border border-black dark:border-white/10 rounded-xl" 52 - > 53 - <div class="flex px-8 mx-0 mt-8 mb-4"> 54 - {/* @ts-ignore - uix doesn't support types? */} 55 - <VideoPlayer queue={session.queue} code={code} /> 56 - </div> 57 - <div class="flex items-center justify-end px-12 h-10 mb-4"> 58 - <ToggleThemeButton /> 59 - </div> 60 - {current} 61 - <div 62 - class="px-4 py-4 border-t border-black dark:border-white/20 mx-0 overflow-y-scroll flex-grow"> 63 - <Queue items={session.$.queue} type={'player'} code={code} /> 64 - <Recommendations recommendedQueue={session.$.recommendedQueue} /> 69 + return ( 70 + <main class="w-screen h-screen relative bg-gray-50 dark:bg-gray-950"> 71 + <div class="mx-auto grid md:grid-cols-2 h-screen"> 72 + <div class="h-screen hidden md:flex items-center flex-col justify-center p-8"> 73 + <QRCode code={code} /> 74 + <div class="text-black dark:text-white text-3xl font-semibold mt-4"> 75 + Party code: <a target="_blank" href={window.location.origin + '/client/' + code}>{code}</a> 76 + </div> 77 + </div> 78 + <div class="flex flex-col overflow-y-hidden h-screen bg-white dark:bg-white/5 border border-black dark:border-white/10 rounded-xl"> 79 + <div class="flex px-8 mx-0 mt-8 mb-4"> 80 + <VideoPlayer queue={session.queue} code={code} /> 81 + </div> 82 + <div class="flex items-center justify-end px-12 h-10 mb-4"> 83 + <ToggleThemeButton /> 84 + </div> 65 85 66 - </div> 67 - </div> 68 - </div> 86 + {current} 87 + <div class="px-4 py-4 border-t border-black dark:border-white/20 dark:text-white mx-0 overflow-y-scroll flex-grow"> 88 + {always(() => 89 + timeLeft.val !== "0:00" ? ( 90 + <p class="mb-4"> Queue will last for {timeLeft} </p> 91 + ) : ( 92 + <div> 93 + <p class="mb-2 font-bold">The queue is empty right now.</p> 94 + <p> 95 + Be the first one to add a song! Just scan the code on the 96 + left. 97 + </p> 98 + </div> 99 + ) 100 + )} 101 + <div class="space-y-4">{ 102 + sorted.$.map(item => { 103 + return <QueueItem item={item} type={'player'} code={code}></QueueItem> 104 + })} 105 + </div> 106 + <Recommendations /> 107 + </div> 108 + </div> 109 + </div> 69 110 70 - <QRCodeOverlay code={code} /> 71 - </main>); 111 + <QRCodeOverlay code={code} /> 112 + </main> 113 + ); 72 114 }
+77
frontend/entrypoint.css
··· 1442 1442 transition-duration: 200ms; 1443 1443 } 1444 1444 1445 + @keyframes bounce { 1446 + 0% { 1447 + transform: scale(0.5); 1448 + } 1449 + 1450 + 25% { 1451 + transform: scale(1); 1452 + } 1453 + 1454 + 50% { 1455 + transform: scale(1.2); 1456 + } 1457 + 1458 + 85% { 1459 + transform: scale(0.9); 1460 + } 1461 + 1462 + 100% { 1463 + transform: scale(1); 1464 + } 1465 + } 1466 + 1467 + .bounce-animation { 1468 + animation: bounce 0.4s; 1469 + } 1470 + 1471 + .size-6 path { 1472 + transition: d 0.3s ease-in-out; 1473 + } 1474 + 1475 + @keyframes fly-in { 1476 + from { 1477 + transform: translateY(-40px); 1478 + opacity: 0; 1479 + } 1480 + 1481 + to { 1482 + transform: translateY(0); 1483 + opacity: 1; 1484 + } 1485 + } 1486 + 1487 + .fly-in-animation { 1488 + animation: fly-in 0.22s ease-out forwards; 1489 + } 1490 + 1491 + @keyframes fly-out { 1492 + 0% { 1493 + transform: translateY(0); 1494 + opacity: 1; 1495 + } 1496 + 1497 + 25% { 1498 + transform: translateY(10px); 1499 + opacity: 1; 1500 + } 1501 + 1502 + 50% { 1503 + transform: translateY(20px); 1504 + opacity: 1; 1505 + } 1506 + 1507 + 100% { 1508 + transform: translateY(40px); 1509 + opacity: 0; 1510 + } 1511 + } 1512 + 1513 + .fly-out-animation { 1514 + animation: fly-out 0.22s ease-out forwards; 1515 + } 1516 + 1445 1517 .placeholder\:text-black::-moz-placeholder { 1446 1518 --tw-text-opacity: 1; 1447 1519 color: rgb(0 0 0 / var(--tw-text-opacity)); ··· 1534 1606 .dark\:text-cyan-400:is(.dark *) { 1535 1607 --tw-text-opacity: 1; 1536 1608 color: rgb(34 211 238 / var(--tw-text-opacity)); 1609 + } 1610 + 1611 + .dark\:text-green-500:is(.dark *) { 1612 + --tw-text-opacity: 1; 1613 + color: rgb(34 197 94 / var(--tw-text-opacity)); 1537 1614 } 1538 1615 1539 1616 .dark\:text-white:is(.dark *) {
+65 -1
frontend/main.css
··· 1 1 @tailwind base; 2 2 @tailwind components; 3 - @tailwind utilities; 3 + @tailwind utilities; 4 + 5 + @keyframes bounce { 6 + 0% { 7 + transform: scale(0.5); 8 + }25% { 9 + transform: scale(1); 10 + } 11 + 50% { 12 + transform: scale(1.2); 13 + } 14 + 85% { 15 + transform: scale(0.9); 16 + } 17 + 100% { 18 + transform: scale(1); 19 + } 20 + } 21 + 22 + .bounce-animation { 23 + animation: bounce 0.4s; 24 + } 25 + 26 + .size-6 path { 27 + transition: d 0.3s ease-in-out; 28 + } 29 + 30 + @keyframes fly-in { 31 + from { 32 + transform: translateY(-40px); 33 + opacity: 0; 34 + } 35 + to { 36 + transform: translateY(0); 37 + opacity: 1; 38 + } 39 + } 40 + 41 + .fly-in-animation { 42 + animation: fly-in 0.22s ease-out forwards; 43 + } 44 + 45 + @keyframes fly-out { 46 + 0% { 47 + transform: translateY(0); 48 + opacity: 1; 49 + } 50 + 25% { 51 + transform: translateY(10px); 52 + opacity: 1; 53 + } 54 + 50% { 55 + transform: translateY(20px); 56 + opacity: 1; 57 + } 58 + 100% { 59 + transform: translateY(40px); 60 + opacity: 0; 61 + } 62 + } 63 + 64 + 65 + .fly-out-animation { 66 + animation: fly-out 0.22s ease-out forwards; 67 + }
+1 -1
package.json
··· 1 1 { 2 2 "scripts": { 3 - "dev": "concurrently \"uix -l\" \"npx tailwindcss -i ./frontend/main.css -o ./frontend/entrypoint.css --watch\"" 3 + "dev": "concurrently \"uix --reload -l\" \"npx tailwindcss -i ./frontend/main.css -o ./frontend/entrypoint.css --watch\"" 4 4 }, 5 5 "devDependencies": { 6 6 "@tailwindcss/forms": "^0.5.7",