From cbc302b0131ea471971f55d385334f13ce53076c Mon Sep 17 00:00:00 2001 From: RafaelCaso <94573618+RafaelCaso@users.noreply.github.com> Date: Mon, 30 Sep 2024 18:39:09 -0400 Subject: [PATCH] add random song functionality --- .../deploy/01_deploy_sound_scaffold.ts | 3 - packages/nextjs/app/listen/page.tsx | 108 ++++++++++++---- .../app/mymusic/_components/SongList.tsx | 35 +++--- packages/nextjs/app/page.tsx | 58 +++++---- packages/nextjs/app/upload/page.tsx | 116 ++++++++++-------- packages/nextjs/components/Header.tsx | 3 +- .../ScaffoldEthAppWithProviders.tsx | 2 +- 7 files changed, 204 insertions(+), 121 deletions(-) diff --git a/packages/hardhat/deploy/01_deploy_sound_scaffold.ts b/packages/hardhat/deploy/01_deploy_sound_scaffold.ts index 753a0ba..dad8158 100644 --- a/packages/hardhat/deploy/01_deploy_sound_scaffold.ts +++ b/packages/hardhat/deploy/01_deploy_sound_scaffold.ts @@ -14,7 +14,6 @@ const deploySoundScaffold: DeployFunction = async function (hre: HardhatRuntimeE const ownerAddress = "0xd1B41bE30F980315b8A6b754754aAa299C7abea2"; - // Get the deployed contract to interact with it after deploying. const soundScaffold = await hre.ethers.getContract("SoundScaffold", deployer); const soundScaffoldAddress = await soundScaffold.getAddress(); const transferOwnership = await soundScaffold.transferOwnership(ownerAddress); @@ -24,6 +23,4 @@ const deploySoundScaffold: DeployFunction = async function (hre: HardhatRuntimeE export default deploySoundScaffold; -// Tags are useful if you have multiple deploy files and only want to run one of them. -// e.g. yarn deploy --tags YourContract deploySoundScaffold.tags = ["SoundScaffold"]; diff --git a/packages/nextjs/app/listen/page.tsx b/packages/nextjs/app/listen/page.tsx index e14b75b..8747187 100644 --- a/packages/nextjs/app/listen/page.tsx +++ b/packages/nextjs/app/listen/page.tsx @@ -1,6 +1,6 @@ "use client"; -import { useEffect, useState } from "react"; +import { useCallback, useEffect, useRef, useState } from "react"; import SongList from "../mymusic/_components/SongList"; import { useScaffoldEventHistory } from "~~/hooks/scaffold-eth"; @@ -12,11 +12,14 @@ interface Song { } const Listen = () => { - const [songs, setSongs] = useState>(new Set()); // Keep songIdSet as a Set - const [songDetails, setSongDetails] = useState([]); // Store detailed song info + const [songs, setSongs] = useState>(new Set()); + const [songDetails, setSongDetails] = useState([]); const [filteredSongs, setFilteredSongs] = useState([]); - const [searchQuery, setSearchQuery] = useState(""); // Search input + const [searchQuery, setSearchQuery] = useState(""); const [currentPlayingId, setCurrentPlayingId] = useState(null); + const [itemsToShow, setItemsToShow] = useState(10); // Items initially shown + const loadMoreRef = useRef(null); // Ref to track when user reaches the bottom + const songRefs = useRef<{ [key: number]: HTMLDivElement | null }>({}); // Refs to track each song element const { data: uploadedSongEvents, isLoading: uploadedSongsIsLoading } = useScaffoldEventHistory({ contractName: "SoundScaffold", @@ -35,7 +38,6 @@ const Listen = () => { const songId = Number(e.args.songId); songIdSet.add(songId); - // Assuming e.args has artistName, songTitle, genre if (e.args.artist && e.args.title && e.args.genre) songDetailsArray.push({ songId, @@ -48,20 +50,20 @@ const Listen = () => { setSongs(songIdSet); setSongDetails(songDetailsArray); - setFilteredSongs(songDetailsArray); // Initially show all songs + setFilteredSongs(songDetailsArray); } }, [uploadedSongEvents, uploadedSongsIsLoading]); const handlePlay = (songId: number) => { if (currentPlayingId !== null && currentPlayingId !== songId) { - setCurrentPlayingId(null); // Stop the current song + setCurrentPlayingId(null); } - setCurrentPlayingId(songId); // Set the new song to play + setCurrentPlayingId(songId); }; const handleSearchClick = () => { if (!searchQuery) { - setFilteredSongs(songDetails); // Show all songs if no search query + setFilteredSongs(songDetails); setSearchQuery(""); } else { const lowercasedSearch = searchQuery.toLowerCase(); @@ -74,33 +76,95 @@ const Listen = () => { setFilteredSongs(filtered); setSearchQuery(""); } + setItemsToShow(10); // Reset items to show when searching + }; + + // Infinite scroll logic using IntersectionObserver + const loadMore = useCallback(() => { + setItemsToShow(prevItems => prevItems + 10); // Load 10 more items + }, []); + + useEffect(() => { + if (loadMoreRef.current) { + const observer = new IntersectionObserver( + entries => { + if (entries[0].isIntersecting) { + loadMore(); + } + }, + { threshold: 1.0 }, + ); + + observer.observe(loadMoreRef.current); + + return () => { + if (loadMoreRef.current) { + observer.unobserve(loadMoreRef.current); + } + }; + } + }, [loadMoreRef, loadMore]); + + const handleRandomSong = () => { + if (filteredSongs.length === 0) return; + + const randomIndex = Math.floor(Math.random() * filteredSongs.length); + const randomSong = filteredSongs[randomIndex]; + + if (randomIndex >= itemsToShow) { + setItemsToShow(randomIndex + 1); + } + + setTimeout(() => { + const songElement = songRefs.current[randomSong.songId]; + if (songElement) { + songElement.scrollIntoView({ behavior: "smooth", block: "center" }); + } + + handlePlay(randomSong.songId); + }, 300); }; return (
- {/* Search widget in the top right corner */} -
- setSearchQuery(e.target.value)} // Only set the input value here - placeholder="Search..." - className="p-2 pl-4 w-64 text-black rounded-full focus:outline-none focus:ring-2 focus:ring-blue-500" - /> + {/* Right-side fixed container */} +
+ {/* Search widget */} +
+ setSearchQuery(e.target.value)} + placeholder="Search..." + className="p-2 pl-4 w-64 text-black rounded-full focus:outline-none focus:ring-2 focus:ring-blue-500" + /> + +
+ + {/* Random song button */}
{/* Song List */} song.songId)} + songs={filteredSongs.slice(0, itemsToShow).map(song => song.songId)} onPlay={handlePlay} currentPlayingId={currentPlayingId} + songRefs={songRefs} /> + + {/* Invisible div to track the end of the list */} +
); }; diff --git a/packages/nextjs/app/mymusic/_components/SongList.tsx b/packages/nextjs/app/mymusic/_components/SongList.tsx index 47c8941..dbb4d73 100644 --- a/packages/nextjs/app/mymusic/_components/SongList.tsx +++ b/packages/nextjs/app/mymusic/_components/SongList.tsx @@ -1,7 +1,5 @@ -import { useEffect, useState } from "react"; -// *********************************************************** -// MAKE SURE ABI IS AVAILABLE IN NEXT PACKAGE BEFORE DEPLOYING -// *********************************************************** +import { useEffect, useRef, useState } from "react"; +import { MutableRefObject } from "react"; import abi from "../../../../hardhat/artifacts/contracts/SoundScaffold.sol/SoundScaffold.json"; import Song from "../../listen/_components/Song"; import { readContract } from "@wagmi/core"; @@ -9,16 +7,14 @@ import { config } from "~~/wagmiConfig"; interface SongListProps { songs: number[]; - onPlay: (songId: number) => void; // Function to handle play event - currentPlayingId: number | null; // The currently playing song ID + onPlay: (songId: number) => void; + currentPlayingId: number | null; + scrollToSongId?: number | null; + songRefs?: MutableRefObject<{ [key: number]: HTMLDivElement | null }>; } -// ************************************************ -// ***** MAKE SURE TO ADJUST CONTRACT ADDRESS ***** -// ************************************************ const contractAddress = "0xe7f1725E7734CE288F8367e1Bb143E90bb3F0512"; -// use wagmi to get around react hook error (different amount of rerenders) const fetchTokenURI = async (songId: bigint) => { try { const songUrl = await readContract(config, { @@ -39,8 +35,9 @@ const fetchTokenURI = async (songId: bigint) => { } }; -const SongList: React.FC = ({ songs, onPlay, currentPlayingId }) => { +const SongList: React.FC = ({ songs, onPlay, currentPlayingId, scrollToSongId }) => { const [songMetadata, setSongMetadata] = useState<{ [key: number]: { fileUrl?: string } | null }>({}); + const songRefs = useRef<{ [key: number]: HTMLDivElement | null }>({}); // Refs for each song div useEffect(() => { const fetchAllMetadata = async () => { @@ -51,7 +48,6 @@ const SongList: React.FC = ({ songs, onPlay, currentPlayingId }) const metadataResults = await Promise.all(metadataPromises); - // Update state with fetched metadata const metadataMap: { [key: number]: { fileUrl?: string } | null } = {}; metadataResults.forEach(({ songId, metadata }) => { metadataMap[songId] = metadata; @@ -65,6 +61,14 @@ const SongList: React.FC = ({ songs, onPlay, currentPlayingId }) } }, [songs]); + // Effect to scroll to the song when scrollToSongId changes + useEffect(() => { + if (scrollToSongId && songRefs.current[scrollToSongId]) { + songRefs.current[scrollToSongId]?.scrollIntoView({ behavior: "smooth", block: "center" }); + onPlay(scrollToSongId); // Automatically play the song when scrolled to + } + }, [scrollToSongId, onPlay]); + return (
{songs.length > 0 ? ( @@ -72,14 +76,17 @@ const SongList: React.FC = ({ songs, onPlay, currentPlayingId }) const metadata = songMetadata[songId]; return ( -
+
(songRefs.current[songId] = el)} // Assign each song to a ref + > {metadata?.fileUrl ? ( ) : (

Loading song {songId}...

diff --git a/packages/nextjs/app/page.tsx b/packages/nextjs/app/page.tsx index decf04b..d37648d 100644 --- a/packages/nextjs/app/page.tsx +++ b/packages/nextjs/app/page.tsx @@ -5,8 +5,7 @@ import Link from "next/link"; import type { NextPage } from "next"; import { useAccount } from "wagmi"; import { Address } from "~~/components/scaffold-eth"; -import PatronizeArtist from "~~/components/scaffold-eth/PatronizeArtist"; -import SoundContribution from "~~/components/scaffold-eth/SoundContribution"; +import { Avatar } from "~~/components/scaffold-eth/Avatar"; import { useScaffoldReadContract, useScaffoldWriteContract } from "~~/hooks/scaffold-eth"; import { useGlobalState } from "~~/services/store/store"; import { notification } from "~~/utils/scaffold-eth"; @@ -16,15 +15,6 @@ const Home: NextPage = () => { const { writeContractAsync: writeSoundScaffoldAsync } = useScaffoldWriteContract("SoundScaffold"); const [artistName, setArtistName] = useState(""); - const [isModalOpen, setIsModalOpen] = useState(false); - - const openModal = () => { - setIsModalOpen(true); - }; - - const closeModal = () => { - setIsModalOpen(false); - }; const { data: isRegistered, error: isRegisteredError } = useScaffoldReadContract({ contractName: "SoundScaffold", @@ -43,12 +33,11 @@ const Home: NextPage = () => { useEffect(() => { const fetchData = async () => { if (isRegistered && isFetched) { - // Wait for the async function to resolve - setArtistName(userArtistName || ""); // Ensure you pass a string, even if the value is undefined + setArtistName(userArtistName || ""); } }; globalSetIsRegistered(true); - fetchData(); // Call the async function + fetchData(); }, [isRegistered, isFetched]); const handleRegister = async () => { @@ -69,28 +58,40 @@ const Home: NextPage = () => { }); }; + const handleOwner = async () => { + await writeSoundScaffoldAsync({ + functionName: "withdraw", + }); + }; + if (!isRegisteredError) { return ( <> -
-

- Welcome to SoundScaffold! This is the just the beginning. I want to bring music to web3 in a much more - meaningful way. +
+

+ This is the just the beginning. We want to bring music to web3 in a much + more meaningful way.

+ {isRegistered ? (

Welcome {artistName}

-
+
+ +
+
- - Upload Music - +
+ + Upload Music + +
) : ( @@ -115,6 +116,13 @@ const Home: NextPage = () => {

)} +
+

+ Sound Scaffold is free to use aside from gas fees. Feel free to contribute to your favorite tracks and the + project itself! +

+
+ {connectedAddress === process.env.NEXT_PUBLIC_OWNER && } ); } else { diff --git a/packages/nextjs/app/upload/page.tsx b/packages/nextjs/app/upload/page.tsx index 9fcbcf2..26922f4 100644 --- a/packages/nextjs/app/upload/page.tsx +++ b/packages/nextjs/app/upload/page.tsx @@ -151,61 +151,69 @@ const UploadMusic: React.FC = () => { }; return ( -
-
-

Upload Music

- - setName(e.target.value)} - className="w-full p-3 mb-4 text-black border border-gray-400 rounded-lg focus:outline-none focus:ring focus:border-blue-500" - /> - - - {genrePresets.length > 0 && ( -
    - {genrePresets.map(preset => ( -
  • { - e.stopPropagation(); - setGenre(preset); - setGenrePresets([]); - }} - > - {preset} -
  • - ))} -
- )} - - - - + <> +
+

+ Our copyright check is currently very strict so we apologize if it incorrectly flags your song. Please reach + out to us at SoundScaffold@gmail.com and supply your track for human review. +

-
+
+
+

Upload Music

+ + setName(e.target.value)} + className="w-full p-3 mb-4 text-black border border-gray-400 rounded-lg focus:outline-none focus:ring focus:border-blue-500" + /> + + + {genrePresets.length > 0 && ( +
    + {genrePresets.map(preset => ( +
  • { + e.stopPropagation(); + setGenre(preset); + setGenrePresets([]); + }} + > + {preset} +
  • + ))} +
+ )} + + + + +
+
+ ); }; diff --git a/packages/nextjs/components/Header.tsx b/packages/nextjs/components/Header.tsx index ad49f5a..c10ab4a 100644 --- a/packages/nextjs/components/Header.tsx +++ b/packages/nextjs/components/Header.tsx @@ -75,7 +75,6 @@ export const HeaderMenuLinks = () => { } if (isRegistered === null) { - // Optionally, show a loading spinner or skeleton while state is resolving return null; } @@ -143,7 +142,7 @@ export const Header = () => { SE2 logo
- Sound-Scaffold + Sound Scaffold
diff --git a/packages/nextjs/components/ScaffoldEthAppWithProviders.tsx b/packages/nextjs/components/ScaffoldEthAppWithProviders.tsx index f67c2f7..d188d70 100644 --- a/packages/nextjs/components/ScaffoldEthAppWithProviders.tsx +++ b/packages/nextjs/components/ScaffoldEthAppWithProviders.tsx @@ -1,12 +1,12 @@ "use client"; import { useEffect, useState } from "react"; +import { Footer } from "./Footer"; import { RainbowKitProvider, darkTheme, lightTheme } from "@rainbow-me/rainbowkit"; import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; import { useTheme } from "next-themes"; import { Toaster } from "react-hot-toast"; import { WagmiProvider } from "wagmi"; -import { Footer } from "~~/components/Footer"; import { Header } from "~~/components/Header"; import { BlockieAvatar } from "~~/components/scaffold-eth"; import { ProgressBar } from "~~/components/scaffold-eth/ProgressBar";