- {/* Beat art — still clickable through to the detail page even when sold exclusive */}
-
setIsCardHovered(true)}
+ onMouseLeave={() => setIsCardHovered(false)}
+ style={{
+ background: "#111",
+ border: "1px solid #1a1a1a",
+ borderRadius: 20,
+ overflow: "hidden",
+ transition: "border-color 0.15s, box-shadow 0.15s",
+ boxShadow: isPlaying ? "0 8px 30px rgba(250, 204, 21, 0.08)" : undefined,
+ borderColor: isPlaying ? "rgba(250, 204, 21, 0.3)" : undefined,
+ }}
+ >
+ {/* Beat art & Waveform area */}
+
+
+
+
+
+ {/* Play / Pause overlay button */}
+
+ {isPlaying ? (
+
+ ) : (
+
+ )}
+
{isExclusive && (
{resalePrice ? "RESALE" : "SOLD EXCLUSIVE"}
)}
-
+
@@ -78,8 +210,30 @@ export function SampleCard({ id, title, producer, genre, bpm, leasePrice, premiu
- {genre}
- {bpm} BPM
+
+ {genre}
+
+
+ {bpm} BPM
+
@@ -88,75 +242,111 @@ export function SampleCard({ id, title, producer, genre, bpm, leasePrice, premiu
onBuyResale?.(id)}
- style={{ width: "100%", background: "#3b82f6", color: "#fff", border: "none", borderRadius: 12, padding: "11px", fontSize: 14, fontWeight: 700, cursor: "pointer", transition: "all 0.15s" }}>
- Buy Resale — {resalePrice} {tokenSymbol}
-
- ) : (
- /* Sold exclusive — all purchase tiers collapse into a single disabled state */
-
setShowTooltip(true)}
- onMouseLeave={() => setShowTooltip(false)}
- >
-
-
- No longer available
-
-
- {showTooltip && (
+ Buy Resale — {resalePrice} {tokenSymbol}
+
+ ) : (
+ /* Sold exclusive — all purchase tiers collapse into a single disabled state */
+
setShowTooltip(true)}
+ onMouseLeave={() => setShowTooltip(false)}
+ >
- {EXCLUSIVE_TOOLTIP}
+
+ No longer available
- )}
-
+
+ {showTooltip && (
+
+ {EXCLUSIVE_TOOLTIP}
+
+ )}
+
)
) : (
<>
{tiers.map((t, i) => (
-
setSelected(i)}
- style={{ flex: 1, background: selected === i ? "rgba(250,204,21,0.12)" : "#0a0a0a", border: `1px solid ${selected === i ? "rgba(250,204,21,0.4)" : "#1a1a1a"}`, borderRadius: 10, padding: "8px 4px", cursor: "pointer", transition: "all 0.15s", textAlign: "center" }}>
- {t.label}
- {t.price} {tokenSymbol}
+ setSelected(i)}
+ style={{
+ flex: 1,
+ background: selected === i ? "rgba(250,204,21,0.12)" : "#0a0a0a",
+ border: `1px solid ${selected === i ? "rgba(250,204,21,0.4)" : "#1a1a1a"}`,
+ borderRadius: 10,
+ padding: "8px 4px",
+ cursor: "pointer",
+ transition: "all 0.15s",
+ textAlign: "center",
+ }}
+ >
+
+ {t.label}
+
+
+ {t.price} {tokenSymbol}
+
{t.desc}
))}
@@ -166,7 +356,19 @@ export function SampleCard({ id, title, producer, genre, bpm, leasePrice, premiu
aria-label={selected !== null ? `Buy ${tiers[selected].label} license for ${title}` : "Select a license tier"}
disabled={selected === null}
onClick={() => selected !== null && onBuy?.(id, selected)}
- style={{ width: "100%", background: selected !== null ? "#facc15" : "#1a1a1a", color: selected !== null ? "#000" : "#525252", border: "none", borderRadius: 12, padding: "11px", fontSize: 14, fontWeight: 700, cursor: selected !== null ? "pointer" : "default", transition: "all 0.15s" }}>
+ style={{
+ width: "100%",
+ background: selected !== null ? "#facc15" : "#1a1a1a",
+ color: selected !== null ? "#000" : "#525252",
+ border: "none",
+ borderRadius: 12,
+ padding: "11px",
+ fontSize: 14,
+ fontWeight: 700,
+ cursor: selected !== null ? "pointer" : "default",
+ transition: "all 0.15s",
+ }}
+ >
{selected !== null ? `Buy ${tiers[selected].label} — ${tiers[selected].price} ${tokenSymbol}` : "Select a license tier"}
>
@@ -175,3 +377,5 @@ export function SampleCard({ id, title, producer, genre, bpm, leasePrice, premiu
);
}
+
+export default SampleCard;
diff --git a/src/contracts/crate.ts b/src/contracts/crate.ts
index 20b45aa..fa35fe3 100644
--- a/src/contracts/crate.ts
+++ b/src/contracts/crate.ts
@@ -111,9 +111,8 @@ export async function getSample(sourceAddress: string, sampleId: bigint): Promis
}
export async function submitTransaction(signed: { signedTxXdr: string }): Promise
{
- const { StellarBase } = await import("@stellar/stellar-sdk");
- const tx = StellarBase.TransactionEnvelope.fromXDR(signed.signedTxXdr, "base64");
- const result = await server().sendTransaction(tx as Parameters["prototype"]["sendTransaction"][0]);
+ const tx = TransactionBuilder.fromXDR(signed.signedTxXdr, NETWORK_PASS);
+ const result = await server().sendTransaction(tx as any);
if (result.status === "ERROR") throw new Error("Transaction submission failed");
return result.hash;
}
@@ -141,16 +140,17 @@ export async function uploadSample(params: {
}
export async function purchaseSample(params: {
- buyer: string; sampleId: number; tokenAddress: string; tier?: number;
+ buyer: string; sampleId: number; tokenAddress?: string; tier?: number;
}): Promise {
const src = await server().getAccount(params.buyer);
const c = new Contract(CONTRACT_ID);
const tier = params.tier ?? 0;
+ const tokenAddress = params.tokenAddress || "CDLZFC3SYJYDZT7K67VZ75HPJVIEUVNIXF47ZG2FB2RMQQVU2HHGCYSC";
const tx = new TransactionBuilder(src, { fee: "1000000", networkPassphrase: NETWORK_PASS })
.addOperation(c.call("purchase_license",
new Address(params.buyer).toScVal(),
nativeToScVal(params.sampleId, { type: "u32" }),
- new Address(params.tokenAddress).toScVal(),
+ new Address(tokenAddress).toScVal(),
nativeToScVal(tier, { type: "u32" }),
)).setTimeout(300).build();
const prepared = await server().prepareTransaction(tx);
diff --git a/src/hooks/useAudioPreview.ts b/src/hooks/useAudioPreview.ts
new file mode 100644
index 0000000..23cbcc8
--- /dev/null
+++ b/src/hooks/useAudioPreview.ts
@@ -0,0 +1,621 @@
+import React, {
+ createContext,
+ useContext,
+ useState,
+ useRef,
+ useEffect,
+ useCallback,
+ useMemo,
+} from "react";
+
+export const MAX_PREVIEW_DURATION = 30; // Strictly 30-second cap for previews
+
+export interface PreviewTrack {
+ id: number | string;
+ title: string;
+ producer: string;
+ genre?: string;
+ bpm?: number;
+ audioUrl?: string;
+ price?: number;
+ leasePrice?: number;
+ premiumPrice?: number;
+ exclusivePrice?: number;
+ tokenSymbol?: string;
+ isExclusive?: boolean;
+ resalePrice?: number;
+ ipfs_cid?: string;
+ peaks?: number[];
+}
+
+export interface AudioPreviewContextType {
+ currentTrack: PreviewTrack | null;
+ isPlaying: boolean;
+ currentTime: number;
+ duration: number;
+ progress: number;
+ volume: number;
+ isMuted: boolean;
+ isLooping: boolean;
+ play: (track?: PreviewTrack) => void;
+ pause: () => void;
+ togglePlay: (track?: PreviewTrack) => void;
+ seek: (value: number, isRatio?: boolean) => void;
+ seekToRatio: (ratio: number) => void;
+ seekToTime: (seconds: number) => void;
+ skip: (seconds: number) => void;
+ setVolume: (vol: number) => void;
+ toggleMute: () => void;
+ setIsMuted: (muted: boolean) => void;
+ toggleLoop: () => void;
+ setIsLooping: (looping: boolean | ((prev: boolean) => boolean)) => void;
+ stop: () => void;
+ clearTrack: () => void;
+ formatTime: (seconds: number) => string;
+ timeRemaining: number;
+}
+
+export function formatTime(seconds: number): string {
+ if (isNaN(seconds) || seconds < 0) return "0:00";
+ const total = Math.floor(seconds);
+ const mins = Math.floor(total / 60);
+ const secs = total % 60;
+ return `${mins}:${secs.toString().padStart(2, "0")}`;
+}
+
+export function formatTimeRemaining(remainingSeconds: number): string {
+ if (isNaN(remainingSeconds) || remainingSeconds < 0) return "-0:00";
+ return `-${formatTime(remainingSeconds)}`;
+}
+
+export const AudioPreviewContext = createContext(null);
+
+export interface AudioPreviewProviderProps {
+ children: React.ReactNode;
+ initialTrack?: PreviewTrack | null;
+ enableKeyboardShortcuts?: boolean;
+}
+
+export function AudioPreviewProvider({
+ children,
+ initialTrack = null,
+ enableKeyboardShortcuts = true,
+}: AudioPreviewProviderProps) {
+ const [currentTrack, setCurrentTrack] = useState(initialTrack);
+ const [isPlaying, setIsPlaying] = useState(false);
+ const [currentTime, setCurrentTime] = useState(0);
+ const [volume, setVolumeState] = useState(0.8);
+ const [isMuted, setIsMuted] = useState(false);
+ const [isLooping, setIsLooping] = useState(false);
+
+ const isPlayingRef = useRef(isPlaying);
+ isPlayingRef.current = isPlaying;
+ const isLoopingRef = useRef(isLooping);
+ isLoopingRef.current = isLooping;
+ const previousVolumeRef = useRef(0.8);
+
+ const duration = MAX_PREVIEW_DURATION; // 30s preview limit
+ const audioRef = useRef(null);
+
+ // Sync volume with audio element
+ useEffect(() => {
+ if (audioRef.current) {
+ audioRef.current.volume = isMuted ? 0 : volume;
+ audioRef.current.muted = isMuted;
+ }
+ }, [volume, isMuted]);
+
+ // Audio element setup and cleanup
+ useEffect(() => {
+ if (currentTrack?.audioUrl) {
+ const audio = new Audio(currentTrack.audioUrl);
+ audio.volume = isMuted ? 0 : volume;
+ audio.muted = isMuted;
+ audioRef.current = audio;
+
+ const handleTimeUpdate = () => {
+ if (audio.currentTime >= MAX_PREVIEW_DURATION) {
+ if (isLoopingRef.current) {
+ audio.currentTime = 0;
+ audio.play().catch(() => {});
+ setCurrentTime(0);
+ } else {
+ audio.pause();
+ audio.currentTime = 0;
+ isPlayingRef.current = false;
+ setIsPlaying(false);
+ setCurrentTime(0);
+ }
+ } else {
+ setCurrentTime(Math.min(MAX_PREVIEW_DURATION, audio.currentTime));
+ }
+ };
+
+ const handleEnded = () => {
+ if (isLoopingRef.current) {
+ audio.currentTime = 0;
+ audio.play().catch(() => {});
+ setCurrentTime(0);
+ } else {
+ isPlayingRef.current = false;
+ setIsPlaying(false);
+ setCurrentTime(0);
+ }
+ };
+
+ audio.addEventListener("timeupdate", handleTimeUpdate);
+ audio.addEventListener("ended", handleEnded);
+
+ if (isPlaying) {
+ audio.play().catch(() => {});
+ }
+
+ return () => {
+ audio.removeEventListener("timeupdate", handleTimeUpdate);
+ audio.removeEventListener("ended", handleEnded);
+ audio.pause();
+ audio.src = "";
+ audioRef.current = null;
+ };
+ } else {
+ audioRef.current = null;
+ }
+ }, [currentTrack?.audioUrl]);
+
+ // Simulated timer playback for mock/demo tracks without audioUrl or in test environments
+ useEffect(() => {
+ let interval: ReturnType | null = null;
+
+ if (isPlaying) {
+ if (audioRef.current) {
+ audioRef.current.play().catch(() => {});
+ }
+
+ const stepMs = 100;
+ const stepSec = stepMs / 1000;
+
+ interval = setInterval(() => {
+ if (!isPlayingRef.current) {
+ if (interval) clearInterval(interval);
+ return;
+ }
+
+ // If native audio element is playing, sync from it
+ if (audioRef.current && !isNaN(audioRef.current.currentTime) && audioRef.current.currentTime > 0) {
+ if (audioRef.current.currentTime >= MAX_PREVIEW_DURATION) {
+ if (isLoopingRef.current) {
+ audioRef.current.currentTime = 0;
+ audioRef.current.play().catch(() => {});
+ setCurrentTime(0);
+ } else {
+ audioRef.current.pause();
+ audioRef.current.currentTime = 0;
+ isPlayingRef.current = false;
+ if (interval) clearInterval(interval);
+ setIsPlaying(false);
+ setCurrentTime(0);
+ }
+ } else {
+ setCurrentTime(Math.min(MAX_PREVIEW_DURATION, audioRef.current.currentTime));
+ }
+ return;
+ }
+
+ // Simulated timer update
+ setCurrentTime((prev) => {
+ if (!isPlayingRef.current) return prev;
+ const next = prev + stepSec;
+ if (next >= MAX_PREVIEW_DURATION) {
+ if (isLoopingRef.current) {
+ return 0;
+ }
+ isPlayingRef.current = false;
+ if (interval) clearInterval(interval);
+ setIsPlaying(false);
+ return 0;
+ }
+ return Math.min(MAX_PREVIEW_DURATION, Number(next.toFixed(2)));
+ });
+ }, stepMs);
+ } else {
+ if (audioRef.current) {
+ audioRef.current.pause();
+ }
+ }
+
+ return () => {
+ if (interval) clearInterval(interval);
+ };
+ }, [isPlaying]);
+
+ const play = useCallback((track?: PreviewTrack) => {
+ if (track) {
+ setCurrentTrack((prev) => {
+ if (prev?.id !== track.id) {
+ setCurrentTime(0);
+ if (audioRef.current) {
+ audioRef.current.currentTime = 0;
+ }
+ }
+ return track;
+ });
+ }
+ isPlayingRef.current = true;
+ setIsPlaying(true);
+ }, []);
+
+ const pause = useCallback(() => {
+ isPlayingRef.current = false;
+ setIsPlaying(false);
+ if (audioRef.current) {
+ audioRef.current.pause();
+ }
+ }, []);
+
+ const togglePlay = useCallback(
+ (track?: PreviewTrack) => {
+ if (track && track.id !== currentTrack?.id) {
+ setCurrentTrack(track);
+ setCurrentTime(0);
+ if (audioRef.current) {
+ audioRef.current.currentTime = 0;
+ }
+ isPlayingRef.current = true;
+ setIsPlaying(true);
+ return;
+ }
+ setIsPlaying((prev) => {
+ const next = !prev;
+ isPlayingRef.current = next;
+ return next;
+ });
+ },
+ [currentTrack]
+ );
+
+ const seekToTime = useCallback((seconds: number) => {
+ const clamped = Math.max(0, Math.min(MAX_PREVIEW_DURATION, seconds));
+ setCurrentTime(Number(clamped.toFixed(2)));
+ if (audioRef.current) {
+ audioRef.current.currentTime = clamped;
+ }
+ }, []);
+
+ const seekToRatio = useCallback((ratio: number) => {
+ const clampedRatio = Math.max(0, Math.min(1, ratio));
+ const targetSeconds = clampedRatio * MAX_PREVIEW_DURATION;
+ seekToTime(targetSeconds);
+ }, [seekToTime]);
+
+ const seek = useCallback(
+ (value: number, isRatio: boolean = false) => {
+ if (isRatio) {
+ seekToRatio(value);
+ } else {
+ seekToTime(value);
+ }
+ },
+ [seekToRatio, seekToTime]
+ );
+
+ const skip = useCallback(
+ (seconds: number) => {
+ setCurrentTime((prev) => {
+ const next = Math.max(0, Math.min(MAX_PREVIEW_DURATION, prev + seconds));
+ if (audioRef.current) {
+ audioRef.current.currentTime = next;
+ }
+ return Number(next.toFixed(2));
+ });
+ },
+ []
+ );
+
+ const setVolume = useCallback((vol: number) => {
+ const clamped = Math.max(0, Math.min(1, vol));
+ setVolumeState(clamped);
+ if (clamped > 0) {
+ previousVolumeRef.current = clamped;
+ setIsMuted(false);
+ } else {
+ setIsMuted(true);
+ }
+ }, []);
+
+ const toggleMute = useCallback(() => {
+ setIsMuted((prev) => {
+ if (prev) {
+ // Unmute -> restore previous volume or 0.8
+ setVolumeState(previousVolumeRef.current || 0.8);
+ return false;
+ } else {
+ // Mute
+ previousVolumeRef.current = volume || 0.8;
+ return true;
+ }
+ });
+ }, [volume]);
+
+ const toggleLoop = useCallback(() => {
+ setIsLooping((prev) => {
+ const next = !prev;
+ isLoopingRef.current = next;
+ return next;
+ });
+ }, []);
+
+ const stop = useCallback(() => {
+ isPlayingRef.current = false;
+ setIsPlaying(false);
+ setCurrentTime(0);
+ if (audioRef.current) {
+ audioRef.current.pause();
+ audioRef.current.currentTime = 0;
+ }
+ }, []);
+
+ const clearTrack = useCallback(() => {
+ stop();
+ setCurrentTrack(null);
+ }, [stop]);
+
+ const progress = useMemo(() => {
+ return Math.max(0, Math.min(1, currentTime / duration));
+ }, [currentTime, duration]);
+
+ const timeRemaining = useMemo(() => {
+ return Math.max(0, duration - currentTime);
+ }, [duration, currentTime]);
+
+ // Global Keyboard Shortcuts
+ useEffect(() => {
+ if (!enableKeyboardShortcuts) return;
+
+ const handleKeyDown = (e: KeyboardEvent) => {
+ const activeEl = document.activeElement;
+ const isInput =
+ activeEl &&
+ (activeEl.tagName === "INPUT" ||
+ activeEl.tagName === "TEXTAREA" ||
+ activeEl.tagName === "SELECT" ||
+ (activeEl as HTMLElement).isContentEditable);
+
+ if (isInput) return;
+
+ if (e.code === "Space" || e.key === " ") {
+ // Spacebar toggles playback
+ if (currentTrack) {
+ e.preventDefault();
+ togglePlay();
+ }
+ } else if (e.key === "m" || e.key === "M") {
+ // M toggles mute
+ toggleMute();
+ } else if (e.key === "ArrowLeft") {
+ // ArrowLeft jumps back 5s
+ e.preventDefault();
+ skip(-5);
+ } else if (e.key === "ArrowRight") {
+ // ArrowRight jumps forward 5s
+ e.preventDefault();
+ skip(5);
+ }
+ };
+
+ window.addEventListener("keydown", handleKeyDown);
+ return () => window.removeEventListener("keydown", handleKeyDown);
+ }, [enableKeyboardShortcuts, currentTrack, togglePlay, toggleMute, skip]);
+
+ const value: AudioPreviewContextType = {
+ currentTrack,
+ isPlaying,
+ currentTime,
+ duration,
+ progress,
+ volume,
+ isMuted,
+ isLooping,
+ play,
+ pause,
+ togglePlay,
+ seek,
+ seekToRatio,
+ seekToTime,
+ skip,
+ setVolume,
+ toggleMute,
+ setIsMuted,
+ toggleLoop,
+ setIsLooping,
+ stop,
+ clearTrack,
+ formatTime,
+ timeRemaining,
+ };
+
+ return React.createElement(AudioPreviewContext.Provider, { value }, children);
+}
+
+/**
+ * Custom hook to consume the global audio preview player context,
+ * or create an isolated audio preview instance if used without a provider.
+ */
+export function useAudioPreview(): AudioPreviewContextType {
+ const context = useContext(AudioPreviewContext);
+ if (context) {
+ return context;
+ }
+
+ // Fallback standalone local state if hook is used without AudioPreviewProvider
+ return useStandaloneAudioPreview();
+}
+
+/**
+ * Isolated standalone audio preview hook
+ */
+function useStandaloneAudioPreview(): AudioPreviewContextType {
+ const [currentTrack, setCurrentTrack] = useState(null);
+ const [isPlaying, setIsPlaying] = useState(false);
+ const [currentTime, setCurrentTime] = useState(0);
+ const [volume, setVolumeState] = useState(0.8);
+ const [isMuted, setIsMuted] = useState(false);
+ const [isLooping, setIsLooping] = useState(false);
+
+ const isPlayingRef = useRef(isPlaying);
+ isPlayingRef.current = isPlaying;
+ const isLoopingRef = useRef(isLooping);
+ isLoopingRef.current = isLooping;
+ const previousVolumeRef = useRef(0.8);
+ const duration = MAX_PREVIEW_DURATION;
+
+ useEffect(() => {
+ let interval: ReturnType | null = null;
+ if (isPlaying) {
+ const stepMs = 100;
+ const stepSec = stepMs / 1000;
+ interval = setInterval(() => {
+ if (!isPlayingRef.current) {
+ if (interval) clearInterval(interval);
+ return;
+ }
+ setCurrentTime((prev) => {
+ if (!isPlayingRef.current) return prev;
+ const next = prev + stepSec;
+ if (next >= MAX_PREVIEW_DURATION) {
+ if (isLoopingRef.current) return 0;
+ isPlayingRef.current = false;
+ if (interval) clearInterval(interval);
+ setIsPlaying(false);
+ return 0;
+ }
+ return Math.min(MAX_PREVIEW_DURATION, Number(next.toFixed(2)));
+ });
+ }, stepMs);
+ }
+ return () => {
+ if (interval) clearInterval(interval);
+ };
+ }, [isPlaying]);
+
+ const play = useCallback((track?: PreviewTrack) => {
+ if (track) {
+ setCurrentTrack(track);
+ setCurrentTime(0);
+ }
+ isPlayingRef.current = true;
+ setIsPlaying(true);
+ }, []);
+
+ const pause = useCallback(() => {
+ isPlayingRef.current = false;
+ setIsPlaying(false);
+ }, []);
+
+ const togglePlay = useCallback((track?: PreviewTrack) => {
+ if (track && track.id !== currentTrack?.id) {
+ setCurrentTrack(track);
+ setCurrentTime(0);
+ isPlayingRef.current = true;
+ setIsPlaying(true);
+ return;
+ }
+ setIsPlaying((p) => {
+ const next = !p;
+ isPlayingRef.current = next;
+ return next;
+ });
+ }, [currentTrack]);
+
+ const seekToTime = useCallback((seconds: number) => {
+ const clamped = Math.max(0, Math.min(MAX_PREVIEW_DURATION, seconds));
+ setCurrentTime(Number(clamped.toFixed(2)));
+ }, []);
+
+ const seekToRatio = useCallback((ratio: number) => {
+ const clampedRatio = Math.max(0, Math.min(1, ratio));
+ seekToTime(clampedRatio * MAX_PREVIEW_DURATION);
+ }, [seekToTime]);
+
+ const seek = useCallback((value: number, isRatio = false) => {
+ if (isRatio) seekToRatio(value);
+ else seekToTime(value);
+ }, [seekToRatio, seekToTime]);
+
+ const skip = useCallback((seconds: number) => {
+ setCurrentTime((prev) => Math.max(0, Math.min(MAX_PREVIEW_DURATION, prev + seconds)));
+ }, []);
+
+ const setVolume = useCallback((vol: number) => {
+ const clamped = Math.max(0, Math.min(1, vol));
+ setVolumeState(clamped);
+ if (clamped > 0) {
+ previousVolumeRef.current = clamped;
+ setIsMuted(false);
+ } else {
+ setIsMuted(true);
+ }
+ }, []);
+
+ const toggleMute = useCallback(() => {
+ setIsMuted((prev) => {
+ if (prev) {
+ setVolumeState(previousVolumeRef.current || 0.8);
+ return false;
+ } else {
+ previousVolumeRef.current = volume || 0.8;
+ return true;
+ }
+ });
+ }, [volume]);
+
+ const toggleLoop = useCallback(() => {
+ setIsLooping((p) => {
+ const next = !p;
+ isLoopingRef.current = next;
+ return next;
+ });
+ }, []);
+
+ const stop = useCallback(() => {
+ isPlayingRef.current = false;
+ setIsPlaying(false);
+ setCurrentTime(0);
+ }, []);
+
+ const clearTrack = useCallback(() => {
+ stop();
+ setCurrentTrack(null);
+ }, [stop]);
+
+ const progress = Math.max(0, Math.min(1, currentTime / duration));
+ const timeRemaining = Math.max(0, duration - currentTime);
+
+ return {
+ currentTrack,
+ isPlaying,
+ currentTime,
+ duration,
+ progress,
+ volume,
+ isMuted,
+ isLooping,
+ play,
+ pause,
+ togglePlay,
+ seek,
+ seekToRatio,
+ seekToTime,
+ skip,
+ setVolume,
+ toggleMute,
+ setIsMuted,
+ toggleLoop,
+ setIsLooping,
+ stop,
+ clearTrack,
+ formatTime,
+ timeRemaining,
+ };
+}
+
+export default useAudioPreview;
diff --git a/src/hooks/useWallet.ts b/src/hooks/useWallet.ts
index 427eb29..20985db 100644
--- a/src/hooks/useWallet.ts
+++ b/src/hooks/useWallet.ts
@@ -95,7 +95,7 @@ export function useWallet(): WalletState {
}, []);
const signTransaction = useCallback(async (xdr: string) => {
- const { signedTxXdr } = await getKit().signTransaction(xdr, { network: NETWORK });
+ const { signedTxXdr } = await getKit().signTransaction(xdr, { networkPassphrase: NETWORK });
return { signedTxXdr };
}, []);
diff --git a/src/test-setup.ts b/src/test-setup.ts
new file mode 100644
index 0000000..f149f27
--- /dev/null
+++ b/src/test-setup.ts
@@ -0,0 +1 @@
+import "@testing-library/jest-dom/vitest";
diff --git a/src/vite-env.d.ts b/src/vite-env.d.ts
new file mode 100644
index 0000000..11f02fe
--- /dev/null
+++ b/src/vite-env.d.ts
@@ -0,0 +1 @@
+///
diff --git a/tsconfig.node.json b/tsconfig.node.json
index fe1e070..fd7c712 100644
--- a/tsconfig.node.json
+++ b/tsconfig.node.json
@@ -1,13 +1,12 @@
{
"compilerOptions": {
+ "composite": true,
"target": "ES2022",
"lib": ["ES2023"],
"module": "ESNext",
"skipLibCheck": true,
"moduleResolution": "bundler",
- "allowImportingTsExtensions": true,
- "noEmit": true,
"strict": true
},
- "include": ["vite.config.ts"]
+ "include": ["vite.config.ts", "vitest.config.ts"]
}
diff --git a/vitest.config.ts b/vitest.config.ts
new file mode 100644
index 0000000..abd7345
--- /dev/null
+++ b/vitest.config.ts
@@ -0,0 +1,12 @@
+import { defineConfig } from "vitest/config";
+import react from "@vitejs/plugin-react";
+
+export default defineConfig({
+ plugins: [react()],
+ test: {
+ globals: true,
+ environment: "jsdom",
+ isolate: false,
+ setupFiles: ["./src/test-setup.ts"],
+ },
+});