Skip to content

Commit

Permalink
add random song functionality
Browse files Browse the repository at this point in the history
  • Loading branch information
RafaelCaso committed Sep 30, 2024
1 parent 2cf9860 commit cbc302b
Show file tree
Hide file tree
Showing 7 changed files with 204 additions and 121 deletions.
3 changes: 0 additions & 3 deletions packages/hardhat/deploy/01_deploy_sound_scaffold.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<Contract>("SoundScaffold", deployer);
const soundScaffoldAddress = await soundScaffold.getAddress();
const transferOwnership = await soundScaffold.transferOwnership(ownerAddress);
Expand All @@ -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"];
108 changes: 86 additions & 22 deletions packages/nextjs/app/listen/page.tsx
Original file line number Diff line number Diff line change
@@ -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";

Expand All @@ -12,11 +12,14 @@ interface Song {
}

const Listen = () => {
const [songs, setSongs] = useState<Set<number>>(new Set()); // Keep songIdSet as a Set
const [songDetails, setSongDetails] = useState<Song[]>([]); // Store detailed song info
const [songs, setSongs] = useState<Set<number>>(new Set());
const [songDetails, setSongDetails] = useState<Song[]>([]);
const [filteredSongs, setFilteredSongs] = useState<Song[]>([]);
const [searchQuery, setSearchQuery] = useState<string>(""); // Search input
const [searchQuery, setSearchQuery] = useState<string>("");
const [currentPlayingId, setCurrentPlayingId] = useState<number | null>(null);
const [itemsToShow, setItemsToShow] = useState<number>(10); // Items initially shown
const loadMoreRef = useRef<HTMLDivElement | null>(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",
Expand All @@ -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,
Expand All @@ -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();
Expand All @@ -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 (
<div className="p-4 bg-gray-900 min-h-screen relative pl-24">
{/* Search widget in the top right corner */}
<div className="absolute top-4 right-4 flex items-center space-x-2">
<input
type="text"
value={searchQuery}
onChange={e => 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 */}
<div className="pt-20 fixed top-4 right-4 flex flex-col items-center space-y-4">
{/* Search widget */}
<div className="flex items-center space-x-2">
<input
type="text"
value={searchQuery}
onChange={e => 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"
/>
<button
onClick={handleSearchClick}
className="p-2 px-4 bg-blue-500 text-white rounded-full focus:outline-none hover:bg-blue-600 transition"
>
Search
</button>
</div>

{/* Random song button */}
<button
onClick={handleSearchClick}
className="p-2 px-4 bg-blue-500 text-white rounded-full focus:outline-none hover:bg-blue-600 transition"
onClick={handleRandomSong}
className="h-10 w-40 bg-green-500 text-white rounded-full focus:outline-none hover:bg-green-600 transition"
>
Search
Play Random Song
</button>
</div>

{/* Song List */}
<SongList
songs={filteredSongs.map(song => 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 */}
<div ref={loadMoreRef} className="h-4"></div>
</div>
);
};
Expand Down
35 changes: 21 additions & 14 deletions packages/nextjs/app/mymusic/_components/SongList.tsx
Original file line number Diff line number Diff line change
@@ -1,24 +1,20 @@
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";
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, {
Expand All @@ -39,8 +35,9 @@ const fetchTokenURI = async (songId: bigint) => {
}
};

const SongList: React.FC<SongListProps> = ({ songs, onPlay, currentPlayingId }) => {
const SongList: React.FC<SongListProps> = ({ 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 () => {
Expand All @@ -51,7 +48,6 @@ const SongList: React.FC<SongListProps> = ({ 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;
Expand All @@ -65,21 +61,32 @@ const SongList: React.FC<SongListProps> = ({ 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 (
<div className="w-full flex flex-col items-start">
{songs.length > 0 ? (
songs.map(songId => {
const metadata = songMetadata[songId];

return (
<div key={songId}>
<div
key={songId}
ref={el => (songRefs.current[songId] = el)} // Assign each song to a ref
>
{metadata?.fileUrl ? (
<Song
songCID={metadata.fileUrl}
metadataCID={JSON.stringify(metadata)}
songId={songId}
onPlay={onPlay}
songIsPlaying={currentPlayingId === songId} // Determine if the current song is playing
songIsPlaying={currentPlayingId === songId}
/>
) : (
<p>Loading song {songId}...</p>
Expand Down
58 changes: 33 additions & 25 deletions packages/nextjs/app/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand All @@ -16,15 +15,6 @@ const Home: NextPage = () => {
const { writeContractAsync: writeSoundScaffoldAsync } = useScaffoldWriteContract("SoundScaffold");

const [artistName, setArtistName] = useState<string>("");
const [isModalOpen, setIsModalOpen] = useState<boolean>(false);

const openModal = () => {
setIsModalOpen(true);
};

const closeModal = () => {
setIsModalOpen(false);
};

const { data: isRegistered, error: isRegisteredError } = useScaffoldReadContract({
contractName: "SoundScaffold",
Expand All @@ -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 () => {
Expand All @@ -69,28 +58,40 @@ const Home: NextPage = () => {
});
};

const handleOwner = async () => {
await writeSoundScaffoldAsync({
functionName: "withdraw",
});
};

if (!isRegisteredError) {
return (
<>
<div>
<h2>
Welcome to SoundScaffold! This is the just the beginning. I want to bring music to web3 in a much more
meaningful way.
<div className="pt-6 bg-gray-900 text-white flex items-center justify-center">
<h2 className="text-xl">
This is the just the beginning. We want to bring<span className="text-4xl"> music</span> to web3 in a much
more meaningful way.
</h2>
</div>

{isRegistered ? (
<div className="min-h-screen flex items-center justify-center bg-gray-900 text-white">
<div className="max-w-2xl mx-auto p-8 bg-gray-800 shadow-md rounded-lg">
<h2 className="text-3xl font-semibold mb-6 text-center">Welcome {artistName}</h2>
<div className="mx-auto p-6">
<div className="flex items-center justify-center">
<Avatar address={connectedAddress} size="10xl" />
</div>
<div className="mx-auto p-6 flex items-center justify-center">
<Address address={connectedAddress} />
</div>
<Link
className="w-full bg-orange-500 p-3 rounded-lg text-white hover:bg-orange-600 transition-colors"
href="/upload"
>
Upload Music
</Link>
<div className="flex items-center justify-center text-center">
<Link
className="w-full bg-orange-500 p-3 rounded-lg text-white hover:bg-orange-600 transition-colors"
href="/upload"
>
Upload Music
</Link>
</div>
</div>
</div>
) : (
Expand All @@ -115,6 +116,13 @@ const Home: NextPage = () => {
</div>
</div>
)}
<div className="p-0 min-h-10 bg-gray-900 text-white flex items-center justify-center text-center">
<h3>
Sound Scaffold is free to use aside from gas fees. Feel free to contribute to your favorite tracks and the
project itself!
</h3>
</div>
{connectedAddress === process.env.NEXT_PUBLIC_OWNER && <button onClick={handleOwner}>Withdraw</button>}
</>
);
} else {
Expand Down
Loading

0 comments on commit cbc302b

Please sign in to comment.