├── public ├── worker.ts ├── bars.gif ├── bg.webp ├── vibe.mp4 ├── cache.jpg ├── heart.png ├── favicon.png ├── InviteCardBase.png ├── onboarding │ ├── 1.mp4 │ ├── 2.mp4 │ └── 3.mp4 ├── apple-touch-icon.png ├── icon512_maskable.png ├── manifest.json ├── cursor.svg └── mask.svg ├── src ├── app │ ├── fonts │ │ ├── GeistVF.woff │ │ └── GeistMonoVF.woff │ ├── n │ │ └── [name] │ │ │ └── page.tsx │ ├── socket.ts │ ├── api │ │ ├── logout │ │ │ └── route.ts │ │ ├── login │ │ │ └── route.ts │ │ └── og │ │ │ └── route.tsx │ ├── actions │ │ └── getLoggedInUser.ts │ ├── terms │ │ └── page.tsx │ ├── privacy │ │ ├── page.tsx │ │ └── Privacy.tsx │ ├── browse │ │ ├── page.tsx │ │ └── saved │ │ │ └── page.tsx │ ├── [name] │ │ └── page.tsx │ ├── v │ │ └── page.tsx │ ├── layout.tsx │ ├── globals.css │ └── favicon.svg ├── utils │ └── lock.ts ├── components │ ├── common │ │ ├── Youtube.tsx │ │ ├── Popups.tsx │ │ ├── Header.tsx │ │ ├── VolumeControl.tsx │ │ ├── InviteFriends.tsx │ │ ├── SmoothScroll.tsx │ │ ├── Logo.tsx │ │ ├── Reconnect.tsx │ │ ├── Context.tsx │ │ ├── TopVotes.tsx │ │ ├── HomeFooter.tsx │ │ ├── VibeAlert.tsx │ │ ├── Footer.tsx │ │ ├── PlayButton.tsx │ │ ├── UpvotedBy.tsx │ │ ├── Player.tsx │ │ ├── UpNextSongs.tsx │ │ ├── InviteFriendsToast.tsx │ │ ├── VoteIcon.tsx │ │ ├── _Changelog.tsx │ │ ├── Changelog.tsx │ │ ├── Login.tsx │ │ ├── Feedback.tsx │ │ ├── Home.tsx │ │ ├── DraggableOptions.tsx │ │ ├── ProgressBar.tsx │ │ ├── SpotifyPlaylist.tsx │ │ ├── Controller.tsx │ │ ├── LinkeButton.tsx │ │ ├── PLayerCover.tsx │ │ └── DesktopChangeLog.tsx │ ├── ui │ │ ├── aspect-ratio.tsx │ │ ├── skeleton.tsx │ │ ├── input.tsx │ │ ├── slider.tsx │ │ ├── tooltip.tsx │ │ ├── badge.tsx │ │ ├── button.tsx │ │ ├── card.tsx │ │ ├── hyper-text.tsx │ │ ├── avatar.tsx │ │ ├── dialog.tsx │ │ └── sheet.tsx │ ├── Userprofile.tsx │ ├── Blur.tsx │ ├── ChatIcon.tsx │ ├── Background.tsx │ └── vibe-games │ │ └── GameModal.tsx ├── lib │ ├── api.ts │ ├── utils.ts │ └── types.ts ├── config │ └── firebase.ts ├── Hooks │ ├── useDebounce.ts │ ├── useSelect.tsx │ ├── useAddSong.ts │ └── useCache.tsx ├── middleware.ts └── store │ └── userStore.tsx ├── .eslintrc.json ├── postcss.config.mjs ├── .env.sample ├── components.json ├── .gitignore ├── tsconfig.json ├── package.json ├── tailwind.config.ts └── next.config.mjs /public/worker.ts: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/bars.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/babyo77/vibe/HEAD/public/bars.gif -------------------------------------------------------------------------------- /public/bg.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/babyo77/vibe/HEAD/public/bg.webp -------------------------------------------------------------------------------- /public/vibe.mp4: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/babyo77/vibe/HEAD/public/vibe.mp4 -------------------------------------------------------------------------------- /public/cache.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/babyo77/vibe/HEAD/public/cache.jpg -------------------------------------------------------------------------------- /public/heart.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/babyo77/vibe/HEAD/public/heart.png -------------------------------------------------------------------------------- /public/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/babyo77/vibe/HEAD/public/favicon.png -------------------------------------------------------------------------------- /public/InviteCardBase.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/babyo77/vibe/HEAD/public/InviteCardBase.png -------------------------------------------------------------------------------- /public/onboarding/1.mp4: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/babyo77/vibe/HEAD/public/onboarding/1.mp4 -------------------------------------------------------------------------------- /public/onboarding/2.mp4: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/babyo77/vibe/HEAD/public/onboarding/2.mp4 -------------------------------------------------------------------------------- /public/onboarding/3.mp4: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/babyo77/vibe/HEAD/public/onboarding/3.mp4 -------------------------------------------------------------------------------- /public/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/babyo77/vibe/HEAD/public/apple-touch-icon.png -------------------------------------------------------------------------------- /public/icon512_maskable.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/babyo77/vibe/HEAD/public/icon512_maskable.png -------------------------------------------------------------------------------- /src/app/fonts/GeistVF.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/babyo77/vibe/HEAD/src/app/fonts/GeistVF.woff -------------------------------------------------------------------------------- /src/app/fonts/GeistMonoVF.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/babyo77/vibe/HEAD/src/app/fonts/GeistMonoVF.woff -------------------------------------------------------------------------------- /src/utils/lock.ts: -------------------------------------------------------------------------------- 1 | import { decrypt, encrypt } from "tanmayo7lock"; 2 | 3 | export { decrypt, encrypt }; 4 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["next/core-web-vitals", "next/typescript"], 3 | "rules": { 4 | "@typescript-eslint/no-explicit-any": "off" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /postcss.config.mjs: -------------------------------------------------------------------------------- 1 | /** @type {import('postcss-load-config').Config} */ 2 | const config = { 3 | plugins: { 4 | tailwindcss: {}, 5 | }, 6 | }; 7 | 8 | export default config; 9 | -------------------------------------------------------------------------------- /src/components/common/Youtube.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | import SearchSongPopup from "../SearchSongPopup"; 3 | function Youtube() { 4 | return ; 5 | } 6 | 7 | export default Youtube; 8 | -------------------------------------------------------------------------------- /src/components/ui/aspect-ratio.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import * as AspectRatioPrimitive from "@radix-ui/react-aspect-ratio" 4 | 5 | const AspectRatio = AspectRatioPrimitive.Root 6 | 7 | export { AspectRatio } 8 | -------------------------------------------------------------------------------- /src/lib/api.ts: -------------------------------------------------------------------------------- 1 | import ApiClient from "hmm-api"; 2 | import { toast } from "sonner"; 3 | 4 | const api = new ApiClient({ 5 | toast, 6 | returnParsedError: true, 7 | }); 8 | 9 | export default api; 10 | -------------------------------------------------------------------------------- /src/app/n/[name]/page.tsx: -------------------------------------------------------------------------------- 1 | import { redirect } from "next/navigation"; 2 | 3 | function page({ params }: { params: { name: string } }) { 4 | const name = params.name; 5 | redirect(`https://ngl-drx.vercel.app/${name}`); 6 | } 7 | 8 | export default page; 9 | -------------------------------------------------------------------------------- /.env.sample: -------------------------------------------------------------------------------- 1 | MONGODB_URL= 2 | FIREBASE_API_KEY= 3 | BACKEND_URI= 4 | SOCKET_URI= 5 | JWT_SECRET= 6 | SPOTIFY_CLIENT_ID= 7 | SPOTIFY_CLIENT_SECRET= 8 | SPOTIFY_REDIRECT_URL= 9 | COOKIES= 10 | STREAM_URL= 11 | LOCK_SECRET= 12 | GLOBAL_BACKEND_URI= 13 | VIDEO_STREAM_URI= 14 | BACKGROUND_STREAM_URI= -------------------------------------------------------------------------------- /src/app/socket.ts: -------------------------------------------------------------------------------- 1 | "use client"; 2 | import { io } from "socket.io-client"; 3 | 4 | export const socket = io(process.env.SOCKET_URI || "", { 5 | autoConnect: false, 6 | withCredentials: true, 7 | // forceNew: true, 8 | reconnectionAttempts: Infinity, 9 | // transports: ["websocket"], 10 | }); 11 | -------------------------------------------------------------------------------- /src/components/Userprofile.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | import { TUser } from "@/lib/types"; 3 | import Profile from "./common/Profile"; 4 | 5 | function Userprofile({ user, roomId }: { user: TUser; roomId?: string }) { 6 | return ; 7 | } 8 | 9 | export default Userprofile; 10 | -------------------------------------------------------------------------------- /src/components/ui/skeleton.tsx: -------------------------------------------------------------------------------- 1 | import { cn } from "@/lib/utils" 2 | 3 | function Skeleton({ 4 | className, 5 | ...props 6 | }: React.HTMLAttributes) { 7 | return ( 8 |
12 | ) 13 | } 14 | 15 | export { Skeleton } 16 | -------------------------------------------------------------------------------- /src/components/Blur.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | import { cn } from "@/lib/utils"; 3 | import React from "react"; 4 | 5 | function Blur({ className }: { className?: string }) { 6 | return ( 7 |
13 | ); 14 | } 15 | 16 | export default Blur; 17 | -------------------------------------------------------------------------------- /src/app/api/logout/route.ts: -------------------------------------------------------------------------------- 1 | import { cookies } from "next/headers"; 2 | import { NextRequest, NextResponse } from "next/server"; 3 | 4 | export async function GET(req: NextRequest) { 5 | try { 6 | cookies().delete("vibeId"); 7 | return NextResponse.redirect(new URL("/v", req.nextUrl)); 8 | } catch (error) { 9 | console.log(error); 10 | return NextResponse.json({ error: error }, { status: 500 }); 11 | } 12 | } 13 | export const runtime = "edge"; 14 | -------------------------------------------------------------------------------- /src/components/common/Popups.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import Background from "../Background"; 3 | import InviteFriendsToast from "./InviteFriendsToast"; 4 | import Blur from "../Blur"; 5 | // import Reconnect from "./Reconnect"; 6 | 7 | function Popups() { 8 | return ( 9 | <> 10 | 11 | 12 | 13 | {/* */} 14 | 15 | ); 16 | } 17 | 18 | export default Popups; 19 | -------------------------------------------------------------------------------- /components.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://ui.shadcn.com/schema.json", 3 | "style": "new-york", 4 | "rsc": true, 5 | "tsx": true, 6 | "tailwind": { 7 | "config": "tailwind.config.ts", 8 | "css": "src/app/globals.css", 9 | "baseColor": "zinc", 10 | "cssVariables": true, 11 | "prefix": "" 12 | }, 13 | "aliases": { 14 | "components": "@/components", 15 | "utils": "@/lib/utils", 16 | "ui": "@/components/ui", 17 | "lib": "@/lib", 18 | "hooks": "@/hooks" 19 | } 20 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | .yarn/install-state.gz 8 | 9 | # testing 10 | /coverage 11 | 12 | # next.js 13 | /.next/ 14 | /out/ 15 | 16 | # production 17 | /build 18 | 19 | # misc 20 | .DS_Store 21 | *.pem 22 | 23 | # debug 24 | npm-debug.log* 25 | yarn-debug.log* 26 | yarn-error.log* 27 | 28 | # local env files 29 | .env*.local 30 | .env 31 | # vercel 32 | .vercel 33 | 34 | # typescript 35 | *.tsbuildinfo 36 | next-env.d.ts 37 | -------------------------------------------------------------------------------- /src/config/firebase.ts: -------------------------------------------------------------------------------- 1 | import { getAuth, GoogleAuthProvider } from "firebase/auth"; 2 | import { initializeApp } from "firebase/app"; 3 | 4 | const firebaseConfig = { 5 | apiKey: process.env.FIREBASE_API_KEY, 6 | authDomain: process.env.FIREBASE_AUTH_DOMAIN, 7 | projectId: process.env.FIREBASE_PROJECT_ID, 8 | storageBucket: process.env.FIREBASE_STORAGE_BUCKET, 9 | messagingSenderId: process.env.FIREBASE_MESSAGING_SENDER_ID, 10 | appId: process.env.FIREBASE_APP_ID, 11 | measurementId: process.env.FIREBASE_MEASUREMENT_ID, 12 | }; 13 | export const app = initializeApp(firebaseConfig); 14 | 15 | export const auth = getAuth(); 16 | export const provider = new GoogleAuthProvider(); 17 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "lib": ["dom", "dom.iterable", "esnext"], 4 | "allowJs": true, 5 | "skipLibCheck": true, 6 | "strict": true, 7 | "noEmit": true, 8 | "esModuleInterop": true, 9 | "module": "esnext", 10 | "moduleResolution": "bundler", 11 | "resolveJsonModule": true, 12 | "isolatedModules": true, 13 | "jsx": "preserve", 14 | "incremental": true, 15 | "plugins": [ 16 | { 17 | "name": "next" 18 | } 19 | ], 20 | "paths": { 21 | "@/*": ["./src/*"] 22 | } 23 | }, 24 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts", "src/app/api/og/route"], 25 | "exclude": ["node_modules"] 26 | } 27 | -------------------------------------------------------------------------------- /src/app/actions/getLoggedInUser.ts: -------------------------------------------------------------------------------- 1 | "use server"; 2 | 3 | import { cookies } from "next/headers"; 4 | 5 | export async function getLoggedInUser() { 6 | try { 7 | const vibeId = cookies().get("vibeId")?.value; 8 | const room = cookies().get("room")?.value; 9 | if (!vibeId) return null; 10 | const res = await fetch(`${process.env.SOCKET_URI}/api/vibe`, { 11 | headers: { 12 | Authorization: `${vibeId}`, 13 | cookie: `room=${room} `, 14 | }, 15 | }); 16 | if (!res.ok) throw new Error(`Failed to fetch user: ${res.status}`); 17 | const data = await res.json(); 18 | 19 | return data; 20 | } catch (error: any) { 21 | console.error("Error in getLoggedInUser:", error.message); 22 | return null; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/components/common/Header.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | import { TUser } from "@/lib/types"; 3 | import SearchSongPopup from "../SearchSongPopup"; 4 | import Userprofile from "../Userprofile"; 5 | import Logo from "./Logo"; 6 | 7 | function Header({ user, roomId }: { user: TUser; roomId?: string }) { 8 | return ( 9 |
10 | 11 | 12 | 13 | 14 |
15 | ); 16 | } 17 | 18 | export default Header; 19 | -------------------------------------------------------------------------------- /public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "theme_color": "#000000", 3 | "background_color": "#000000", 4 | "icons": [ 5 | { 6 | "purpose": "maskable", 7 | "sizes": "512x512", 8 | "src": "icon512_maskable.png", 9 | "type": "image/png" 10 | }, 11 | { 12 | "purpose": "any", 13 | "sizes": "512x512", 14 | "src": "icon512_maskable.png", 15 | "type": "image/png" 16 | } 17 | ], 18 | "orientation": "portrait", 19 | "display": "standalone", 20 | "name": "Vibe", 21 | "start_url": "https://getvibe.in/?utm_source=pwa_install", 22 | "scope": "https://getvibe.in/", 23 | "short_name": "Vibe", 24 | "description": "vibe on your favourite songs with your friends", 25 | "edge_side_panel": { 26 | "preferred_width": 800 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/Hooks/useDebounce.ts: -------------------------------------------------------------------------------- 1 | import { useCallback, useEffect, useRef } from "react"; 2 | 3 | type debounceCallback = (...args: any[]) => void; 4 | function useDebounce(callback: debounceCallback, delay: number = 100) { 5 | const timeoutRef = useRef(null); 6 | const debouncedFunction = useCallback( 7 | (...args: any[]) => { 8 | if (timeoutRef.current) { 9 | clearTimeout(timeoutRef.current); 10 | } 11 | timeoutRef.current = setTimeout(() => { 12 | callback(...args); 13 | }, delay); 14 | }, 15 | [callback, delay] 16 | ); 17 | 18 | useEffect(() => { 19 | return () => { 20 | if (timeoutRef.current) { 21 | clearTimeout(timeoutRef.current); 22 | } 23 | }; 24 | }, []); 25 | return debouncedFunction; 26 | } 27 | 28 | export default useDebounce; 29 | -------------------------------------------------------------------------------- /src/components/ui/input.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | 3 | import { cn } from "@/lib/utils"; 4 | 5 | export interface InputProps 6 | extends React.InputHTMLAttributes {} 7 | 8 | const Input = React.forwardRef( 9 | ({ className, type, ...props }, ref) => { 10 | return ( 11 | 20 | ); 21 | } 22 | ); 23 | Input.displayName = "Input"; 24 | 25 | export { Input }; 26 | -------------------------------------------------------------------------------- /src/app/api/login/route.ts: -------------------------------------------------------------------------------- 1 | import { NextRequest, NextResponse } from "next/server"; 2 | 3 | export async function POST(req: NextRequest) { 4 | try { 5 | const { token: accessToken } = await req.json(); 6 | if (accessToken) { 7 | const response = NextResponse.json({ success: true }); 8 | 9 | const cookieExpirationDate = new Date(); 10 | cookieExpirationDate.setDate(cookieExpirationDate.getDate() + 30); 11 | 12 | response.cookies.set("vibeId", accessToken, { 13 | httpOnly: true, 14 | sameSite: "lax", 15 | secure: true, 16 | path: "/", 17 | expires: cookieExpirationDate, 18 | }); 19 | return response; 20 | } 21 | return NextResponse.json({ success: false, data: {} }, { status: 500 }); 22 | } catch (error: any) { 23 | console.log(error.message); 24 | 25 | return NextResponse.json( 26 | { success: false, data: {}, message: error?.message }, 27 | { status: 500 } 28 | ); 29 | } 30 | } 31 | 32 | export const runtime = "edge"; 33 | -------------------------------------------------------------------------------- /src/Hooks/useSelect.tsx: -------------------------------------------------------------------------------- 1 | import { searchResults } from "@/lib/types"; 2 | import { useCallback, useState } from "react"; 3 | import { toast } from "sonner"; 4 | 5 | function useSelect() { 6 | const [selectedSongs, setSelectedSongs] = useState([]); 7 | 8 | const handleSelect = useCallback( 9 | async (song: searchResults, limit: boolean) => { 10 | if (!song) return; 11 | 12 | if (selectedSongs.includes(song)) { 13 | // If the song is already selected (uncheck), remove it from the list 14 | setSelectedSongs(selectedSongs.filter((s) => s !== song)); 15 | } else { 16 | if (selectedSongs.length >= 5 && limit) 17 | return toast.error("Limit reached only 5 songs at a time"); 18 | 19 | // If the song is not selected (check), add it to the list 20 | setSelectedSongs([song, ...selectedSongs]); 21 | } 22 | }, 23 | [selectedSongs] 24 | ); 25 | 26 | return { handleSelect, selectedSongs, setSelectedSongs }; 27 | } 28 | 29 | export default useSelect; 30 | -------------------------------------------------------------------------------- /src/components/common/VolumeControl.tsx: -------------------------------------------------------------------------------- 1 | import { Slider } from "../ui/slider"; 2 | 3 | export function VolumeControl({ 4 | volume, 5 | setVolume, 6 | }: { 7 | volume: number; 8 | setVolume: (volume: number, commit?: boolean) => void; 9 | }) { 10 | // Convert between exponential and linear values using a more dramatic curve 11 | const toExponential = (value: number) => { 12 | // Using exponential curve: e^(ax) - 1 / (e^a - 1) 13 | // 'a' controls the curve shape (higher = more dramatic) 14 | const a = 4; 15 | return (Math.exp(a * value) - 1) / (Math.exp(a) - 1); 16 | }; 17 | 18 | const fromExponential = (value: number) => { 19 | // Inverse of the above function 20 | const a = 4; 21 | return Math.log(value * (Math.exp(a) - 1) + 1) / a; 22 | }; 23 | 24 | return ( 25 | setVolume(toExponential(e[0]), true)} 32 | /> 33 | ); 34 | } 35 | 36 | export default VolumeControl; 37 | -------------------------------------------------------------------------------- /src/components/ui/slider.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import * as React from "react"; 4 | import * as SliderPrimitive from "@radix-ui/react-slider"; 5 | 6 | import { cn } from "@/lib/utils"; 7 | 8 | const Slider = React.forwardRef< 9 | React.ElementRef, 10 | React.ComponentPropsWithoutRef 11 | >(({ className, ...props }, ref) => ( 12 | 20 | 21 | 22 | 23 | 24 | 25 | )); 26 | Slider.displayName = SliderPrimitive.Root.displayName; 27 | 28 | export { Slider }; 29 | -------------------------------------------------------------------------------- /src/components/ui/tooltip.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import * as React from "react" 4 | import * as TooltipPrimitive from "@radix-ui/react-tooltip" 5 | 6 | import { cn } from "@/lib/utils" 7 | 8 | const TooltipProvider = TooltipPrimitive.Provider 9 | 10 | const Tooltip = TooltipPrimitive.Root 11 | 12 | const TooltipTrigger = TooltipPrimitive.Trigger 13 | 14 | const TooltipContent = React.forwardRef< 15 | React.ElementRef, 16 | React.ComponentPropsWithoutRef 17 | >(({ className, sideOffset = 4, ...props }, ref) => ( 18 | 27 | )) 28 | TooltipContent.displayName = TooltipPrimitive.Content.displayName 29 | 30 | export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider } 31 | -------------------------------------------------------------------------------- /src/components/common/InviteFriends.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | import Listeners from "./Listeners"; 3 | // import { PiBookmarkSimpleBold } from "react-icons/pi"; 4 | import React from "react"; 5 | import { cn } from "@/lib/utils"; 6 | import InviteButton from "./inviteButton"; 7 | // import { Dialog, DialogTrigger } from "../ui/dialog"; 8 | import Youtube from "./Youtube"; 9 | import ChatIcon from "../ChatIcon"; 10 | 11 | function InviteFriendsComp({ className }: { className?: string }) { 12 | return ( 13 |
14 | 15 | 16 |
17 | 18 | 19 | {/* 20 | 24 | 25 | 26 | */} 27 |
28 |
29 | ); 30 | } 31 | const InviteFriends = React.memo(InviteFriendsComp); 32 | export default InviteFriends; 33 | -------------------------------------------------------------------------------- /src/components/ui/badge.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | import { cva, type VariantProps } from "class-variance-authority" 3 | 4 | import { cn } from "@/lib/utils" 5 | 6 | const badgeVariants = cva( 7 | "inline-flex items-center rounded-md border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2", 8 | { 9 | variants: { 10 | variant: { 11 | default: 12 | "border-transparent bg-primary text-primary-foreground shadow hover:bg-primary/80", 13 | secondary: 14 | "border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80", 15 | destructive: 16 | "border-transparent bg-destructive text-destructive-foreground shadow hover:bg-destructive/80", 17 | outline: "text-foreground", 18 | }, 19 | }, 20 | defaultVariants: { 21 | variant: "default", 22 | }, 23 | } 24 | ) 25 | 26 | export interface BadgeProps 27 | extends React.HTMLAttributes, 28 | VariantProps {} 29 | 30 | function Badge({ className, variant, ...props }: BadgeProps) { 31 | return ( 32 |
33 | ) 34 | } 35 | 36 | export { Badge, badgeVariants } 37 | -------------------------------------------------------------------------------- /src/app/terms/page.tsx: -------------------------------------------------------------------------------- 1 | import Terms from "./Terms"; 2 | 3 | export async function generateMetadata() { 4 | return { 5 | title: `Vibe Terms of Service`, 6 | description: `Democratic Music Selection:`, 7 | 8 | icons: { icon: "/favicon.png" }, 9 | openGraph: { 10 | title: "Vibe", 11 | description: 12 | "Explore, vote, and enjoy a community-driven music experience where your votes decide the beats.", 13 | url: "https://getvibe.in", 14 | type: "website", 15 | images: [ 16 | { 17 | url: "https://us-east-1.tixte.net/uploads/tanmay111-files.tixte.co/OGIMG.png", 18 | width: 1200, 19 | height: 630, 20 | alt: "Vibe", 21 | }, 22 | ], 23 | }, 24 | twitter: { 25 | card: "summary_large_image", 26 | site: "@tanmay11117", 27 | title: "Vibe", 28 | description: "Vibe together over millions of songs.", 29 | images: [ 30 | { 31 | url: "https://us-east-1.tixte.net/uploads/tanmay111-files.tixte.co/OGIMG.png", 32 | width: 1200, 33 | height: 630, 34 | alt: "Vibe", 35 | }, 36 | { 37 | url: "https://us-east-1.tixte.net/uploads/tanmay111-files.tixte.co/OGIMG.png", 38 | width: 800, 39 | height: 600, 40 | alt: "Vibe Music Collaboration", 41 | }, 42 | ], 43 | }, 44 | }; 45 | } 46 | 47 | function page() { 48 | return ; 49 | } 50 | 51 | export default page; 52 | -------------------------------------------------------------------------------- /src/app/privacy/page.tsx: -------------------------------------------------------------------------------- 1 | import Privacy from "./Privacy"; 2 | 3 | export async function generateMetadata() { 4 | return { 5 | title: `Vibe Privacy Policy`, 6 | description: `Democratic Music Selection:`, 7 | 8 | icons: { icon: "/favicon.png" }, 9 | openGraph: { 10 | title: "Vibe", 11 | description: 12 | "Explore, vote, and enjoy a community-driven music experience where your votes decide the beats.", 13 | url: "https://getvibe.in", 14 | type: "website", 15 | images: [ 16 | { 17 | url: "https://us-east-1.tixte.net/uploads/tanmay111-files.tixte.co/OGIMG.png", 18 | width: 1200, 19 | height: 630, 20 | alt: "Vibe", 21 | }, 22 | ], 23 | }, 24 | twitter: { 25 | card: "summary_large_image", 26 | site: "@tanmay11117", 27 | title: "Vibe", 28 | description: "Vibe together over millions of songs.", 29 | images: [ 30 | { 31 | url: "https://us-east-1.tixte.net/uploads/tanmay111-files.tixte.co/OGIMG.png", 32 | width: 1200, 33 | height: 630, 34 | alt: "Vibe", 35 | }, 36 | { 37 | url: "https://us-east-1.tixte.net/uploads/tanmay111-files.tixte.co/OGIMG.png", 38 | width: 800, 39 | height: 600, 40 | alt: "Vibe Music Collaboration", 41 | }, 42 | ], 43 | }, 44 | }; 45 | } 46 | 47 | function page() { 48 | return ; 49 | } 50 | 51 | export default page; 52 | -------------------------------------------------------------------------------- /src/components/common/SmoothScroll.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | // import { useEffect, useRef } from "react"; 3 | // import Lenis from "@studio-freight/lenis"; 4 | 5 | function SmoothScrollingDiv({ children }: { children: React.ReactNode }) { 6 | // const scrollRef = useRef(null); 7 | // const lenisRef = useRef(null); 8 | 9 | // useEffect(() => { 10 | // if (!scrollRef.current) return; 11 | 12 | // // Initialize Lenis with the scrollable div as the wrapper 13 | // const lenis = new Lenis({ 14 | // wrapper: scrollRef.current, 15 | // }); 16 | 17 | // lenisRef.current = lenis; 18 | 19 | // // Animation frame to continuously update Lenis 20 | // function raf(time: number) { 21 | // lenis.raf(time); 22 | // requestAnimationFrame(raf); 23 | // } 24 | 25 | // requestAnimationFrame(raf); 26 | 27 | // // Clean up when the component unmounts 28 | // return () => { 29 | // lenis.destroy(); 30 | // }; 31 | // }, []); 32 | 33 | // // Update Lenis after dynamic content changes (trigger reflow) 34 | // useEffect(() => { 35 | // if (lenisRef.current) { 36 | // lenisRef.current.resize(); // Recalculate scrolling after content change 37 | // } 38 | // }, [children]); // Re-run when the children (dynamic content) changes 39 | 40 | return ( 41 |
45 | {children} 46 |
47 | ); 48 | } 49 | 50 | export default SmoothScrollingDiv; 51 | -------------------------------------------------------------------------------- /src/components/common/Logo.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { cn } from "@/lib/utils"; 4 | import { useUserContext } from "@/store/userStore"; 5 | import { Pacifico } from "next/font/google"; 6 | import Image from "next/image"; 7 | 8 | const pacifico = Pacifico({ 9 | subsets: ["latin"], 10 | weight: ["400"], 11 | variable: "--font-pacifico", 12 | }); 13 | 14 | function Logo({ className }: { className?: string }) { 15 | const { user } = useUserContext(); 16 | return ( 17 |
{ 19 | if (!user) { 20 | window.location.href = "/"; 21 | } 22 | }} 23 | className={!user ? "cursor-pointer" : ""} 24 | > 25 | 26 | {/*

Vibe

*/} 27 | logo 37 |
38 | ); 39 | } 40 | 41 | export const VibeLogoText = ({ 42 | className, 43 | text, 44 | }: { 45 | className?: string; 46 | text: string; 47 | }) => { 48 | return ( 49 |

56 | {text} 57 |

58 | ); 59 | }; 60 | export default Logo; 61 | -------------------------------------------------------------------------------- /src/components/common/Reconnect.tsx: -------------------------------------------------------------------------------- 1 | // "use client"; 2 | // import { useSocket } from "@/Hooks/useSocket"; 3 | // import { BACKGROUND_APP_TIMEOUT, getRandom } from "@/utils/utils"; 4 | // import { LoaderCircleIcon } from "lucide-react"; 5 | // import Image from "next/image"; 6 | // import { useState } from "react"; 7 | // const message = [ 8 | // { 9 | // msg: "Welcome back", 10 | // gif: "https://media.tenor.com/xAXxRsuJG9QAAAAj/white-rabbit.gif", 11 | // }, 12 | // { 13 | // msg: "Missed you", 14 | // gif: "https://media.tenor.com/8bUmBbKU618AAAAi/cony-and.gif", 15 | // }, 16 | // { 17 | // msg: "oops, i was just hiding", 18 | // gif: "https://media.tenor.com/LMuHrXvRHLQAAAAi/the-simpsons-homer-simpson.gif", 19 | // }, 20 | // ]; 21 | 22 | // function Reconnect() { 23 | // const { hiddenTimeRef } = useSocket(); 24 | // const [emotion] = useState(getRandom(message)); 25 | // if (!hiddenTimeRef.current) return; 26 | // if (hiddenTimeRef.current < BACKGROUND_APP_TIMEOUT) return; 27 | // return ( 28 | //
29 | // {/*

{emotion.msg}

*/} 30 | //
31 | // {emotion.msg} 32 | //
33 | //

Loading latest snaps...

34 | // 35 | //
36 | // ); 37 | // } 38 | 39 | // export default Reconnect; 40 | -------------------------------------------------------------------------------- /src/components/common/Context.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | import React, { useCallback } from "react"; 3 | import { ContextMenuContent, ContextMenuItem } from "../ui/context-menu"; 4 | import { toast } from "sonner"; 5 | import { useUserContext } from "@/store/userStore"; 6 | import { getInviteLink } from "@/utils/utils"; 7 | 8 | function Context() { 9 | const { setIsChatOpen, isChatOpen } = useUserContext(); 10 | const { roomId, user } = useUserContext(); 11 | const handleShare = useCallback(async () => { 12 | if (!roomId) return; 13 | const shareUrl = getInviteLink(roomId, user?.username); 14 | 15 | try { 16 | await navigator.clipboard.writeText(shareUrl); 17 | toast.success("Link copied to clipboard!"); 18 | } catch (error: any) {} 19 | }, [roomId, user]); 20 | return ( 21 | 22 | { 24 | const confirmLeave = window.confirm( 25 | "Are you sure you want to leave this page?" 26 | ); 27 | if (confirmLeave) { 28 | window.location.href = "/browse"; 29 | } 30 | }} 31 | > 32 | Your Rooms 33 | 34 | setIsChatOpen((prev) => !prev)}> 35 | {isChatOpen ? "Close Chat" : " Open Chat"} 36 | 37 | toast.info("Coming soon")} 40 | > 41 | Add to Room 42 | 43 | Invite Friend 44 | 45 | ); 46 | } 47 | 48 | export default Context; 49 | -------------------------------------------------------------------------------- /src/components/ChatIcon.tsx: -------------------------------------------------------------------------------- 1 | import { useUserContext } from "@/store/userStore"; 2 | 3 | function ChatIcon() { 4 | const { seen, setIsChatOpen } = useUserContext(); 5 | return ( 6 |
7 | setIsChatOpen((prev) => !prev)} 14 | className=" cursor-pointer mr-1.5" 15 | > 16 | 22 | 23 |
24 | ); 25 | } 26 | 27 | export default ChatIcon; 28 | -------------------------------------------------------------------------------- /src/Hooks/useAddSong.ts: -------------------------------------------------------------------------------- 1 | import api from "@/lib/api"; 2 | import { searchResults } from "@/lib/types"; 3 | import { useUserContext } from "@/store/userStore"; 4 | import { useCallback } from "react"; 5 | import { toast } from "sonner"; 6 | 7 | function useAddSong() { 8 | const { queue, socketRef, emitMessage } = useUserContext(); 9 | const addSong = useCallback( 10 | async ( 11 | selectedSongs: searchResults[], 12 | roomId?: string | null, 13 | check: boolean = true 14 | ) => { 15 | if (!roomId) return; 16 | let uniqueSongs: searchResults[] = []; 17 | if (check) { 18 | const queuedSongIds = new Set(queue?.map((song) => song.id)); 19 | uniqueSongs = selectedSongs.filter( 20 | (song) => !queuedSongIds.has(song.id) 21 | ); 22 | 23 | if (uniqueSongs.length === 0) { 24 | toast.info("No new songs to add."); 25 | return; 26 | } 27 | } 28 | 29 | toast.loading("Adding songs to queue", { id: "adding" }); 30 | const added = await api.post( 31 | `${process.env.SOCKET_URI}/api/add?room=${roomId}`, 32 | check ? uniqueSongs : selectedSongs 33 | ); 34 | if (added.success) { 35 | emitMessage("update", "update"); 36 | toast.success( 37 | `${selectedSongs.length == 1 ? "Song" : "Songs"} added to ${ 38 | check ? "queue" : roomId 39 | }` 40 | ); 41 | if (!check) { 42 | socketRef.current.emit("event", roomId); 43 | } 44 | } 45 | toast.dismiss("adding"); 46 | }, 47 | [queue, socketRef, emitMessage] 48 | ); 49 | 50 | return { addSong }; 51 | } 52 | 53 | export default useAddSong; 54 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vibe", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "dev": "next dev", 7 | "build": "next build", 8 | "start": "next start", 9 | "lint": "next lint" 10 | }, 11 | "dependencies": { 12 | "@next/third-parties": "^14.2.15", 13 | "@phosphor-icons/react": "^2.1.5-alpha.3", 14 | "@radix-ui/react-aspect-ratio": "^1.1.1", 15 | "@radix-ui/react-avatar": "^1.1.0", 16 | "@radix-ui/react-context-menu": "^2.2.2", 17 | "@radix-ui/react-dialog": "^1.1.4", 18 | "@radix-ui/react-icons": "^1.3.0", 19 | "@radix-ui/react-slider": "^1.2.1", 20 | "@radix-ui/react-slot": "^1.1.0", 21 | "@radix-ui/react-tooltip": "^1.1.2", 22 | "@react-hook/media-query": "^1.1.1", 23 | "canvas-confetti": "^1.9.3", 24 | "class-variance-authority": "^0.7.0", 25 | "clsx": "^2.1.1", 26 | "fast-deep-equal": "^3.1.3", 27 | "firebase": "^11.0.1", 28 | "framer-motion": "^11.11.17", 29 | "hmm-api": "^1.1.3", 30 | "html-react-parser": "^5.1.18", 31 | "linkify-react": "^4.1.3", 32 | "lucide-react": "^0.439.0", 33 | "marked": "^15.0.1", 34 | "next": "^14.2.15", 35 | "react": "^18", 36 | "react-dom": "^18", 37 | "react-icons": "^5.3.0", 38 | "react-qrcode-logo": "^3.0.0", 39 | "react-youtube": "^10.1.0", 40 | "socket.io-client": "^4.7.5", 41 | "sonner": "^1.5.0", 42 | "tailwind-merge": "^2.5.2", 43 | "tailwindcss-animate": "^1.0.7", 44 | "tanmayo7lock": "^1.0.16" 45 | }, 46 | "devDependencies": { 47 | "@tailwindcss/typography": "^0.5.15", 48 | "@types/canvas-confetti": "^1.6.4", 49 | "@types/node": "^20", 50 | "@types/react": "^18", 51 | "@types/react-dom": "^18", 52 | "eslint": "^8", 53 | "eslint-config-next": "14.2.8", 54 | "postcss": "^8", 55 | "tailwindcss": "^3.4.1", 56 | "typescript": "^5" 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/Hooks/useCache.tsx: -------------------------------------------------------------------------------- 1 | import { useAudio } from "@/store/AudioContext"; 2 | import { useUserContext } from "@/store/userStore"; 3 | import getURL, { getBackgroundURL } from "@/utils/utils"; 4 | import { useCallback, useEffect } from "react"; 5 | 6 | function useCache() { 7 | const { currentSong, videoRef, backgroundVideoRef } = useAudio(); 8 | const { showVideo } = useUserContext(); 9 | 10 | const loadVideos = useCallback(async () => { 11 | if (!currentSong) return; 12 | const currentVideoUrl = getURL(currentSong); 13 | const backGroundVideoUrl = getBackgroundURL(currentSong); 14 | if (currentVideoUrl) { 15 | // const cachedCurrentSongUrl = await cacheVideo( 16 | // currentVideoUrl, 17 | // currentSong.id 18 | // ); 19 | 20 | if ( 21 | videoRef?.current && 22 | videoRef?.current.src !== currentVideoUrl + "?v=v" 23 | ) { 24 | videoRef.current.src = currentVideoUrl + "?v=v"; 25 | } 26 | if ( 27 | backgroundVideoRef?.current && 28 | backgroundVideoRef?.current.src !== backGroundVideoUrl + "?v=v" 29 | ) { 30 | backgroundVideoRef.current.src = currentVideoUrl + "?v=v"; 31 | } 32 | } 33 | }, [currentSong, videoRef, backgroundVideoRef]); 34 | 35 | // const cacheUpNextSong = useCallback(async () => { 36 | // for (const song of upNextSongs) { 37 | // if (song.source !== "youtube") return; 38 | // const videoUrl = getURL(song); 39 | // if (videoUrl) { 40 | // await cacheVideo(videoUrl, song.id); 41 | // } 42 | // } 43 | // }, [upNextSongs]); 44 | useEffect(() => { 45 | if (currentSong && currentSong.source == "youtube") { 46 | loadVideos(); 47 | } 48 | }, [currentSong, loadVideos, showVideo]); 49 | 50 | // useEffect(() => { 51 | // if (upNextSongs.length > 0) { 52 | // cacheUpNextSong(); 53 | // } 54 | // }, [upNextSongs, cacheUpNextSong]); 55 | return; 56 | } 57 | 58 | export default useCache; 59 | -------------------------------------------------------------------------------- /src/app/browse/page.tsx: -------------------------------------------------------------------------------- 1 | import { Browse } from "@/components/common/Browse"; 2 | import api from "@/lib/api"; 3 | import { cookies } from "next/headers"; 4 | import { redirect } from "next/navigation"; 5 | import { Suspense } from "react"; 6 | 7 | export async function generateMetadata() { 8 | return { 9 | title: `Your Rooms`, 10 | description: `Democratic Music Selection:`, 11 | 12 | icons: { icon: "/favicon.png" }, 13 | openGraph: { 14 | title: "Vibe", 15 | description: 16 | "Explore, vote, and enjoy a community-driven music experience where your votes decide the beats.", 17 | url: "https://getvibe.in", 18 | type: "website", 19 | images: [ 20 | { 21 | url: "https://us-east-1.tixte.net/uploads/tanmay111-files.tixte.co/OGIMG.png", 22 | width: 1200, 23 | height: 630, 24 | alt: "Vibe", 25 | }, 26 | ], 27 | }, 28 | twitter: { 29 | card: "summary_large_image", 30 | site: "@tanmay11117", 31 | title: "Vibe", 32 | description: "Vibe together over millions of songs.", 33 | images: [ 34 | { 35 | url: "https://us-east-1.tixte.net/uploads/tanmay111-files.tixte.co/OGIMG.png", 36 | width: 1200, 37 | height: 630, 38 | alt: "Vibe", 39 | }, 40 | { 41 | url: "https://us-east-1.tixte.net/uploads/tanmay111-files.tixte.co/OGIMG.png", 42 | width: 800, 43 | height: 600, 44 | alt: "Vibe Music Collaboration", 45 | }, 46 | ], 47 | }, 48 | }; 49 | } 50 | async function page() { 51 | const vibeId = cookies().get("vibeId")?.value; 52 | if (!vibeId) redirect("/"); 53 | const res = await api.get(`${process.env.SOCKET_URI}/api/rooms/browse`, { 54 | headers: { 55 | cookie: `vibeIdR=${vibeId}`, 56 | }, 57 | showErrorToast: false, 58 | }); 59 | if (!res.success) redirect("/"); 60 | 61 | return ( 62 | 63 | {" "} 64 | 65 | ); 66 | } 67 | 68 | export default page; 69 | -------------------------------------------------------------------------------- /src/middleware.ts: -------------------------------------------------------------------------------- 1 | import { NextRequest, NextResponse } from "next/server"; 2 | import { generateRoomId } from "./utils/utils"; 3 | 4 | export default async function middleware(req: NextRequest) { 5 | try { 6 | const url = new URL(req.url); 7 | const vibeId = req.cookies.get("vibeId")?.value; 8 | const room = url.searchParams.get("room"); 9 | const accessToken = url.searchParams.get("vibe_token"); 10 | if (accessToken) { 11 | const response = NextResponse.redirect( 12 | new URL(`${room}`, req.url) // Redirect to the same URL with the room 13 | ); 14 | 15 | await response.cookies.set("vibeId", accessToken, { 16 | httpOnly: true, 17 | sameSite: "strict", 18 | secure: true, 19 | path: "/", 20 | expires: new Date(Date.now() + 7 * 24 * 60 * 60 * 1000), 21 | }); 22 | 23 | return response; 24 | } 25 | if (url.pathname == "/") { 26 | const isLoggedIn = await fetch(`${process.env.SOCKET_URI}/api/@me`, { 27 | headers: { 28 | Authorization: `${vibeId}`, 29 | }, 30 | }); 31 | if (isLoggedIn.ok) { 32 | return NextResponse.redirect(new URL("/browse", req.nextUrl.origin)); 33 | } 34 | } 35 | 36 | if (url.pathname.startsWith("/v")) { 37 | if (room) { 38 | const res = NextResponse.next(); 39 | res.cookies.set("room", room, { 40 | path: "/", 41 | httpOnly: true, 42 | secure: true, 43 | sameSite: "none", 44 | }); 45 | return res; 46 | } 47 | if (!room) { 48 | const newRoomId = generateRoomId(); 49 | const res = NextResponse.redirect( 50 | new URL(`${url.pathname}?room=${newRoomId}`, req.url) 51 | ); 52 | res.cookies.set("room", newRoomId, { 53 | path: "/", 54 | httpOnly: true, 55 | secure: true, 56 | sameSite: "none", 57 | }); 58 | return res; 59 | } 60 | } 61 | return NextResponse.next(); 62 | } catch (error) { 63 | return NextResponse.next(); 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /src/app/[name]/page.tsx: -------------------------------------------------------------------------------- 1 | import { redirect } from "next/navigation"; 2 | import { Metadata } from "next"; 3 | 4 | type Props = { 5 | params: { name: string }; 6 | searchParams: { [key: string]: string | string[] | undefined }; 7 | }; 8 | export async function generateMetadata({ 9 | params, 10 | searchParams, 11 | }: Props): Promise { 12 | const room = params.name; 13 | const username = searchParams["ref"]; 14 | let user = null; 15 | 16 | const res = await fetch(`${process.env.SOCKET_URI}/api/metadata`, { 17 | method: "POST", 18 | credentials: "include", 19 | headers: { 20 | "Content-Type": "application/json", 21 | }, 22 | body: JSON.stringify({ 23 | payload: username ? "user" : "roomAdmin", 24 | text: username ? username : room, 25 | }), 26 | cache: "no-cache", 27 | }); 28 | if (res.ok) { 29 | user = await res.json(); 30 | } 31 | 32 | if (!user) return {}; 33 | return { 34 | title: `Vibe x ${user?.name?.split(" ")[0] || "404"} `, 35 | description: `${user?.name || "404"}'s Inviting you to listen together`, 36 | icons: [ 37 | { 38 | rel: "icon", 39 | url: user?.imageUrl || "/", 40 | }, 41 | ], 42 | openGraph: { 43 | images: [ 44 | { 45 | width: 736, 46 | height: 464, 47 | url: 48 | "https://getvibe.in/api/og?image=" + 49 | user?.imageUrl + 50 | "&name=" + 51 | user?.name || "/", 52 | }, 53 | ], 54 | }, 55 | twitter: { 56 | card: "summary_large_image", 57 | site: "@tanmay11117", 58 | title: `${user?.name?.split(" ")[0]} Vibe`, 59 | description: `${user?.name?.split(" ")[0]} is listening on Vibe`, 60 | images: 61 | "https://getvibe.in/api/og?image=" + 62 | user?.imageUrl + 63 | "&name=" + 64 | user?.name || "/", 65 | }, 66 | }; 67 | } 68 | function page({ params }: { params: { name: string } }) { 69 | const name = params.name; 70 | redirect(`/v?room=${name}`); 71 | } 72 | 73 | export default page; 74 | -------------------------------------------------------------------------------- /src/app/browse/saved/page.tsx: -------------------------------------------------------------------------------- 1 | import { Browse } from "@/components/common/Browse"; 2 | import api from "@/lib/api"; 3 | import { cookies } from "next/headers"; 4 | import { redirect } from "next/navigation"; 5 | import { Suspense } from "react"; 6 | 7 | export async function generateMetadata() { 8 | return { 9 | title: `Your Saved Rooms`, 10 | description: `Democratic Music Selection:`, 11 | 12 | icons: { icon: "/favicon.png" }, 13 | openGraph: { 14 | title: "Vibe", 15 | description: 16 | "Explore, vote, and enjoy a community-driven music experience where your votes decide the beats.", 17 | url: "https://getvibe.in", 18 | type: "website", 19 | images: [ 20 | { 21 | url: "https://us-east-1.tixte.net/uploads/tanmay111-files.tixte.co/OGIMG.png", 22 | width: 1200, 23 | height: 630, 24 | alt: "Vibe", 25 | }, 26 | ], 27 | }, 28 | twitter: { 29 | card: "summary_large_image", 30 | site: "@tanmay11117", 31 | title: "Vibe", 32 | description: "Vibe together over millions of songs.", 33 | images: [ 34 | { 35 | url: "https://us-east-1.tixte.net/uploads/tanmay111-files.tixte.co/OGIMG.png", 36 | width: 1200, 37 | height: 630, 38 | alt: "Vibe", 39 | }, 40 | { 41 | url: "https://us-east-1.tixte.net/uploads/tanmay111-files.tixte.co/OGIMG.png", 42 | width: 800, 43 | height: 600, 44 | alt: "Vibe Music Collaboration", 45 | }, 46 | ], 47 | }, 48 | }; 49 | } 50 | async function page() { 51 | const vibeId = cookies().get("vibeId")?.value; 52 | if (!vibeId) redirect("/"); 53 | const res = await api.get(`${process.env.SOCKET_URI}/api/rooms/saved`, { 54 | headers: { 55 | cookie: `vibeIdR=${vibeId}`, 56 | }, 57 | showErrorToast: false, 58 | }); 59 | if (!res.success) redirect("/"); 60 | 61 | return ( 62 | 63 | {" "} 64 | 65 | ); 66 | } 67 | 68 | export default page; 69 | -------------------------------------------------------------------------------- /src/lib/utils.ts: -------------------------------------------------------------------------------- 1 | import { clsx, type ClassValue } from "clsx"; 2 | import { twMerge } from "tailwind-merge"; 3 | import api from "./api"; 4 | import { ApiResponse } from "hmm-api"; 5 | import { uploadedImageT } from "./types"; 6 | 7 | export function cn(...inputs: ClassValue[]) { 8 | return twMerge(clsx(inputs)); 9 | } 10 | 11 | export const onBoarding = [ 12 | { 13 | heading: "Own your vibe.", 14 | description: 15 | "Get the party started! Add a song you love, and let’s kick things off.", 16 | ctaText: "Save", 17 | }, 18 | { 19 | heading: "Add & Mix", 20 | description: 21 | "Invite everyone to add songs to the queue. The more, the merrier!", 22 | ctaText: "Next", 23 | src: "/onboarding/1.mp4", 24 | }, 25 | { 26 | heading: "Jam Together", 27 | description: 28 | "Get the party started! Add a song you love, and let’s kick things off.", 29 | ctaText: "Next", 30 | src: "/onboarding/2.mp4", 31 | }, 32 | { 33 | heading: "Just vote, no chaos", 34 | description: 35 | "As you listen, your friends can vote on what plays next. It’s all about the music everyone loves.", 36 | ctaText: "Next", 37 | src: "/onboarding/3.mp4", 38 | }, 39 | { 40 | heading: "Just vote, no chaos", 41 | description: 42 | "Start jamming and enjoy the music! Come back anytime to add more friends, songs, or customize the settings.", 43 | ctaText: "Start Listening", 44 | src: "/onboarding/3.mp4", 45 | }, 46 | ]; 47 | 48 | export const uploadImage = async ( 49 | formData: FormData 50 | ): Promise> => { 51 | const rateLimit = await api.get(`${process.env.SOCKET_URI}/api/ping`); 52 | if (rateLimit.error) { 53 | return rateLimit; 54 | } 55 | const res = await api.post( 56 | `${process.env.PROXY_SERVER_URL}/upload-image`, 57 | formData, 58 | { 59 | credentials: "same-origin", 60 | } 61 | ); 62 | await api.get( 63 | `${process.env.SOCKET_URI}/api/ping?url=${res.data?.data.deletion_url}`, 64 | { showErrorToast: false } 65 | ); 66 | return res; 67 | }; 68 | -------------------------------------------------------------------------------- /src/components/ui/button.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import { Slot } from "@radix-ui/react-slot"; 3 | import { cva, type VariantProps } from "class-variance-authority"; 4 | 5 | import { cn } from "@/lib/utils"; 6 | 7 | const buttonVariants = cva( 8 | "inline-flex items-center outline-none justify-center whitespace-nowrap rounded-lg text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-0 focus:outline-none focus-visible:ring-0 disabled:pointer-events-none disabled:opacity-50", 9 | { 10 | variants: { 11 | variant: { 12 | default: 13 | "bg-primary text-primary-foreground shadow hover:bg-primary/90", 14 | destructive: 15 | "bg-destructive text-destructive-foreground hover:bg-destructive/90", 16 | outline: 17 | "border border-input bg-transparent border-white/50 hover:bg-accent hover:text-accent-foreground", 18 | secondary: 19 | "bg-secondary text-secondary-foreground hover:bg-secondary/80", 20 | ghost: "hover:bg-accent hover:text-accent-foreground", 21 | link: "text-primary underline-offset-4 hover:underline", 22 | purple: "w-full bg-purple text-white hover:bg-purple/80", 23 | }, 24 | size: { 25 | default: "h-9 px-4 py-2", 26 | sm: "h-8 rounded-lg px-3 text-xs", 27 | lg: "h-10 rounded-lg px-8", 28 | icon: "h-9 w-9", 29 | }, 30 | }, 31 | defaultVariants: { 32 | variant: "default", 33 | size: "default", 34 | }, 35 | } 36 | ); 37 | 38 | export interface ButtonProps 39 | extends React.ButtonHTMLAttributes, 40 | VariantProps { 41 | asChild?: boolean; 42 | } 43 | 44 | const Button = React.forwardRef( 45 | ({ className, variant, size, asChild = false, ...props }, ref) => { 46 | const Comp = asChild ? Slot : "button"; 47 | return ( 48 | 53 | ); 54 | } 55 | ); 56 | Button.displayName = "Button"; 57 | 58 | export { Button, buttonVariants }; 59 | -------------------------------------------------------------------------------- /src/app/v/page.tsx: -------------------------------------------------------------------------------- 1 | import Home from "@/components/common/Home"; 2 | import { Metadata } from "next"; 3 | import { getLoggedInUser } from "../actions/getLoggedInUser"; 4 | import { cookies } from "next/headers"; 5 | 6 | type Props = { 7 | searchParams: { [key: string]: string | string[] | undefined }; 8 | }; 9 | export async function generateMetadata({ 10 | searchParams, 11 | }: Props): Promise { 12 | const room = searchParams["room"]; 13 | const username = searchParams["ref"]; 14 | let user = null; 15 | 16 | const res = await fetch(`${process.env.SOCKET_URI}/api/metadata`, { 17 | method: "POST", 18 | credentials: "include", 19 | headers: { 20 | "Content-Type": "application/json", 21 | }, 22 | body: JSON.stringify({ 23 | payload: username ? "user" : "roomAdmin", 24 | text: username ? username : room, 25 | }), 26 | cache: "no-cache", 27 | }); 28 | if (res.ok) { 29 | user = await res.json(); 30 | } 31 | 32 | if (!user) return {}; 33 | return { 34 | title: `Vibe x ${user?.name?.split(" ")[0] || "404"} `, 35 | description: `${user?.name || "404"}'s Inviting you to listen together`, 36 | icons: [ 37 | { 38 | rel: "icon", 39 | url: user?.imageUrl || "/", 40 | }, 41 | ], 42 | openGraph: { 43 | images: [ 44 | { 45 | width: 736, 46 | height: 464, 47 | url: 48 | "https://getvibe.in/api/og?image=" + 49 | user?.imageUrl + 50 | "&name=" + 51 | user?.name || "/", 52 | }, 53 | ], 54 | }, 55 | twitter: { 56 | card: "summary_large_image", 57 | site: "@tanmay11117", 58 | title: `${user?.name?.split(" ")[0]} Vibe`, 59 | description: `${user?.name?.split(" ")[0]} is listening on Vibe`, 60 | images: 61 | "https://getvibe.in/api/og?image=" + 62 | user?.imageUrl + 63 | "&name=" + 64 | user?.name || "/", 65 | }, 66 | }; 67 | } 68 | export default async function page() { 69 | const user = await getLoggedInUser(); 70 | const roomId = cookies().get("room")?.value; 71 | return ; 72 | } 73 | -------------------------------------------------------------------------------- /src/components/Background.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | import useCache from "@/Hooks/useCache"; 3 | import { useAudio } from "@/store/AudioContext"; 4 | import { useUserContext } from "@/store/userStore"; 5 | 6 | function Background() { 7 | const { showVideo, setShowVideo } = useUserContext(); 8 | const { currentSong, backgroundVideoRef, state } = useAudio(); 9 | useCache(); 10 | 11 | return ( 12 |
13 | {currentSong?.video ? ( 14 | <> 15 |