diff --git a/package.json b/package.json index bf30a2d9..6b34bd41 100644 --- a/package.json +++ b/package.json @@ -45,7 +45,6 @@ "@heroicons/react": "^2.0.18", "@hookform/resolvers": "^3.3.4", "@internationalized/date": "^3.5.4", - "@lifi/widget": "^3.4.4", "@mod-protocol/core": "^0.2.1", "@mod-protocol/mod-registry": "^0.2.2", "@mod-protocol/react": "^0.2.0", @@ -187,4 +186,4 @@ "ramda": "^0.29.1", "supabase": "^1.123.0" } -} +} \ No newline at end of file diff --git a/src/common/components/molecules/AlchemyChainSelector.tsx b/src/common/components/molecules/AlchemyChainSelector.tsx index 343d7af9..1f23e848 100644 --- a/src/common/components/molecules/AlchemyChainSelector.tsx +++ b/src/common/components/molecules/AlchemyChainSelector.tsx @@ -9,16 +9,20 @@ import { SelectValue, } from "@/common/components/atoms/select"; -export const CHAIN_OPTIONS: { id: AlchemyNetwork; name: string }[] = [ - { id: "eth", name: "Ethereum" }, - { id: "polygon", name: "Polygon" }, - { id: "opt", name: "Optimism" }, - { id: "arb", name: "Arbitrum" }, - { id: "base", name: "Base" }, - { id: "starknet", name: "StarkNet" }, - // { id: "astar", name: "Astar" }, - // { id: "frax", name: "Frax" }, - // { id: "zora", name: "Zora" }, +export const CHAIN_OPTIONS: { + id: AlchemyNetwork; + name: string; + scanUrl: string; +}[] = [ + { id: "eth", name: "Ethereum", scanUrl: "https://etherscan.io" }, + { id: "polygon", name: "Polygon", scanUrl: "https://polygonscan.com" }, + { id: "opt", name: "Optimism", scanUrl: "https://optimistic.etherscan.io" }, + { id: "arb", name: "Arbitrum", scanUrl: "https://arbiscan.io" }, + { id: "base", name: "Base", scanUrl: "https://basescan.org" }, + { id: "starknet", name: "StarkNet", scanUrl: "https://voyager.online" }, + // { id: "astar", name: "Astar", scanUrl: "https://astar.subscan.io" }, + // { id: "frax", name: "Frax", scanUrl: "https://fraxscan.com" }, + // { id: "zora", name: "Zora", scanUrl: "https://zorascan.io" }, ]; export interface AlchemyChainSelectorProps { diff --git a/src/common/components/molecules/AlchemyVideoNFTSelector.tsx b/src/common/components/molecules/AlchemyVideoNFTSelector.tsx new file mode 100644 index 00000000..73eb1112 --- /dev/null +++ b/src/common/components/molecules/AlchemyVideoNFTSelector.tsx @@ -0,0 +1,322 @@ +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/common/components/atoms/select"; +import { useLoadFarcasterUser } from "@/common/data/queries/farcaster"; +import { useNeynarUser } from "@/common/lib/hooks/useNeynarUser"; +import { formatEthereumAddress } from "@/common/lib/utils/ethereum"; +import { useFarcasterSigner } from "@/fidgets/farcaster"; +import { AlchemyNetwork, getAlchemyChainUrlV3 } from "@/fidgets/ui/gallery"; +import { first } from "lodash"; +import React, { useEffect, useMemo, useState } from "react"; +import { CHAIN_OPTIONS } from "./AlchemyChainSelector"; +import { zeroAddress } from "viem"; +import Image from "next/image"; + +export interface AlchemyVideoNftSelectorValue { + chain?: AlchemyNetwork; + walletAddress?: string; + selectedImage?: number; + imageUrl?: string; +} + +export interface AlchemyVideoNftSelectorProps { + onChange: (value: AlchemyVideoNftSelectorValue) => void; + value: AlchemyVideoNftSelectorValue; + className?: string; +} + +export function formatIpfsUrl(url?: string) { + if (!url || !url.startsWith("ipfs://")) return url || ""; + const token = process.env.NEXT_PUBLIC_IPFS_TOKEN; + const isDev = process.env.NODE_ENV === "development"; + const baseUrl = `https://gateway.pinata.cloud/ipfs/${url.split("://")[1]}`; + return isDev && token ? `${baseUrl}?pinataGatewayToken=${token}` : baseUrl; +} + +export function formatArweaveUrl(url?: string) { + if (!url) return url; + return `https://arweave.net/${url.split("://")[1]}`; +} + +function formatNftUrl(nft: any, chain: AlchemyNetwork = "eth") { + let baseUrl = + nft.raw?.metadata?.content?.uri || nft.raw?.metadata?.animation_url; + + if (!baseUrl) { + return null; + } + + if (baseUrl.startsWith("ipfs://")) { + baseUrl = formatIpfsUrl(baseUrl); + } else if (baseUrl.startsWith("ar://")) { + baseUrl = formatArweaveUrl(baseUrl); + } + + const contractName = nft?.name || ""; + const contractAddress = nft.contract?.address || ""; + const thumbnailUrl = formatIpfsUrl( + nft.image?.thumbnailUrl || nft?.raw?.metadata?.image || "", + ); + + const url = new URL(baseUrl); + + url.searchParams.set("contractName", contractName); + url.searchParams.set("contractAddress", contractAddress); + url.searchParams.set("thumbnailUrl", thumbnailUrl); + url.searchParams.set("chain", chain); + + return url.toString(); +} + +export const AlchemyVideoNftSelector: React.FC< + AlchemyVideoNftSelectorProps +> = ({ onChange, value, className }) => { + const settings = CHAIN_OPTIONS; + + const farcasterSigner = useFarcasterSigner("gallery"); + const fid = farcasterSigner.fid; + const { data } = useLoadFarcasterUser(fid); + const user = useMemo(() => first(data?.users), [data]); + const username = useMemo(() => user?.username, [user]); + + const { + user: neynarUser, + error: neynarError, + isLoading: isLoadingAddresses, + } = useNeynarUser(username); + const verifiedAddresses = useMemo( + () => neynarUser?.verifications || [], + [neynarUser], + ); + + // Initialize local state with values from props + const [selectedImage, setSelectedImage] = useState( + value.selectedImage, + ); + const [walletAddress, setWalletAddress] = useState( + value.walletAddress, + ); + const [selectedChain, setSelectedChain] = useState< + AlchemyNetwork | undefined + >(value.chain); + + const [nftImages, setNftImages] = useState([]); + const [error, setError] = useState(null); + const [isLoadingNFTs, setIsLoadingNFTs] = useState(false); + + useEffect(() => { + const abortController = new AbortController(); + + const fetchNFTs = async () => { + if (selectedChain && walletAddress) { + setIsLoadingNFTs(true); + const base_url = getAlchemyChainUrlV3(selectedChain); + const url = `${base_url}/getNFTsForOwner?owner=${walletAddress}&withMetadata=true&excludeFilters[]=AIRDROPS&pageSize=100`; + const options = { + method: "GET", + headers: { accept: "application/json" }, + signal: abortController.signal, + }; + + try { + const response = await fetch(url, options); + if (!response.ok) { + throw new Error(`Error fetching NFTs: ${response.statusText}`); + } + const data = await response.json(); + if (data.error) { + throw new Error(data.error.message); + } else if (data.ownedNfts.length === 0) { + throw new Error("No NFTs found for this address"); + } else { + setError(null); + } + + const videoNfts = data.ownedNfts.filter( + (nft: any) => + nft.raw?.metadata?.mimeType === "audio/wave" || + nft.raw?.metadata?.content?.mime === "video/mp4", + ); + + const images = videoNfts + .map((nft: any) => { + return formatNftUrl(nft, selectedChain); + }) + .filter((url: string | null) => url !== null); + + setNftImages(images); + setError(null); + } catch (err: any) { + if (!abortController.signal.aborted) { + setError(err.message); + } + } finally { + setIsLoadingNFTs(false); + } + } + }; + + fetchNFTs(); + + return () => { + abortController.abort(); + }; + }, [selectedChain, walletAddress]); + + // Show neynar error if present + useEffect(() => { + if (neynarError) { + setError(neynarError); + } + }, [neynarError]); + + // useEffect(() => { + // setSelectedImage(value.selectedImage); + // }, [value.selectedImage]); + + // useEffect(() => { + // setWalletAddress(value.walletAddress); + // }, [value.walletAddress]); + + // useEffect(() => { + // setSelectedChain(value.chain); + // }, [value.chain]); + + return ( +
+
+ Wallet Address + +
+ {walletAddress && ( + <> +
+ Network + +
+ {selectedChain && ( +
+ NFT +
+ {isLoadingNFTs ? ( +
Loading NFTs...
+ ) : error ? ( +
+ {error} +
+ ) : nftImages.length === 0 ? ( +
+ No video or audio NFTs found +
+ ) : ( + nftImages.map((image, index) => { + const thumbnailUrl = + new URL(image).searchParams.get("thumbnailUrl") || image; + return ( +
{ + setSelectedImage(index); + onChange({ + chain: selectedChain, + walletAddress: walletAddress, + selectedImage: index, + imageUrl: image, + }); + }} + > + NFT Thumbnail { + e.currentTarget.style.display = "none"; + }} + /> + +
+ ); + }) + )} +
+
+ )} + + )} +
+ ); +}; + +export default AlchemyVideoNftSelector; diff --git a/src/common/components/molecules/ChainSelector.tsx b/src/common/components/molecules/ChainSelector.tsx index 93d5d7cc..9e7538e4 100644 --- a/src/common/components/molecules/ChainSelector.tsx +++ b/src/common/components/molecules/ChainSelector.tsx @@ -1,5 +1,4 @@ import React from "react"; - import { Select, SelectContent, @@ -7,11 +6,75 @@ import { SelectTrigger, SelectValue, } from "@/common/components/atoms/select"; -import { CHAIN_OPTIONS } from "@/fidgets/swap/utils/chains"; + +const CHAIN_OPTIONS = [ + { + id: "ETH", + name: "Ethereum", + logo: "https://raw.githubusercontent.com/rango-exchange/assets/main/blockchains/ETH/icon.svg", + }, + { + id: "BSC", + name: "Binance Smart Chain", + logo: "https://raw.githubusercontent.com/rango-exchange/assets/main/blockchains/BSC/icon.svg", + }, + { + id: "ARBITRUM", + name: "Arbitrum", + logo: "https://raw.githubusercontent.com/rango-exchange/assets/main/blockchains/ARBITRUM/icon.svg", + }, + { + id: "POLYGON", + name: "Polygon", + logo: "https://raw.githubusercontent.com/rango-exchange/assets/main/blockchains/POLYGON/icon.svg", + }, + { + id: "AVAX", + name: "Avalanche", + logo: "https://raw.githubusercontent.com/rango-exchange/assets/main/blockchains/AVAX_CCHAIN/icon.svg", + }, + { + id: "OPTIMISM", + name: "Optimism", + logo: "https://raw.githubusercontent.com/rango-exchange/assets/main/blockchains/OPTIMISM/icon.svg", + }, + // Blast + { + id: "BLAST", + name: "Blast", + logo: "https://raw.githubusercontent.com/rango-exchange/assets/main/blockchains/BLAST/icon.svg", + }, + { + id: "LINEA", + name: "Linea", + logo: "https://raw.githubusercontent.com/rango-exchange/assets/main/blockchains/LINEA/icon.svg", + }, + // { + // id: "BTC", + // name: "Bitcoin", + // logo: "https://raw.githubusercontent.com/rango-exchange/assets/main/blockchains/BTC/icon.svg", + // }, + // { + // id: "LTC", + // name: "Litecoin", + // logo: "https://raw.githubusercontent.com/rango-exchange/assets/main/blockchains/LTC/icon.svg", + // }, + // { + // id: "DOGE", + // name: "Dogecoin", + // logo: "https://raw.githubusercontent.com/rango-exchange/assets/main/blockchains/DOGE/icon.svg", + // }, + // add BASE + { + id: "BASE", + name: "Base", + logo: "https://raw.githubusercontent.com/rango-exchange/assets/main/blockchains/BASE/icon.svg", + }, +]; export interface ChainSelectorProps { - onChange: (chainId: number) => void; - value: number | null; + onChange: (chainName: string) => void; + value: string | null; className?: string; } @@ -20,32 +83,53 @@ export const ChainSelector: React.FC = ({ value, className, }) => { - const settings = CHAIN_OPTIONS; + // Find the selected chain name or fallback to "Select a chain" + const selectedChain = CHAIN_OPTIONS.find((chain) => chain.id === value); + const selectedChainName = selectedChain?.name || "Select a chain"; + const selectedChainLogo = selectedChain?.logo; return ( { - const selectedTheme = THEME_OPTIONS_BY_NAME[selectedName]; - if (selectedTheme) { - onChange(selectedName === "Custom" ? "Custom" : selectedTheme.config); - } - }} - > - - - {value === "Custom" - ? "Custom" - : getSettingByValue(settings, value)?.name || "Select a theme"} - - - - {settings.map((theme, i) => ( - - {theme.name} - - ))} - - - ); -}; - -export default ThemeSelector; diff --git a/src/common/components/molecules/VideoSelector.tsx b/src/common/components/molecules/VideoSelector.tsx new file mode 100644 index 00000000..567a2471 --- /dev/null +++ b/src/common/components/molecules/VideoSelector.tsx @@ -0,0 +1,78 @@ +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/common/components/atoms/select"; +import { + analytics, + AnalyticsEvent, +} from "@/common/providers/AnalyticsProvider"; +import React, { useState } from "react"; +import AlchemyVideoNftSelector, { + AlchemyVideoNftSelectorValue, +} from "./AlchemyVideoNFTSelector"; +import { YouTubeSelector } from "./YouTubeSelector"; +import Player from "../organisms/Player"; + +type VideoSource = "youtube" | "wallet"; + +export interface VideoSelectorProps { + initialVideoURL: string | null; + onVideoSelect: (url: string) => void; +} + +export function VideoSelector({ + initialVideoURL, + onVideoSelect, +}: VideoSelectorProps) { + const [selectedVideo, setSelectedVideo] = useState( + initialVideoURL, + ); + const [videoSource, setVideoSource] = useState(); + + function handleVideoSelect(videoUrl: string) { + setSelectedVideo(videoUrl); + onVideoSelect(videoUrl); + analytics.track(AnalyticsEvent.MUSIC_UPDATED, { url: videoUrl }); + } + + function handleNftSelect(value: AlchemyVideoNftSelectorValue) { + console.log("NFT selected", value); + if (value.imageUrl) handleVideoSelect(value.imageUrl); + } + + return ( +
+ + + {videoSource === "youtube" && ( + + )} + {videoSource === "wallet" && ( +
+ +
+ )} + + {selectedVideo && ( +
+
Selected Song:
+ +
+ )} +
+ ); +} diff --git a/src/common/components/molecules/YouTubeSelector.tsx b/src/common/components/molecules/YouTubeSelector.tsx new file mode 100644 index 00000000..171ef159 --- /dev/null +++ b/src/common/components/molecules/YouTubeSelector.tsx @@ -0,0 +1,79 @@ +import React, { useState, ChangeEvent } from "react"; + +interface VideoResult { + id: { + videoId: string; + }; + snippet: { + title: string; + thumbnails: { + default: { + url: string; + }; + }; + }; +} + +interface YouTubeSelectorProps { + onVideoSelect: (videoUrl: string) => void; +} + +export function YouTubeSelector({ onVideoSelect }: YouTubeSelectorProps) { + const [searchQuery, setSearchQuery] = useState(""); + const [searchResults, setSearchResults] = useState([]); + + async function searchYouTube(query: string) { + try { + const response = await fetch( + `/api/youtube-search?query=${encodeURIComponent(query)}`, + ); + const data = await response.json(); + setSearchResults(data || []); + } catch (error) { + console.error("Error fetching YouTube search results:", error); + } + } + + function handleSearchChange(event: ChangeEvent) { + const query = event.target.value; + setSearchQuery(query); + if (query.length > 2) searchYouTube(query); + } + + function handleVideoSelect(videoId: string) { + const videoUrl = `https://www.youtube.com/embed/${videoId}`; + onVideoSelect(videoUrl); + setSearchResults([]); + setSearchQuery(""); + } + + return ( +
+ +
    + {searchResults.map((result) => ( +
  • handleVideoSelect(result.id.videoId)} + className="cursor-pointer hover:bg-gray-200 p-2 rounded text-xs" + > +
    + {result.snippet.title} + {result.snippet.title} +
    +
  • + ))} +
+
+ ); +} diff --git a/src/common/components/organisms/LoadingSidebar.tsx b/src/common/components/organisms/LoadingSidebar.tsx index c2294d83..5841858f 100644 --- a/src/common/components/organisms/LoadingSidebar.tsx +++ b/src/common/components/organisms/LoadingSidebar.tsx @@ -5,12 +5,11 @@ export default function LoadingSidebar() { return ( ); } diff --git a/src/common/components/organisms/Navigation.tsx b/src/common/components/organisms/Navigation.tsx index 25134ad5..c56692e3 100644 --- a/src/common/components/organisms/Navigation.tsx +++ b/src/common/components/organisms/Navigation.tsx @@ -62,6 +62,8 @@ const Navigation: React.FC = ({ isEditable, enterEditMode }) => { }), ); const userTheme: UserTheme = useUserTheme(); + console.log("Navigation.tsx: userTheme", userTheme); + const logout = useLogout(); const notificationBadgeText = useNotificationBadgeText(); const pathname = usePathname(); @@ -73,7 +75,6 @@ const Navigation: React.FC = ({ isEditable, enterEditMode }) => { logout(); } - function turnOnEditMode() { enterEditMode(); } diff --git a/src/common/components/organisms/Player.tsx b/src/common/components/organisms/Player.tsx index cff6ef61..33a0d9ee 100644 --- a/src/common/components/organisms/Player.tsx +++ b/src/common/components/organisms/Player.tsx @@ -1,5 +1,12 @@ -import React, { useState, useEffect, useCallback, useRef } from "react"; -import ReactPlayer, { YouTubeConfig } from "react-player/youtube"; +import React, { + useState, + useEffect, + useCallback, + useRef, + ReactElement, +} from "react"; +import { YouTubeConfig } from "react-player/youtube"; +import ReactPlayer from "react-player"; import Image from "next/image"; import useHasWindow from "@/common/lib/hooks/useHasWindow"; import { IconType } from "react-icons"; @@ -10,12 +17,17 @@ import { } from "react-icons/lia"; import { Button } from "@/common/components/atoms/button"; import { trackAnalyticsEvent } from "@/common/lib/utils/analyticsUtils"; +import { AnalyticsEvent } from "@/common/providers/AnalyticsProvider"; +import { formatEthereumAddress } from "@/common/lib/utils/ethereum"; +import { Address, isAddress, zeroAddress } from "viem"; +import ScanAddress from "../molecules/ScanAddress"; +import { AlchemyNetwork } from "@/fidgets/ui/gallery"; + type ContentMetadata = { title?: string | null; - channel?: string | null; + channel?: string | null | ReactElement; thumbnail?: string | null; }; -import { AnalyticsEvent } from "@/common/providers/AnalyticsProvider"; export type PlayerProps = { url: string | string[]; }; @@ -44,8 +56,30 @@ export const Player: React.FC = ({ url }) => { ready, }); - const getYouTubeMetadata = async (_url) => { - const response = await fetch(`/api/metadata/youtube?url=${_url}`); + const getMetadata = async (_url: string | string[]) => { + // Handle array of URLs by taking the first one + const videoUrl = Array.isArray(_url) ? _url[0] : _url; + + if (videoUrl.includes("ipfs") || videoUrl.includes("arweave")) { + // Parse URL parameters for IPFS content + const url = new URL(videoUrl); + const contractName = url.searchParams.get("contractName"); + const contractAddress = url.searchParams.get( + "contractAddress", + ) as Address; + const thumbnailUrl = url.searchParams.get("thumbnailUrl"); + const chain = url.searchParams.get("chain") as AlchemyNetwork; + + setMetadata({ + title: contractName || "NFT", + channel: , + thumbnail: thumbnailUrl || null, + }); + return; + } + + // Default to YouTube metadata + const response = await fetch(`/api/metadata/youtube?url=${videoUrl}`); const data = await response.json(); const snippet = data?.value?.snippet; @@ -59,7 +93,7 @@ export const Player: React.FC = ({ url }) => { }; useEffect(() => { - getYouTubeMetadata(url); + getMetadata(url); }, [url]); useEffect(() => { @@ -142,7 +176,9 @@ export const Player: React.FC = ({ url }) => { light={false} controls={false} muted={muted} - config={youtubeConfig} + config={{ + youtube: youtubeConfig, + }} onReady={onReady} onStart={onStart} onPause={onPause} diff --git a/src/common/components/organisms/Sidebar.tsx b/src/common/components/organisms/Sidebar.tsx index c1cc02f2..cb1e0f7a 100644 --- a/src/common/components/organisms/Sidebar.tsx +++ b/src/common/components/organisms/Sidebar.tsx @@ -4,9 +4,10 @@ import React, { useState, useRef, useMemo, + useEffect, } from "react"; import Navigation from "./Navigation"; - +import LoadingSidebar from "./LoadingSidebar"; export interface SidebarProps {} export type SidebarContextProviderProps = { children: React.ReactNode }; diff --git a/src/common/components/pages/SpacePage.tsx b/src/common/components/pages/SpacePage.tsx index 9b210f40..e3ccfe13 100644 --- a/src/common/components/pages/SpacePage.tsx +++ b/src/common/components/pages/SpacePage.tsx @@ -36,7 +36,7 @@ export default function SpacePage({ isUndefined(commitConfig) || isUndefined(resetConfig) || loading ? ( - + ) : ( + {inEditMode && }
-
-
-
- {[...Array(cols * maxRows)].map((_, i) => ( -
- ))} + {!isUndefined(profile) ? ( +
{profile}
+ ) : null} + {tabBar} + {inEditMode && ( +
+
+
+ {[...Array(cols * maxRows)].map((_, i) => ( +
+ ))} +
-
+ )}
diff --git a/src/common/data/stores/app/homebase/homebaseTabsStore.ts b/src/common/data/stores/app/homebase/homebaseTabsStore.ts index a5478ae3..a1000679 100644 --- a/src/common/data/stores/app/homebase/homebaseTabsStore.ts +++ b/src/common/data/stores/app/homebase/homebaseTabsStore.ts @@ -309,8 +309,9 @@ export const createHomeBaseTabStoreFunc = ( } }, commitHomebaseTabToDatabase: debounce(async (tabname) => { - const localCopy = cloneDeep(get().homebase.tabs[tabname].config); - if (localCopy) { + const tab = get().homebase.tabs[tabname]; + if (tab && tab.config) { + const localCopy = cloneDeep(tab.config); const file = await get().account.createEncryptedSignedFile( stringify(localCopy), "json", diff --git a/src/common/lib/hooks/useNeynarUser.ts b/src/common/lib/hooks/useNeynarUser.ts new file mode 100644 index 00000000..6bbe92d7 --- /dev/null +++ b/src/common/lib/hooks/useNeynarUser.ts @@ -0,0 +1,46 @@ +import { NeynarUser } from "@/pages/api/farcaster/neynar/user"; +import { useState, useEffect } from "react"; + +export const useNeynarUser = (username: string | undefined) => { + const [user, setUser] = useState(null); + const [error, setError] = useState(null); + const [isLoading, setLoading] = useState(false); + + useEffect(() => { + const abortController = new AbortController(); + + const fetchUser = async () => { + if (!username) return; + + setLoading(true); + try { + const response = await fetch( + `/api/farcaster/neynar/user?username=${username}`, + { signal: abortController.signal }, + ); + const data = await response.json(); + if (!data) { + setError("No user found for username " + username); + return; + } + + setUser(data.user); + setError(null); + } catch (err: any) { + if (!abortController.signal.aborted) { + setError("Error fetching user"); + } + } finally { + setLoading(false); + } + }; + + fetchUser(); + + return () => { + abortController.abort(); + }; + }, [username]); + + return { user, error, isLoading }; +}; diff --git a/src/common/lib/theme/ThemeSettingsEditor.tsx b/src/common/lib/theme/ThemeSettingsEditor.tsx index 73254484..3a150562 100644 --- a/src/common/lib/theme/ThemeSettingsEditor.tsx +++ b/src/common/lib/theme/ThemeSettingsEditor.tsx @@ -37,6 +37,7 @@ import { FONT_FAMILY_OPTIONS_BY_NAME } from "@/common/lib/theme/fonts"; import { GiOpenBook } from "react-icons/gi"; import { FaBook } from "react-icons/fa"; import { MdMenuBook } from "react-icons/md"; +import { VideoSelector } from "@/common/components/molecules/VideoSelector"; export type ThemeSettingsEditorArgs = { theme: ThemeSettings; @@ -52,12 +53,13 @@ export function ThemeSettingsEditor({ cancelExitEditMode, }: ThemeSettingsEditorArgs) { const [showConfirmCancel, setShowConfirmCancel] = useState(false); + const [activeTheme, setActiveTheme] = useState(theme.id); + const [searchQuery, setSearchQuery] = useState(""); const [searchResults, setSearchResults] = useState([]); const [selectedVideo, setSelectedVideo] = useState( theme.properties.musicURL, ); - const [activeTheme, setActiveTheme] = useState(theme.id); function handleSearchChange(event: ChangeEvent) { const query = event.target.value; @@ -336,47 +338,10 @@ export function ThemeSettingsEditor({

Music

- -
    - {searchResults.map((result: any) => ( -
  • { - handleVideoSelect(result.id.videoId); - setSearchResults([]); - }} - className="cursor-pointer hover:bg-gray-200 p-2 rounded text-xs" - > -
    - {result.snippet.title} - {result.snippet.title} -
    -
  • - ))} -
- {selectedVideo && ( -
-
Selected Song:
-