Initial commit: CHORUS PING! blog
- Next.js 14 blog application with theme support - Docker containerization with volume bindings - Traefik integration with Let's Encrypt SSL - MDX support for blog posts - Theme toggle with localStorage persistence - Scheduled posts directory structure - Brand guidelines compliance with CHORUS colors 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
91
components/BlogFooter.tsx
Normal file
91
components/BlogFooter.tsx
Normal file
@@ -0,0 +1,91 @@
|
||||
import Link from 'next/link'
|
||||
import ThreeLogo from './ThreeLogo'
|
||||
|
||||
export default function BlogFooter() {
|
||||
return (
|
||||
<footer className="bg-white/80 dark:bg-carbon-950/80 backdrop-blur-md border-t border-carbon-200/50 dark:border-carbon-800/50 mt-24">
|
||||
<div className="blog-container py-12">
|
||||
<div className="grid grid-cols-1 md:grid-cols-4 gap-8">
|
||||
<div className="col-span-1 md:col-span-2">
|
||||
<div className="flex items-center space-x-3 mb-4">
|
||||
<div className="w-10 h-10">
|
||||
<ThreeLogo className="w-full h-full" />
|
||||
</div>
|
||||
<div className="flex flex-col">
|
||||
<span className="font-logo font-semibold text-lg leading-none text-carbon-900 dark:text-carbon-100">CHORUS</span>
|
||||
<span className="text-carbon-600 dark:text-carbon-500 text-xs font-medium">PING!</span>
|
||||
</div>
|
||||
</div>
|
||||
<p className="text-carbon-600 dark:text-carbon-400 text-sm leading-relaxed mb-6 max-w-md">
|
||||
Insights and deep dives into contextual AI orchestration, agent coordination,
|
||||
and the future of intelligent systems.
|
||||
</p>
|
||||
<div className="flex space-x-4">
|
||||
<Link
|
||||
href="https://www.linkedin.com/in/anthonylewisrawlins"
|
||||
className="text-carbon-600 dark:text-carbon-500 hover:text-carbon-800 dark:hover:text-carbon-300 transition-colors"
|
||||
>
|
||||
<span className="sr-only">LinkedIn</span>
|
||||
<svg className="w-5 h-5" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path fillRule="evenodd" d="M16.338 16.338H13.67V12.16c0-.995-.017-2.277-1.387-2.277-1.39 0-1.601 1.086-1.601 2.207v4.248H8.014v-8.59h2.559v1.174h.037c.356-.675 1.227-1.387 2.526-1.387 2.703 0 3.203 1.778 3.203 4.092v4.711zM5.005 6.575a1.548 1.548 0 11-.003-3.096 1.548 1.548 0 01.003 3.096zm-1.337 9.763H6.34v-8.59H3.667v8.59zM17.668 1H2.328C1.595 1 1 1.581 1 2.298v15.403C1 18.418 1.595 19 2.328 19h15.34c.734 0 1.332-.582 1.332-1.299V2.298C19 1.581 18.402 1 17.668 1z" clipRule="evenodd"/>
|
||||
</svg>
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h3 className="text-carbon-900 dark:text-carbon-100 font-semibold mb-4">Blog</h3>
|
||||
<ul className="space-y-2 text-sm">
|
||||
<li>
|
||||
<Link href="/" className="text-carbon-600 dark:text-carbon-400 hover:text-carbon-800 dark:hover:text-carbon-200 transition-colors">
|
||||
All Posts
|
||||
</Link>
|
||||
</li>
|
||||
<li>
|
||||
<Link href="/tags" className="text-carbon-600 dark:text-carbon-400 hover:text-carbon-800 dark:hover:text-carbon-200 transition-colors">
|
||||
Topics
|
||||
</Link>
|
||||
</li>
|
||||
<li>
|
||||
<Link href="/rss.xml" className="text-carbon-600 dark:text-carbon-400 hover:text-carbon-800 dark:hover:text-carbon-200 transition-colors">
|
||||
RSS Feed
|
||||
</Link>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h3 className="text-carbon-900 dark:text-carbon-100 font-semibold mb-4">CHORUS</h3>
|
||||
<ul className="space-y-2 text-sm">
|
||||
<li>
|
||||
<Link href="https://chorus.services" className="text-carbon-600 dark:text-carbon-400 hover:text-carbon-800 dark:hover:text-carbon-200 transition-colors">
|
||||
Platform
|
||||
</Link>
|
||||
</li>
|
||||
<li>
|
||||
<Link href="https://chorus.services/about" className="text-carbon-600 dark:text-carbon-400 hover:text-carbon-800 dark:hover:text-carbon-200 transition-colors">
|
||||
About
|
||||
</Link>
|
||||
</li>
|
||||
<li>
|
||||
<Link href="https://chorus.services/contact" className="text-carbon-600 dark:text-carbon-400 hover:text-carbon-800 dark:hover:text-carbon-200 transition-colors">
|
||||
Contact
|
||||
</Link>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="border-t border-carbon-200 dark:border-carbon-800 mt-8 pt-8 text-center">
|
||||
<p className="text-carbon-600 dark:text-carbon-500 text-sm">
|
||||
© {new Date().getFullYear()} CHORUS Services. Built with care by{' '}
|
||||
<Link href="https://deepblack.cloud" className="text-carbon-700 dark:text-carbon-400 hover:text-carbon-900 dark:hover:text-carbon-200 transition-colors">
|
||||
Deep Black Cloud
|
||||
</Link>
|
||||
.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
)
|
||||
}
|
||||
72
components/BlogHeader.tsx
Normal file
72
components/BlogHeader.tsx
Normal file
@@ -0,0 +1,72 @@
|
||||
'use client'
|
||||
|
||||
import Link from 'next/link'
|
||||
import { useState, useEffect } from 'react'
|
||||
import ThreeLogo from './ThreeLogo'
|
||||
import ThemeToggle from './ThemeToggle'
|
||||
|
||||
export default function BlogHeader() {
|
||||
const [isScrolled, setIsScrolled] = useState(false)
|
||||
|
||||
useEffect(() => {
|
||||
const handleScroll = () => {
|
||||
setIsScrolled(window.scrollY > 0)
|
||||
}
|
||||
|
||||
window.addEventListener('scroll', handleScroll)
|
||||
return () => window.removeEventListener('scroll', handleScroll)
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<header className={`sticky top-0 z-50 transition-all duration-300 ${
|
||||
isScrolled
|
||||
? 'bg-white/80 dark:bg-carbon-950/80 backdrop-blur-md border-b border-carbon-200/50 dark:border-carbon-800/50'
|
||||
: 'bg-white/20 dark:bg-carbon-950/20 backdrop-blur-sm'
|
||||
}`}>
|
||||
<nav className="blog-container py-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<Link href="/" className="flex items-center space-x-3 group">
|
||||
<div className="w-14 h-14 group-hover:scale-110 transition-transform">
|
||||
<ThreeLogo className="w-full h-full" />
|
||||
</div>
|
||||
<div className="flex flex-col">
|
||||
<span className="font-logo font-semibold text-lg leading-none text-carbon-900 dark:text-carbon-100">CHORUS</span>
|
||||
<span className="text-carbon-600 dark:text-carbon-500 text-xs font-medium">PING!</span>
|
||||
</div>
|
||||
</Link>
|
||||
|
||||
<div className="hidden md:flex items-center space-x-8">
|
||||
<Link
|
||||
href="/"
|
||||
className="text-carbon-600 dark:text-carbon-300 hover:text-carbon-900 dark:hover:text-carbon-100 transition-colors font-medium"
|
||||
>
|
||||
All Posts
|
||||
</Link>
|
||||
<Link
|
||||
href="/tags"
|
||||
className="text-carbon-600 dark:text-carbon-300 hover:text-carbon-900 dark:hover:text-carbon-100 transition-colors font-medium"
|
||||
>
|
||||
Topics
|
||||
</Link>
|
||||
<Link
|
||||
href="https://chorus.services"
|
||||
className="text-carbon-600 dark:text-carbon-300 hover:text-carbon-900 dark:hover:text-carbon-100 transition-colors font-medium"
|
||||
>
|
||||
About CHORUS
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center space-x-4">
|
||||
<ThemeToggle />
|
||||
<Link
|
||||
href="https://chorus.services"
|
||||
className="px-4 py-2 bg-carbon-900 dark:bg-mulberry-700 hover:bg-carbon-800 dark:hover:bg-mulberry-600 text-white dark:text-mulberry-100 rounded-lg font-medium transition-colors text-sm"
|
||||
>
|
||||
Join Waitlist
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
</header>
|
||||
)
|
||||
}
|
||||
87
components/PostCard.tsx
Normal file
87
components/PostCard.tsx
Normal file
@@ -0,0 +1,87 @@
|
||||
import Link from 'next/link'
|
||||
import { BlogPost } from '@/types/blog'
|
||||
|
||||
interface PostCardProps {
|
||||
post: BlogPost
|
||||
featured?: boolean
|
||||
}
|
||||
|
||||
export default function PostCard({ post, featured = false }: PostCardProps) {
|
||||
const formattedDate = new Date(post.date).toLocaleDateString('en-US', {
|
||||
year: 'numeric',
|
||||
month: 'long',
|
||||
day: 'numeric'
|
||||
})
|
||||
|
||||
return (
|
||||
<article className={`group ${
|
||||
featured
|
||||
? 'bg-carbon-50 dark:bg-carbon-900 border border-carbon-200 dark:border-carbon-800 rounded-xl p-8 hover:border-carbon-400 dark:hover:border-mulberry-700 transition-all duration-300'
|
||||
: 'bg-carbon-100/50 dark:bg-carbon-900/50 border border-carbon-200/50 dark:border-carbon-800/50 rounded-lg p-6 hover:bg-carbon-50 dark:hover:bg-carbon-900 hover:border-carbon-300 dark:hover:border-carbon-700 transition-all duration-300'
|
||||
}`}>
|
||||
<Link href={`/posts/${post.slug}`} className="block">
|
||||
<div className="mb-4">
|
||||
<div className="flex items-center justify-between mb-3">
|
||||
<div className="blog-meta">
|
||||
<time dateTime={post.date} className="text-carbon-600 dark:text-carbon-500">
|
||||
{formattedDate}
|
||||
</time>
|
||||
<span className="text-carbon-500 dark:text-carbon-600">•</span>
|
||||
<span className="text-carbon-600 dark:text-carbon-500">
|
||||
{post.readingTime} min read
|
||||
</span>
|
||||
</div>
|
||||
{post.featured && (
|
||||
<span className="blog-tag bg-mulberry-700 text-mulberry-200">
|
||||
Featured
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<h2 className={`${
|
||||
featured ? 'text-h3' : 'text-h4'
|
||||
} font-logo text-carbon-950 dark:text-carbon-100 group-hover:text-carbon-700 dark:group-hover:text-mulberry-200 transition-colors mb-3`}>
|
||||
{post.title}
|
||||
</h2>
|
||||
|
||||
<p className="text-carbon-700 dark:text-carbon-300 leading-relaxed mb-4 line-clamp-3">
|
||||
{post.description}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center space-x-2">
|
||||
<div className="w-8 h-8 bg-gradient-to-br from-mulberry-400 to-ocean-500 rounded-full flex items-center justify-center">
|
||||
<span className="text-carbon-950 font-semibold text-xs">
|
||||
{post.author?.name?.charAt(0) || 'C'}
|
||||
</span>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-carbon-950 dark:text-carbon-200 font-medium text-sm">
|
||||
{post.author?.name || 'CHORUS Team'}
|
||||
</p>
|
||||
{post.author?.role && (
|
||||
<p className="text-carbon-600 dark:text-carbon-500 text-xs">
|
||||
{post.author.role}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{post.tags?.slice(0, 3).map((tag) => (
|
||||
<span key={tag} className="blog-tag">
|
||||
{tag}
|
||||
</span>
|
||||
)) || []}
|
||||
{(post.tags?.length || 0) > 3 && (
|
||||
<span className="text-carbon-600 dark:text-carbon-500 text-xs">
|
||||
+{(post.tags?.length || 0) - 3} more
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</Link>
|
||||
</article>
|
||||
)
|
||||
}
|
||||
58
components/ThemeToggle.tsx
Normal file
58
components/ThemeToggle.tsx
Normal file
@@ -0,0 +1,58 @@
|
||||
'use client'
|
||||
|
||||
import React, { useState, useEffect } from 'react'
|
||||
|
||||
export default function ThemeToggle() {
|
||||
const [theme, setTheme] = useState<'light' | 'dark'>('dark')
|
||||
|
||||
useEffect(() => {
|
||||
// Check for saved theme preference or default to dark
|
||||
const savedTheme = localStorage.getItem('theme') as 'light' | 'dark' | null
|
||||
const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches
|
||||
const initialTheme = savedTheme || (prefersDark ? 'dark' : 'light')
|
||||
|
||||
setTheme(initialTheme)
|
||||
|
||||
// Apply theme to document
|
||||
if (initialTheme === 'dark') {
|
||||
document.documentElement.classList.add('dark')
|
||||
} else {
|
||||
document.documentElement.classList.remove('dark')
|
||||
}
|
||||
}, [])
|
||||
|
||||
const toggleTheme = () => {
|
||||
const newTheme = theme === 'dark' ? 'light' : 'dark'
|
||||
setTheme(newTheme)
|
||||
|
||||
// Save to localStorage
|
||||
localStorage.setItem('theme', newTheme)
|
||||
|
||||
// Apply to document
|
||||
if (newTheme === 'dark') {
|
||||
document.documentElement.classList.add('dark')
|
||||
} else {
|
||||
document.documentElement.classList.remove('dark')
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<button
|
||||
onClick={toggleTheme}
|
||||
className="p-chorus-sm hover:bg-black/10 dark:hover:bg-white/10 transition-colors rounded-lg backdrop-blur-sm bg-white/20 dark:bg-black/20 border border-white/30 dark:border-white/20"
|
||||
aria-label="Toggle theme"
|
||||
>
|
||||
{theme === 'dark' ? (
|
||||
// Sun icon for switching to light mode
|
||||
<svg className="w-5 h-5 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 3v1m0 16v1m9-9h-1M4 12H3m15.364 6.364l-.707-.707M6.343 6.343l-.707-.707m12.728 0l-.707.707M6.343 17.657l-.707.707M16 12a4 4 0 11-8 0 4 4 0 018 0z" />
|
||||
</svg>
|
||||
) : (
|
||||
// Moon icon for switching to dark mode
|
||||
<svg className="w-5 h-5 text-carbon-950" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M20.354 15.354A9 9 0 018.646 3.646 9.003 9.003 0 0012 21a9.003 9.003 0 008.354-5.646z" />
|
||||
</svg>
|
||||
)}
|
||||
</button>
|
||||
)
|
||||
}
|
||||
236
components/ThreeLogo.tsx
Normal file
236
components/ThreeLogo.tsx
Normal file
@@ -0,0 +1,236 @@
|
||||
'use client';
|
||||
|
||||
import { useEffect, useRef } from 'react';
|
||||
import * as THREE from 'three';
|
||||
import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader.js';
|
||||
import { DRACOLoader } from 'three/examples/jsm/loaders/DRACOLoader.js';
|
||||
|
||||
interface ThreeLogoProps {
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export default function ThreeLogo({ className = "" }: ThreeLogoProps) {
|
||||
const containerRef = useRef<HTMLDivElement | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (!containerRef.current) return;
|
||||
|
||||
const container = containerRef.current;
|
||||
|
||||
// Scene / Renderer / Camera
|
||||
const scene = new THREE.Scene();
|
||||
const camera = new THREE.PerspectiveCamera(45, 1, 0.1, 100);
|
||||
camera.position.set(0, 0, 3.0); // Move camera back to prevent clipping
|
||||
camera.lookAt(0, 0, 0); // Ensure camera looks at exact center
|
||||
|
||||
const renderer = new THREE.WebGLRenderer({ antialias: true, alpha: true });
|
||||
renderer.setPixelRatio(window.devicePixelRatio);
|
||||
renderer.setClearColor(0x000000, 0); // transparent background
|
||||
container.appendChild(renderer.domElement);
|
||||
|
||||
// Resize handling with proper aspect ratio
|
||||
const resize = () => {
|
||||
const { clientWidth, clientHeight } = container;
|
||||
// Ensure square aspect ratio for the logo
|
||||
const size = Math.min(clientWidth, clientHeight);
|
||||
renderer.setSize(size, size, false);
|
||||
camera.aspect = 1; // Always square
|
||||
camera.updateProjectionMatrix();
|
||||
};
|
||||
resize();
|
||||
window.addEventListener('resize', resize);
|
||||
|
||||
// Exact lighting setup from your reference logo.html
|
||||
const light = new THREE.PointLight(0xffffff, 1.4);
|
||||
light.position.set(0, 4, 1);
|
||||
scene.add(light);
|
||||
|
||||
const bottomLight = new THREE.PointLight(0x800080, 1.2, 12);
|
||||
bottomLight.position.set(0, -4, 1);
|
||||
scene.add(bottomLight);
|
||||
|
||||
const leftLight = new THREE.PointLight(0x808000, 1.45, 5);
|
||||
leftLight.position.set(-5, 0, 4);
|
||||
scene.add(leftLight);
|
||||
|
||||
scene.add(new THREE.AmbientLight(0xffffff, 0.45));
|
||||
|
||||
// Load environment map from your horizon gradient image
|
||||
const expandGradient = (img: HTMLImageElement) => {
|
||||
const canvas = document.createElement('canvas');
|
||||
const w = 512, h = 256; // safe HDRI-like size
|
||||
canvas.width = w;
|
||||
canvas.height = h;
|
||||
const ctx = canvas.getContext('2d')!;
|
||||
|
||||
// Stretch the narrow gradient strip to fill the canvas
|
||||
ctx.drawImage(img, 0, 0, w, h);
|
||||
|
||||
return new THREE.CanvasTexture(canvas);
|
||||
};
|
||||
|
||||
const envLoader = new THREE.ImageLoader();
|
||||
envLoader.load('/logos/horizon-gradient.png', (image) => {
|
||||
const tex = expandGradient(image);
|
||||
tex.mapping = THREE.EquirectangularReflectionMapping;
|
||||
|
||||
// Generate proper environment map using PMREM
|
||||
const pmrem = new THREE.PMREMGenerator(renderer);
|
||||
const envMap = pmrem.fromEquirectangular(tex).texture;
|
||||
|
||||
scene.environment = envMap;
|
||||
pmrem.dispose();
|
||||
});
|
||||
|
||||
scene.background = null; // keep transparent
|
||||
|
||||
// Theme-aware material colors
|
||||
const getThemeMaterial = (theme: string = 'default') => {
|
||||
const colorMap = {
|
||||
'default': 0x333333,
|
||||
'protanopia': 0x1e40af, // Blue-800 for red-blind
|
||||
'deuteranopia': 0x6b21a8, // Purple-800 for green-blind
|
||||
'tritanopia': 0x991b1b, // Red-800 for blue-blind
|
||||
'achromatopsia': 0x374151, // Gray-700 for color-blind
|
||||
};
|
||||
|
||||
return new THREE.MeshPhysicalMaterial({
|
||||
color: colorMap[theme as keyof typeof colorMap] || colorMap.default,
|
||||
roughness: 0.24,
|
||||
metalness: 1.0,
|
||||
clearcoat: 0.48,
|
||||
clearcoatRoughness: 0.15,
|
||||
reflectivity: 1.2,
|
||||
sheen: 0.35,
|
||||
sheenColor: new THREE.Color(0x212121),
|
||||
sheenRoughness: 0.168,
|
||||
envMapIntensity: 1,
|
||||
});
|
||||
};
|
||||
|
||||
// Initialize with default material
|
||||
let currentMaterial = getThemeMaterial('default');
|
||||
|
||||
// Load GLB with DRACO support
|
||||
const loader = new GLTFLoader();
|
||||
const draco = new DRACOLoader();
|
||||
draco.setDecoderPath('/draco/');
|
||||
loader.setDRACOLoader(draco);
|
||||
|
||||
let model: THREE.Object3D | null = null;
|
||||
|
||||
console.log('Loading your mobius-ring.glb...');
|
||||
loader.load(
|
||||
'/logos/mobius-ring.glb',
|
||||
(gltf) => {
|
||||
console.log('🎉 Your GLB loaded successfully!', gltf);
|
||||
model = gltf.scene;
|
||||
|
||||
// Apply the theme-aware material
|
||||
model.traverse((child) => {
|
||||
if ((child as THREE.Mesh).isMesh) {
|
||||
(child as THREE.Mesh).material = currentMaterial;
|
||||
}
|
||||
});
|
||||
|
||||
// Center the model exactly at origin
|
||||
const box = new THREE.Box3().setFromObject(model);
|
||||
const center = box.getCenter(new THREE.Vector3());
|
||||
model.position.sub(center); // Move model so its center is at (0,0,0)
|
||||
|
||||
scene.add(model);
|
||||
console.log('🎯 Your Möbius GLB model added to scene and centered at origin');
|
||||
},
|
||||
(progress) => {
|
||||
if (progress.total > 0) {
|
||||
const percent = Math.round(progress.loaded / progress.total * 100);
|
||||
console.log(`GLB loading progress: ${percent}% (${progress.loaded}/${progress.total} bytes)`);
|
||||
}
|
||||
},
|
||||
(err) => {
|
||||
console.error('❌ GLB load error:', err);
|
||||
|
||||
// Fallback: create a torus geometry with the theme-aware material
|
||||
console.log('Creating fallback torus geometry...');
|
||||
const fallbackGeometry = new THREE.TorusGeometry(0.6, 0.2, 16, 100);
|
||||
const fallbackMesh = new THREE.Mesh(fallbackGeometry, currentMaterial);
|
||||
fallbackMesh.position.set(0, 0, 0); // Ensure fallback is also centered
|
||||
scene.add(fallbackMesh);
|
||||
model = fallbackMesh;
|
||||
|
||||
console.log('⚠️ Fallback torus geometry created (placeholder for your GLB)');
|
||||
}
|
||||
);
|
||||
|
||||
// Listen for accessibility theme changes
|
||||
const handleThemeChange = (event: any) => {
|
||||
const newTheme = event.detail.theme;
|
||||
const newMaterial = getThemeMaterial(newTheme);
|
||||
|
||||
if (model) {
|
||||
// Update material on all meshes
|
||||
model.traverse((child: any) => {
|
||||
if (child.isMesh) {
|
||||
child.material.dispose(); // Clean up old material
|
||||
child.material = newMaterial;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Update current material reference
|
||||
currentMaterial = newMaterial;
|
||||
console.log(`🎨 Logo theme changed to: ${newTheme}`);
|
||||
};
|
||||
|
||||
window.addEventListener('accessibilityThemeChanged', handleThemeChange);
|
||||
|
||||
let raf = 0;
|
||||
const tick = () => {
|
||||
raf = requestAnimationFrame(tick);
|
||||
|
||||
if (model) {
|
||||
// Use exact rotation parameters from your reference logo.html
|
||||
model.rotation.x += 0.010; // spinSpeedX from params
|
||||
model.rotation.y += -0.010; // spinSpeedY from params
|
||||
model.rotation.z += -0.1; // spinSpeedZ from params
|
||||
}
|
||||
|
||||
renderer.render(scene, camera);
|
||||
};
|
||||
tick();
|
||||
|
||||
// Cleanup
|
||||
return () => {
|
||||
cancelAnimationFrame(raf);
|
||||
window.removeEventListener('resize', resize);
|
||||
window.removeEventListener('accessibilityThemeChanged', handleThemeChange);
|
||||
renderer.dispose();
|
||||
draco.dispose();
|
||||
if (container.contains(renderer.domElement)) {
|
||||
container.removeChild(renderer.domElement);
|
||||
}
|
||||
scene.traverse((obj: any) => {
|
||||
if (obj.geometry) obj.geometry.dispose?.();
|
||||
if (obj.material) {
|
||||
const mats = Array.isArray(obj.material) ? obj.material : [obj.material];
|
||||
mats.forEach((m: any) => m.dispose?.());
|
||||
}
|
||||
});
|
||||
};
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={containerRef}
|
||||
className={className}
|
||||
style={{
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
aspectRatio: '1 / 1',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center'
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user