[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.

Merge branch 'main' into client-search-likeButton

+392 -47
+68 -13
backend/data.tsx
··· 1 1 import { Innertube } from "https://deno.land/x/youtubei@v10.0.0-deno/deno.ts"; 2 + import uniqBy from 'https://cdn.skypack.dev/lodash/uniqBy'; 3 + import shuffle from 'https://cdn.skypack.dev/lodash/shuffle'; 4 + import take from 'https://cdn.skypack.dev/lodash/take'; 2 5 3 6 const youtube = await Innertube.create(); 4 7 ··· 11 14 // @ts-ignore - no type for duration 12 15 const videos = result.videos.filter((item) => item.duration?.seconds).slice(0, 10); 13 16 14 - return videos.map((item) => { 15 - return { 16 - title: item.title.text ?? 'Untitled', 17 - // @ts-ignore - no type for thumbnails 18 - thumbnail: item.thumbnails?.[0].url, 19 - // @ts-ignore - no type for id 20 - id: item.id, 21 - likes: new Set<string>(), 22 - added: Date.now(), 23 - // @ts-ignore - no type for duration 24 - duration: item.duration?.text ?? '-', 25 - }; 26 - }); 17 + return videos.map(item => normalizeVideo(item)); 27 18 } catch (error) { 28 19 console.error(error); 29 20 return []; 30 21 } 31 22 } 23 + 24 + const normalizeVideo = (video: any) => { 25 + console.log(video); 26 + return { 27 + title: video.title.text ?? 'Untitled', 28 + // @ts-ignore - no type for thumbnails 29 + thumbnail: video.thumbnails?.[0].url, 30 + // @ts-ignore - no type for id 31 + id: video.id, 32 + likes: new Set<string>(), 33 + added: Date.now(), 34 + // @ts-ignore - no type for duration 35 + duration: video.duration?.text ?? '-', 36 + } 37 + } 38 + 39 + async function getRelatedVideos(videoId: string) { 40 + try { 41 + const videoDetails = await youtube.getInfo(videoId); 42 + const related = videoDetails.watch_next_feed.map(related => normalizeVideo(related)); 43 + return related; 44 + } catch (error) { 45 + console.error("Error fetching related videos:", error); 46 + throw error; 47 + } 48 + } 49 + 50 + export async function getRecommendations(queue: { id: string }[], maxRecommendations = 5) { 51 + let allRecommendations: string[] = []; 52 + 53 + for (const video of queue) { 54 + try { 55 + const relatedVideos = await getRelatedVideos(video.id); 56 + allRecommendations.push(...relatedVideos.slice(0, maxRecommendations)); 57 + } catch (error) { 58 + console.error(`Error processing video ID ${video.id}:`, error); 59 + } 60 + } 61 + 62 + // filter out duplicates that are in queue 63 + const newRecommendations = allRecommendations.filter((video) => !queue.some((item) => item.id === video.id)); 64 + 65 + const uniqueRecommendations = uniqBy(newRecommendations, 'id'); 66 + 67 + const shuffledRecommendations = shuffle(uniqueRecommendations); 68 + 69 + const finalRecommendations = take(shuffledRecommendations, maxRecommendations); 70 + 71 + console.log("FINDAL RECOMMENDATIONS", finalRecommendations); 72 + return finalRecommendations; 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 + }
+39 -2
backend/sessions.ts
··· 11 11 added: number; 12 12 }; 13 13 14 + export interface Client { 15 + id: string; 16 + name: string; 17 + } 18 + 19 + // TO ADD: In the session, clientName should also be saved together with clientId 14 20 export interface SessionData { 15 21 code: string; 16 22 hostId: string; 17 23 clientIds: Set<string>; 24 + clients: Record<string, Client>; 18 25 queue: Item[]; 26 + recommendedQueue: Item[]; 19 27 currentlyPlaying: Item | null; 20 28 }; 21 29 ··· 56 64 return session; 57 65 } 58 66 59 - export const addClientToSession = async (code: string) => { 67 + 68 + export const updateUser = async (code: string) => { 60 69 const client = await getUserId(); 61 70 const session = sessions[code]; 62 71 if (!session) { ··· 65 74 session.clientIds.add(client.userId); 66 75 67 76 console.log(session); 77 + 78 + return session; 79 + } 80 + 81 + export const addClientsInfo = async (code: string, nick: string) => { 82 + const client = await getUserId(); 83 + const session = sessions[code]; 84 + if (!session) { 85 + return; 86 + } 87 + 88 + session.clients[client.userId] = { 89 + id: client.userId, 90 + name: nick 91 + }; 68 92 69 93 return session; 70 94 } ··· 92 116 } 93 117 94 118 // this breaks shit 95 - //sortVideos(session.queue); 119 + // sortVideos(session.queue); 96 120 97 121 return video; 98 122 } catch (error) { ··· 121 145 code, 122 146 hostId: userId, 123 147 clientIds: new Set() as Set<string>, 148 + clients: {}, 124 149 queue: [] as Item[], 150 + recommendedQueue: [] as Item[], 125 151 currentlyPlaying: null as Item | null, 126 152 }; 127 153 sessions[code] = session; ··· 155 181 if (a.added < b.added) return -1; 156 182 return 0; 157 183 }); 184 + }); 185 + } 186 + 187 + export const getRecommendedQueue = (code: string) => { 188 + const session = sessions[code]; 189 + if (!session) { 190 + console.log("no session!") 191 + return $$([]) 192 + } 193 + return always(() => { 194 + return session.recommendedQueue 158 195 }); 159 196 }
+6 -2
common/client.tsx
··· 1 1 import { QueueItem } from "./components/QueueItem.tsx"; 2 2 import SearchBar from "./components/SearchBar.tsx"; 3 3 import { search } from "backend/data.tsx"; 4 - import { addClientToSession, getSortedQueue, Item } from "backend/sessions.ts"; 4 + import { updateUser, getSortedQueue, Item } from "backend/sessions.ts"; 5 5 import { Context } from "uix/routing/context.ts"; 6 6 import { loadInitialTheme } from "./components/ToggleThemeButton.tsx"; 7 7 import NavMenu from "./components/nav/NavMenu.tsx"; ··· 9 9 export default async function App(ctx: Context) { 10 10 const code = ctx.urlPattern?.pathname.groups[0] ?? "XXXX"; 11 11 12 - const session = await addClientToSession(code); 12 + //get the nick of the user from sessiondata 13 + //const nick = (ctx.searchParams.get('nick') ?? "anon"); 14 + 15 + const session = await updateUser(code); 16 + 13 17 14 18 if (!session) { 15 19 return;
+1 -1
common/components/QR.tsx
··· 19 19 20 20 return <span style={{ color: "black" }}>{loadingText}</span>; 21 21 } else { 22 - const qrCodeSrc = `https://api.qrserver.com/v1/create-qr-code/?data=${encodeURIComponent([new URL(window.location.href).origin, "/client/", code].join(""))}&format=svg` 22 + const qrCodeSrc = `https://api.qrserver.com/v1/create-qr-code/?data=${encodeURIComponent(`${new URL(window.location.href).origin}/welcome?code=${encodeURIComponent(code)}`)}&format=svg`; 23 23 24 24 return <img class="h-full" src={qrCodeSrc} alt="QR code" />; 25 25 }
+2 -1
common/components/QueueItem.tsx
··· 1 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, updateRecommendations } from "backend/data.tsx" 4 5 5 6 const userId = (await getUserId()).userId; 6 7 ··· 216 217 hookElement.classList.remove("fly-in-animation"); 217 218 await sleep(1000); 218 219 toggleLike(code, updatedItem.id); 219 - updateLikeButton(item, code); 220 + updateLikeButton(item, code); await updateRecommendations(session, code); 220 221 } 221 222 } 222 223
+28
common/components/UserDisplay.tsx
··· 1 + export default function UserDisplay({ names }: Readonly<{ names: string[] }>) { 2 + // Filter out any names that are "anon" before rendering 3 + const filteredNames = names.filter(name => name.toLowerCase() !== "anon"); 4 + 5 + const userDisplay = always(() => { 6 + if (filteredNames.length === 0) { 7 + return <></>; 8 + } else { 9 + return ( 10 + <div class="w-full flex justify-center mt-4"> 11 + <div class="flex flex-wrap justify-start items-center gap-4"> 12 + {filteredNames.map((name) => ( 13 + <div class="bg-purple-600 text-white px-4 py-2 rounded-full shadow-lg"> 14 + {name} 15 + </div> 16 + ))} 17 + </div> 18 + </div> 19 + ) 20 + } 21 + }); 22 + 23 + return ( 24 + <> 25 + {userDisplay} 26 + </> 27 + ); 28 + }
+47 -5
common/home.tsx
··· 1 + 1 2 export default function App() { 3 + 4 + const typed_code = $$(""); 5 + 6 + const navigateToWelcome = () => { 7 + const code = typed_code.val.trim() === "" ? "XXXX" : typed_code.val; 8 + window.location.href = `/welcome?code=${encodeURIComponent(code)}`; 9 + }; 10 + 2 11 return ( 3 - <main class="w-screen h-screen relative text-white bg-gray-950"> 12 + <main class="w-screen h-screen absolute inset-0 text-white bg-gray-950"> 13 + 4 14 <div class="absolute w-full bg-[#1f1f26] py-8 px-12"> 5 15 <img src="./rsc/logo_white.svg" alt="Logo" class="h-12"></img> 6 16 </div> 17 + 7 18 <div class="w-full h-full md:flex md:flex-row pt-32"> 8 - <div class="md:h-full w-full md:w-1/2 flex flex-col place-content-center place-items-center gap-6 p-12"> 19 + 20 + <div class="md:h-full w-full md:w-1/2 flex flex-col place-content-center place-items-center gap-6 p-12 bg-gray-950"> 21 + 9 22 <h1 10 23 class="w-full font-semibold text-rose-200 leading-tight" 11 24 style="font-size: 3em;" ··· 13 26 Like YouTube<br></br>but 14 27 <span class="text-rose-500 font-bold"> with friends.</span> 15 28 </h1> 29 + 16 30 <div class="w-full"> 17 31 <p class="max-w-[34rem] text-rose-200 text-lg"> 18 32 Take the hassle out of starting a watch party. Just let your ··· 20 34 autopilot. 21 35 </p> 22 36 </div> 23 - <div class="min-w-full"> 37 + 38 + <div class="min-w-full flex flex-col gap-10 justify-center"> 39 + 40 + <div class="w-full h-px bg-gray-400 my-1 md:my-1"></div> 41 + 24 42 <a href="/player"> 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"> 43 + <button class="bg-rose-500 hover:bg-rose-600 w-full text-white font-medium rounded-full text-2xl py-6 px-10 flex items-center justify-center"> 26 44 <svg 27 45 xmlns="http://www.w3.org/2000/svg" 28 46 viewBox="0 0 24 24" ··· 35 53 clip-rule="evenodd" 36 54 /> 37 55 </svg> 38 - Get started 56 + Start your own party ! 39 57 </button> 40 58 </a> 59 + 60 + <div class="w-full h-px bg-gray-400 my-1 md:my-1"></div> 61 + 62 + <div class="flex flex-col items-center justify-center gap-4 w-full md:w-1/2r"> 63 + <input 64 + type="text" 65 + placeholder="Enter 4-digit code" 66 + value={typed_code} 67 + onchange={() => { 68 + typed_code.val = typed_code; 69 + }} 70 + class="text-black font-bold rounded-lg py-3 w-full text-center text-2xl" 71 + /> 72 + <a rel="noopener noreferrer"> 73 + <button 74 + class="bg-rose-500 hover:bg-rose-600 text-white font-medium rounded-full py-6 px-10 w-full text-2xl" 75 + onclick={navigateToWelcome} 76 + > 77 + Join the party 78 + </button> 79 + </a> 80 + </div> 81 + 41 82 </div> 42 83 </div> 84 + 43 85 <div class="md:h-full w-full md:w-1/2 flex items-center justify-center pt-12 md:pt-0"> 44 86 <div class="absolute w-80 h-80 bg-rose-300 rounded-full z-[-5]"></div> 45 87 <img
+45 -7
common/page.tsx
··· 4 4 import { QueueItem } from "./components/QueueItem.tsx"; 5 5 6 6 import QRCodeOverlay from "./components/QRCodeOverlay.tsx"; 7 - import { getSessionUserHosts, getSortedQueue } from "backend/sessions.ts"; 7 + import UserDisplay from "./components/UserDisplay.tsx"; 8 + import { getSessionUserHosts, getSortedQueue, getRecommendedQueue } from "backend/sessions.ts"; 9 + 8 10 import { NowPlaying } from "./components/NowPlaying.tsx"; 9 11 import addDurations from "./helper.tsx"; 10 12 ··· 15 17 16 18 export default async function App() { 17 19 const session = await getSessionUserHosts(); 20 + 21 + const code = $$(session.code); 18 22 19 - const code = $$(session.code); 23 + const arr = Array.from(session.clientIds); 24 + const num = arr.length; 25 + console.log(arr); 26 + const users = Object.values(session.clients).map(client => client.name); 27 + console.log(users); 20 28 21 29 const current = always(() => { 22 30 if (session.currentlyPlaying) { ··· 34 42 }); 35 43 36 44 const sorted = await getSortedQueue(code); 45 + const recommended = await getRecommendedQueue(code); 37 46 38 47 const timeLeft = always(() => { 39 48 let timeCounter = "0:00"; ··· 45 54 46 55 loadInitialTheme(); 47 56 57 + 58 + const Recommendations = () => { 59 + 60 + // console.log('session', session) 61 + if (recommended.$.length === 0) { 62 + return null 63 + } 64 + 65 + return ( 66 + <div class="flex flex-col gap-3 mt-5"> 67 + <div class="text-white">RECOMMENDED:</div> 68 + <div class="space-y-4">{ 69 + recommended.$.map(item => { 70 + return <QueueItem item={item} type={'search'} code={code}></QueueItem> 71 + })} 72 + </div> 73 + </div> 74 + ) 75 + 76 + } 77 + 48 78 return ( 49 79 <main class="w-screen h-screen relative bg-gray-50 dark:bg-gray-950"> 50 80 <div class="mx-auto grid md:grid-cols-2 h-screen"> 51 - <div class="h-screen hidden md:flex items-center flex-col justify-center p-8"> 52 - <QRCode code={code} /> 53 - <div class="text-black dark:text-white text-3xl font-semibold mt-4"> 54 - Party code: <a target="_blank" href={window.location.origin + '/client/' + code}>{code}</a> 81 + 82 + <div class="flex flex-col h-screen hidden md:flex items-center justify-center p-8"> 83 + <QRCode code={code}/> 84 + <div class="text-black dark:text-white text-3xl font-semibold mt-4 text-center"> 85 + Party code: <a target="_blank" href={`${window.location.origin}/welcome?code=${encodeURIComponent(code)}`}>{code}</a> 55 86 </div> 87 + {/* <div class="text-xl text-white dark:text-white font-semibold"> 88 + {num} 89 + </div> */} 90 + <UserDisplay names={users} /> 91 + 56 92 </div> 93 + 57 94 <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"> 58 95 <div class="flex px-8 mx-0 mt-8 mb-4"> 59 96 <VideoPlayer queue={session.queue} code={code} /> ··· 82 119 return <QueueItem item={item} type={'player'} code={code}></QueueItem> 83 120 })} 84 121 </div> 122 + <Recommendations /> 85 123 </div> 86 124 </div> 87 125 </div> ··· 89 127 <QRCodeOverlay code={code} /> 90 128 </main> 91 129 ); 92 - } 130 + }
+60
common/welcome.tsx
··· 1 + import { redirect } from "uix/utils/window-apis.ts" 2 + import { Context } from "uix/routing/context.ts"; 3 + import { addClientsInfo} from "backend/sessions.ts"; 4 + 5 + export default function Welcome(ctx: Context) { 6 + 7 + const user_name = $$(""); 8 + const code = (ctx.searchParams.get('code') ?? "XXXX"); 9 + 10 + const handleWithNick = () => { 11 + if(!user_name.val){ 12 + return; 13 + } 14 + addClientsInfo(code, user_name.val); 15 + const url = `/client/${encodeURIComponent(code)}`; 16 + redirect(url); 17 + }; 18 + 19 + const handleAnonym = () => { 20 + addClientsInfo(code, "anon"); 21 + const url = `/client/${encodeURIComponent(code)}`; 22 + redirect(url); 23 + }; 24 + 25 + return ( 26 + <main class="w-screen h-screen absolute inset-0 text-white bg-gray-950 flex flex-col items-center justify-center"> 27 + 28 + <h1 class="text-6xl text-rose-500 font-bold m-5 text-center p-8"> Welcome <br></br> to the Party </h1> 29 + 30 + <div class="flex flex-col items-center w-full md:w-1/2 px-5"> 31 + 32 + <input 33 + class="text-black font-bold rounded-lg py-3 w-full text-center text-2xl" 34 + type="text" 35 + placeholder={"Type in your nick for the Party"} 36 + value={user_name} 37 + onchange={() => { 38 + user_name.val = user_name; 39 + }} 40 + /> 41 + 42 + <button 43 + class="bg-rose-500 hover:bg-rose-600 text-white font-bold rounded-full text-2xl py-4 px-8 m-5 w-full" 44 + onclick={handleWithNick} 45 + > Lets Gooo ! 46 + </button> 47 + 48 + <div class="w-full h-px bg-gray-400 my-1 md:my-1"></div> 49 + 50 + <button 51 + class="bg-rose-500 hover:bg-rose-600 text-white font-bold rounded-full text-2xl py-4 px-8 m-5 w-full" 52 + onclick={handleAnonym} 53 + > Continue anonymously :) 54 + </button> 55 + 56 + </div> 57 + 58 + </main> 59 + ); 60 + }
+84 -11
frontend/entrypoint.css
··· 1 1 /* 2 - ! tailwindcss v3.4.3 | MIT License | https://tailwindcss.com 2 + ! tailwindcss v3.4.4 | MIT License | https://tailwindcss.com 3 3 */ 4 4 5 5 /* ··· 823 823 z-index: -5; 824 824 } 825 825 826 + .m-5 { 827 + margin: 1.25rem; 828 + } 829 + 826 830 .mx-0 { 827 831 margin-left: 0px; 828 832 margin-right: 0px; ··· 833 837 margin-right: auto; 834 838 } 835 839 840 + .my-1 { 841 + margin-top: 0.25rem; 842 + margin-bottom: 0.25rem; 843 + } 844 + 836 845 .my-4 { 837 846 margin-top: 1rem; 838 847 margin-bottom: 1rem; 839 848 } 840 849 850 + .mb-12 { 851 + margin-bottom: 3rem; 852 + } 853 + 841 854 .mb-2 { 842 855 margin-bottom: 0.5rem; 843 856 } ··· 856 869 857 870 .mt-4 { 858 871 margin-top: 1rem; 872 + } 873 + 874 + .mt-6 { 875 + margin-top: 1.5rem; 876 + .mt-5 { 877 + margin-top: 1.25rem; 859 878 } 860 879 861 880 .mt-8 { ··· 959 978 width: 20rem; 960 979 } 961 980 962 - .w-fit { 963 - width: -moz-fit-content; 964 - width: fit-content; 965 - } 966 - 967 981 .w-full { 968 982 width: 100%; 969 983 } ··· 982 996 983 997 .max-w-\[34rem\] { 984 998 max-width: 34rem; 999 + } 1000 + 1001 + .max-w-lg { 1002 + max-width: 32rem; 985 1003 } 986 1004 987 1005 .flex-1 { ··· 1005 1023 flex-direction: column; 1006 1024 } 1007 1025 1026 + .flex-wrap { 1027 + flex-wrap: wrap; 1028 + } 1029 + 1008 1030 .place-content-center { 1009 1031 place-content: center; 1010 1032 } ··· 1017 1039 align-items: center; 1018 1040 } 1019 1041 1042 + .justify-start { 1043 + justify-content: flex-start; 1044 + } 1045 + 1020 1046 .justify-end { 1021 1047 justify-content: flex-end; 1022 1048 } ··· 1033 1059 gap: 0px; 1034 1060 } 1035 1061 1036 - .gap-2 { 1037 - gap: 0.5rem; 1062 + .gap-10 { 1063 + gap: 2.5rem; 1064 + } 1065 + 1066 + .gap-4 { 1067 + gap: 1rem; 1068 + } 1069 + 1070 + .gap-3 { 1071 + gap: 0.75rem; 1038 1072 } 1039 1073 1040 1074 .gap-6 { ··· 1102 1136 background-color: rgb(31 31 38 / var(--tw-bg-opacity)); 1103 1137 } 1104 1138 1139 + .bg-gray-400 { 1140 + --tw-bg-opacity: 1; 1141 + background-color: rgb(156 163 175 / var(--tw-bg-opacity)); 1142 + } 1143 + 1105 1144 .bg-gray-50 { 1106 1145 --tw-bg-opacity: 1; 1107 1146 background-color: rgb(249 250 251 / var(--tw-bg-opacity)); ··· 1112 1151 background-color: rgb(3 7 18 / var(--tw-bg-opacity)); 1113 1152 } 1114 1153 1154 + .bg-purple-600 { 1155 + --tw-bg-opacity: 1; 1156 + background-color: rgb(147 51 234 / var(--tw-bg-opacity)); 1157 + } 1158 + 1115 1159 .bg-rose-300 { 1116 1160 --tw-bg-opacity: 1; 1117 1161 background-color: rgb(253 164 175 / var(--tw-bg-opacity)); ··· 1192 1236 padding-right: 0.25rem; 1193 1237 } 1194 1238 1239 + .px-10 { 1240 + padding-left: 2.5rem; 1241 + padding-right: 2.5rem; 1242 + } 1243 + 1195 1244 .px-12 { 1196 1245 padding-left: 3rem; 1197 1246 padding-right: 3rem; ··· 1212 1261 padding-right: 1rem; 1213 1262 } 1214 1263 1215 - .px-6 { 1216 - padding-left: 1.5rem; 1217 - padding-right: 1.5rem; 1264 + .px-5 { 1265 + padding-left: 1.25rem; 1266 + padding-right: 1.25rem; 1218 1267 } 1219 1268 1220 1269 .px-8 { ··· 1252 1301 padding-bottom: 1rem; 1253 1302 } 1254 1303 1304 + .py-6 { 1305 + padding-top: 1.5rem; 1306 + padding-bottom: 1.5rem; 1307 + } 1308 + 1255 1309 .py-8 { 1256 1310 padding-top: 2rem; 1257 1311 padding-bottom: 2rem; ··· 1281 1335 padding-top: 8rem; 1282 1336 } 1283 1337 1338 + .text-center { 1339 + text-align: center; 1340 + } 1341 + 1342 + .text-2xl { 1343 + font-size: 1.5rem; 1344 + line-height: 2rem; 1345 + } 1346 + 1284 1347 .text-3xl { 1285 1348 font-size: 1.875rem; 1286 1349 line-height: 2.25rem; 1350 + } 1351 + 1352 + .text-6xl { 1353 + font-size: 3.75rem; 1354 + line-height: 1; 1287 1355 } 1288 1356 1289 1357 .text-lg { ··· 1641 1709 } 1642 1710 1643 1711 @media (min-width: 768px) { 1712 + .md\:my-1 { 1713 + margin-top: 0.25rem; 1714 + margin-bottom: 0.25rem; 1715 + } 1716 + 1644 1717 .md\:flex { 1645 1718 display: flex; 1646 1719 }
+3 -2
frontend/entrypoint.ts
··· 11 11 const App = await import("../common/page.tsx"); 12 12 return App.default(); 13 13 }, 14 - '/client/([A-Z0-9]{4})': async (ctx: Context) => { 14 + '/client/([A-Z0-9]{4})': async (ctx: Context) => { 15 15 const App = await import("../common/client.tsx"); 16 16 return App.default(ctx); 17 17 }, 18 - 18 + "/welcome": import("../common/welcome.tsx"), 19 + 19 20 "/": import("../common/home.tsx"), 20 21 }
+1
package-lock.json
··· 495 495 "resolved": "https://registry.npmjs.org/concurrently/-/concurrently-8.2.2.tgz", 496 496 "integrity": "sha512-1dP4gpXFhei8IOtlXRE/T/4H88ElHgTiUzh71YUmtjTEHMSRS2Z/fgOxHSxxusGHogsRfxNq1vyAwxSC+EVyDg==", 497 497 "dev": true, 498 + "license": "MIT", 498 499 "dependencies": { 499 500 "chalk": "^4.1.2", 500 501 "date-fns": "^2.30.0",
+3 -3
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", ··· 10 10 "tailwindcss": "^3.4.3" 11 11 }, 12 12 "dependencies": { 13 - "tailwindcss": "^3.4.3", 14 13 "node": "^20.14.0", 15 14 "tailwind-scrollbar": "^3.1.0", 15 + "tailwindcss": "^3.4.3", 16 16 "typescript": "^5.4.5" 17 17 } 18 - } 18 + }
+5
yarn.lock
··· 345 345 resolved "https://registry.npmjs.org/fraction.js/-/fraction.js-4.3.7.tgz" 346 346 integrity sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew== 347 347 348 + fsevents@~2.3.2: 349 + version "2.3.3" 350 + resolved "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz" 351 + integrity sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw== 352 + 348 353 function-bind@^1.1.2: 349 354 version "1.1.2" 350 355 resolved "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz"