Skip to content

Instantly share code, notes, and snippets.

@rollendxavier
Last active May 30, 2025 15:10
Show Gist options
  • Save rollendxavier/701decce56b6ed8885335741b3a80dd5 to your computer and use it in GitHub Desktop.
Save rollendxavier/701decce56b6ed8885335741b3a80dd5 to your computer and use it in GitHub Desktop.
Crypto 3d Dashboard using threejs & Fetch coin market data with coingecko api
import axios from "axios";
const API_URL = process.env.REACT_APP_API_URL;
const API_KEY = process.env.REACT_APP_API_KEY;
export const fetchMarketData = async () => {
try {
const response = await axios.get(API_URL, {
params: {
vs_currency: "usd",
order: "market_cap_desc",
per_page: 10,
page: 1,
sparkline: false,
},
headers: { 'x-cg-pro-api-key': API_KEY },
});
console.log("API response:", response.data); // <-- Add this line
return response.data;
} catch (error) {
console.error("Error fetching market data:", error);
return [];
}
};
import React, { useEffect } from "react";
import * as THREE from "three";
const ThreeScene = () => {
  useEffect(() => {
    const scene = new THREE.Scene();
    const camera = new THREE.PerspectiveCamera(
      75,
      window.innerWidth / window.innerHeight,
      0.1,
      1000
    );
    camera.position.z = 5;
    const renderer = new THREE.WebGLRenderer();
    renderer.setSize(window.innerWidth, window.innerHeight);
    document.body.appendChild(renderer.domElement);
    const geometry = new THREE.BoxGeometry();
    const material = new THREE.MeshBasicMaterial({ color: 0x00ff00 });
    const cube = new THREE.Mesh(geometry, material);
    scene.add(cube);
    const animate = () => {
      requestAnimationFrame(animate);
      cube.rotation.x += 0.01;
      cube.rotation.y += 0.01;
      renderer.render(scene, camera);
    };
    animate();
    return () => {
      document.body.removeChild(renderer.domElement);
    };
  }, []);
  return null;
};
export default ThreeScene;
import { useEffect, useState } from "react";
import * as THREE from "three";
import { OrbitControls } from "three/examples/jsm/controls/OrbitControls";
import { fetchMarketData } from "./fetchMarketData";
const FONT_FAMILY = "'Segoe UI', Arial, sans-serif";
const ThreeScene = () => {
const [marketData, setMarketData] = useState([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
const getData = async () => {
setLoading(true);
setError(null);
try {
const data = await fetchMarketData();
if (data && data.length > 0) {
setMarketData(data);
} else {
setError("No data received from API.");
}
} catch (err) {
setError("Failed to fetch data.");
}
setLoading(false);
};
getData();
}, []);
useEffect(() => {
if (loading || error || marketData.length === 0) return;
// Remove any previous renderer
const prevRenderer = document.getElementById("threejs-canvas");
if (prevRenderer) {
prevRenderer.remove();
}
// Scene setup
const scene = new THREE.Scene();
scene.background = new THREE.Color(0x181c20);
// Camera
const camera = new THREE.PerspectiveCamera(
60,
window.innerWidth / window.innerHeight,
0.1,
1000
);
camera.position.set(0, 0, 30);
// Renderer
const renderer = new THREE.WebGLRenderer({ antialias: true, alpha: false });
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.domElement.id = "threejs-canvas";
document.body.appendChild(renderer.domElement);
// Controls
const controls = new OrbitControls(camera, renderer.domElement);
controls.enableDamping = true;
// Lighting
scene.add(new THREE.AmbientLight(0xffffff, 0.8));
// Helper: Label with name (symbol) and price
const createTextSprite = (name, symbol, price) => {
const canvas = document.createElement("canvas");
const size = 320;
canvas.width = size;
canvas.height = 90;
const ctx = canvas.getContext("2d");
ctx.clearRect(0, 0, size, 90);
ctx.font = `bold 24px ${FONT_FAMILY}`;
ctx.fillStyle = "#fff";
ctx.textAlign = "center";
ctx.textBaseline = "top";
ctx.fillText(`${name} (${symbol})`, size / 2, 10);
ctx.font = `20px ${FONT_FAMILY}`;
ctx.fillStyle = "#ffd700";
ctx.fillText(`$${price.toLocaleString(undefined, {minimumFractionDigits: 2, maximumFractionDigits: 8})}`, size / 2, 44);
const texture = new THREE.CanvasTexture(canvas);
const spriteMaterial = new THREE.SpriteMaterial({
map: texture,
transparent: true,
});
const sprite = new THREE.Sprite(spriteMaterial);
sprite.scale.set(7, 2, 1);
return sprite;
};
// Add coins as transparent spheres with logo and label
const loader = new THREE.TextureLoader();
const spheres = [];
marketData.forEach((coin, i) => {
const geometry = new THREE.SphereGeometry(2, 32, 32);
const material = new THREE.MeshStandardMaterial({
color: 0xffffff,
transparent: true,
opacity: 0.6,
});
const sphere = new THREE.Mesh(geometry, material);
// Arrange in a circle
const angle = (i / marketData.length) * Math.PI * 2;
const radius = 10;
sphere.position.set(
Math.cos(angle) * radius,
0,
Math.sin(angle) * radius
);
scene.add(sphere);
spheres.push(sphere);
// Add coin logo as sprite centered inside the sphere
loader.load(coin.image, (texture) => {
const logoMat = new THREE.SpriteMaterial({ map: texture, transparent: true });
const logo = new THREE.Sprite(logoMat);
logo.scale.set(1.5, 1.5, 1);
logo.position.set(0, 0, 0);
sphere.add(logo);
});
// Add label above
const label = createTextSprite(coin.name, coin.symbol, coin.current_price);
label.position.copy(sphere.position).add(new THREE.Vector3(0, 3, 0));
scene.add(label);
});
// Animation
const animate = () => {
requestAnimationFrame(animate);
controls.update();
spheres.forEach((sphere) => {
sphere.rotation.y += 0.008;
});
renderer.render(scene, camera);
};
animate();
// Resize handler
const handleResize = () => {
camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();
renderer.setSize(window.innerWidth, window.innerHeight);
};
window.addEventListener("resize", handleResize);
// Cleanup
return () => {
window.removeEventListener("resize", handleResize);
if (renderer.domElement && renderer.domElement.parentNode) {
renderer.domElement.parentNode.removeChild(renderer.domElement);
}
};
}, [marketData, loading, error]);
if (loading) return <div style={{ color: "white" }}>Loading...</div>;
if (error) return <div style={{ color: "red" }}>Error: {error}</div>;
if (marketData.length === 0) return <div style={{ color: "yellow" }}>No data available.</div>;
return null;
};
export default ThreeScene;
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment