Initial commit: CHORUS Services marketing website
Complete Next.js website with Docker containerization: - Next.js 14 with TypeScript and Tailwind CSS - Responsive design with modern UI components - Hero section, features showcase, testimonials - FAQ section with comprehensive content - Contact forms and newsletter signup - Docker production build with Nginx - Health checks and monitoring support - SEO optimization and performance tuning Ready for integration as git submodule in main CHORUS project. Generated with Claude Code Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
100
components/ui/Button.tsx
Normal file
100
components/ui/Button.tsx
Normal file
@@ -0,0 +1,100 @@
|
||||
'use client';
|
||||
|
||||
import React from 'react';
|
||||
import { Button as AntButton, ButtonProps as AntButtonProps } from 'antd';
|
||||
import { motion, MotionProps } from 'framer-motion';
|
||||
import { cn } from '@/utils/cn';
|
||||
|
||||
// Extend Ant Design ButtonProps with custom variants
|
||||
interface CustomButtonProps {
|
||||
variant?: 'primary' | 'secondary' | 'outline' | 'ghost' | 'gradient';
|
||||
fullWidth?: boolean;
|
||||
animated?: boolean;
|
||||
}
|
||||
|
||||
type ButtonProps = AntButtonProps & CustomButtonProps & Partial<MotionProps>;
|
||||
|
||||
const MotionButton = motion(AntButton);
|
||||
|
||||
export const Button: React.FC<ButtonProps> = ({
|
||||
variant = 'primary',
|
||||
fullWidth = false,
|
||||
animated = true,
|
||||
className,
|
||||
children,
|
||||
...props
|
||||
}) => {
|
||||
const getVariantClasses = () => {
|
||||
switch (variant) {
|
||||
case 'primary':
|
||||
return 'bg-chorus-blue hover:bg-blue-600 border-chorus-blue hover:border-blue-600 text-white shadow-lg hover:shadow-xl';
|
||||
case 'secondary':
|
||||
return 'bg-chorus-green hover:bg-green-600 border-chorus-green hover:border-green-600 text-white shadow-lg hover:shadow-xl';
|
||||
case 'outline':
|
||||
return 'bg-transparent border-2 border-chorus-blue text-chorus-blue hover:bg-chorus-blue hover:text-white';
|
||||
case 'ghost':
|
||||
return 'bg-transparent border-transparent text-white hover:bg-white/10 hover:border-white/20';
|
||||
case 'gradient':
|
||||
return 'bg-gradient-chorus border-transparent text-white shadow-lg hover:shadow-xl';
|
||||
default:
|
||||
return '';
|
||||
}
|
||||
};
|
||||
|
||||
const buttonClasses = cn(
|
||||
'font-semibold transition-all duration-200 border-0 rounded-lg',
|
||||
'focus:ring-2 focus:ring-chorus-blue focus:ring-opacity-50 focus:outline-none',
|
||||
getVariantClasses(),
|
||||
fullWidth && 'w-full',
|
||||
className
|
||||
);
|
||||
|
||||
const animationProps = animated
|
||||
? {
|
||||
whileHover: { scale: 1.02 },
|
||||
whileTap: { scale: 0.98 },
|
||||
transition: { duration: 0.2 },
|
||||
}
|
||||
: {};
|
||||
|
||||
if (animated) {
|
||||
return (
|
||||
<MotionButton
|
||||
className={buttonClasses}
|
||||
{...animationProps}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</MotionButton>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<AntButton className={buttonClasses} {...props}>
|
||||
{children}
|
||||
</AntButton>
|
||||
);
|
||||
};
|
||||
|
||||
// Specific button variants for common use cases
|
||||
export const PrimaryButton: React.FC<Omit<ButtonProps, 'variant'>> = (props) => (
|
||||
<Button variant="primary" {...props} />
|
||||
);
|
||||
|
||||
export const SecondaryButton: React.FC<Omit<ButtonProps, 'variant'>> = (props) => (
|
||||
<Button variant="secondary" {...props} />
|
||||
);
|
||||
|
||||
export const OutlineButton: React.FC<Omit<ButtonProps, 'variant'>> = (props) => (
|
||||
<Button variant="outline" {...props} />
|
||||
);
|
||||
|
||||
export const GhostButton: React.FC<Omit<ButtonProps, 'variant'>> = (props) => (
|
||||
<Button variant="ghost" {...props} />
|
||||
);
|
||||
|
||||
export const GradientButton: React.FC<Omit<ButtonProps, 'variant'>> = (props) => (
|
||||
<Button variant="gradient" {...props} />
|
||||
);
|
||||
|
||||
export default Button;
|
||||
69
components/ui/Loading.tsx
Normal file
69
components/ui/Loading.tsx
Normal file
@@ -0,0 +1,69 @@
|
||||
'use client';
|
||||
|
||||
import React from 'react';
|
||||
import { motion } from 'framer-motion';
|
||||
import { Spin, Typography } from 'antd';
|
||||
import { cn } from '@/utils/cn';
|
||||
|
||||
const { Text } = Typography;
|
||||
|
||||
interface LoadingProps {
|
||||
size?: 'small' | 'default' | 'large';
|
||||
text?: string;
|
||||
fullScreen?: boolean;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export const Loading: React.FC<LoadingProps> = ({
|
||||
size = 'default',
|
||||
text = 'Loading...',
|
||||
fullScreen = false,
|
||||
className,
|
||||
}) => {
|
||||
const containerClasses = cn(
|
||||
'flex flex-col items-center justify-center',
|
||||
fullScreen ? 'fixed inset-0 bg-chorus-charcoal z-50' : 'py-8',
|
||||
className
|
||||
);
|
||||
|
||||
const spinnerSize = size === 'small' ? 20 : size === 'large' ? 40 : 30;
|
||||
|
||||
return (
|
||||
<motion.div
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
exit={{ opacity: 0 }}
|
||||
className={containerClasses}
|
||||
>
|
||||
<motion.div
|
||||
animate={{ rotate: 360 }}
|
||||
transition={{
|
||||
duration: 1,
|
||||
repeat: Infinity,
|
||||
ease: 'linear',
|
||||
}}
|
||||
className="mb-4"
|
||||
>
|
||||
<div
|
||||
className="rounded-full border-4 border-chorus-blue/20 border-t-chorus-blue"
|
||||
style={{
|
||||
width: spinnerSize,
|
||||
height: spinnerSize,
|
||||
}}
|
||||
/>
|
||||
</motion.div>
|
||||
|
||||
{text && (
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 10 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ delay: 0.2 }}
|
||||
>
|
||||
<Text className="text-gray-300 text-sm">{text}</Text>
|
||||
</motion.div>
|
||||
)}
|
||||
</motion.div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Loading;
|
||||
101
components/ui/ProgressIndicator.tsx
Normal file
101
components/ui/ProgressIndicator.tsx
Normal file
@@ -0,0 +1,101 @@
|
||||
'use client';
|
||||
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { motion, AnimatePresence } from 'framer-motion';
|
||||
import { Progress } from 'antd';
|
||||
|
||||
interface ProgressIndicatorProps {
|
||||
className?: string;
|
||||
}
|
||||
|
||||
const sections = ['hero', 'whoosh', 'bzzz', 'slurp', 'cooee', 'integration'];
|
||||
|
||||
export default function ProgressIndicator({ className = '' }: ProgressIndicatorProps) {
|
||||
const [progress, setProgress] = useState(0);
|
||||
const [currentSection, setCurrentSection] = useState('');
|
||||
const [isVisible, setIsVisible] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
const handleScroll = () => {
|
||||
const scrollY = window.scrollY;
|
||||
const documentHeight = document.documentElement.scrollHeight - window.innerHeight;
|
||||
const scrollProgress = Math.min((scrollY / documentHeight) * 100, 100);
|
||||
|
||||
setProgress(scrollProgress);
|
||||
setIsVisible(scrollY > 200);
|
||||
|
||||
// Find current section
|
||||
const sectionElements = sections.map(section =>
|
||||
document.getElementById(section) || document.querySelector('.hero-section')
|
||||
).filter(Boolean);
|
||||
|
||||
for (let i = sectionElements.length - 1; i >= 0; i--) {
|
||||
const element = sectionElements[i];
|
||||
if (element && element.offsetTop <= scrollY + 300) {
|
||||
setCurrentSection(sections[i]);
|
||||
break;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
window.addEventListener('scroll', handleScroll);
|
||||
handleScroll(); // Check initial position
|
||||
|
||||
return () => window.removeEventListener('scroll', handleScroll);
|
||||
}, []);
|
||||
|
||||
const getSectionTitle = (section: string) => {
|
||||
switch (section) {
|
||||
case 'hero': return 'Welcome to CHORUS';
|
||||
case 'whoosh': return 'WHOOSH - Orchestration Engine';
|
||||
case 'bzzz': return 'BZZZ - P2P Coordination';
|
||||
case 'slurp': return 'SLURP - Context Curator';
|
||||
case 'cooee': return 'COOEE - Feedback & Learning';
|
||||
case 'integration': return 'Complete Ecosystem';
|
||||
default: return 'CHORUS Services';
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<AnimatePresence>
|
||||
{isVisible && (
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: -50 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
exit={{ opacity: 0, y: -50 }}
|
||||
transition={{ duration: 0.3 }}
|
||||
className={`fixed top-0 left-0 right-0 z-40 ${className}`}
|
||||
>
|
||||
<div className="glass-effect border-b border-gray-600">
|
||||
<div className="container-chorus py-3">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex-1">
|
||||
<div className="text-sm text-gray-300 mb-1">
|
||||
{getSectionTitle(currentSection)}
|
||||
</div>
|
||||
<Progress
|
||||
percent={progress}
|
||||
strokeColor={{
|
||||
'0%': '#007aff',
|
||||
'25%': '#30d158',
|
||||
'50%': '#eab308',
|
||||
'75%': '#a855f7',
|
||||
'100%': '#f97316',
|
||||
}}
|
||||
trailColor="rgba(255, 255, 255, 0.1)"
|
||||
showInfo={false}
|
||||
strokeWidth={3}
|
||||
className="max-w-md"
|
||||
/>
|
||||
</div>
|
||||
<div className="text-xs text-gray-400 ml-4">
|
||||
{Math.round(progress)}% complete
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
);
|
||||
}
|
||||
139
components/ui/SectionNavigation.tsx
Normal file
139
components/ui/SectionNavigation.tsx
Normal file
@@ -0,0 +1,139 @@
|
||||
'use client';
|
||||
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { motion, AnimatePresence } from 'framer-motion';
|
||||
import { Tooltip } from 'antd';
|
||||
import {
|
||||
RocketIcon,
|
||||
ZapIcon,
|
||||
DatabaseIcon,
|
||||
BrainCircuitIcon,
|
||||
NetworkIcon,
|
||||
ChevronUpIcon
|
||||
} from 'lucide-react';
|
||||
|
||||
interface SectionNavigationProps {
|
||||
className?: string;
|
||||
}
|
||||
|
||||
const sections = [
|
||||
{ id: 'whoosh', name: 'WHOOSH', icon: RocketIcon, color: '#007aff' },
|
||||
{ id: 'bzzz', name: 'BZZZ', icon: ZapIcon, color: '#30d158' },
|
||||
{ id: 'slurp', name: 'SLURP', icon: DatabaseIcon, color: '#eab308' },
|
||||
{ id: 'cooee', name: 'COOEE', icon: BrainCircuitIcon, color: '#a855f7' },
|
||||
{ id: 'integration', name: 'Integration', icon: NetworkIcon, color: '#f97316' },
|
||||
];
|
||||
|
||||
export default function SectionNavigation({ className = '' }: SectionNavigationProps) {
|
||||
const [activeSection, setActiveSection] = useState('');
|
||||
const [isVisible, setIsVisible] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
const handleScroll = () => {
|
||||
const scrollY = window.scrollY;
|
||||
setIsVisible(scrollY > 800);
|
||||
|
||||
// Find active section
|
||||
const sectionElements = sections.map(section =>
|
||||
document.getElementById(section.id)
|
||||
).filter(Boolean);
|
||||
|
||||
for (let i = sectionElements.length - 1; i >= 0; i--) {
|
||||
const element = sectionElements[i];
|
||||
if (element && element.offsetTop <= scrollY + 200) {
|
||||
setActiveSection(sections[i].id);
|
||||
break;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
window.addEventListener('scroll', handleScroll);
|
||||
handleScroll(); // Check initial position
|
||||
|
||||
return () => window.removeEventListener('scroll', handleScroll);
|
||||
}, []);
|
||||
|
||||
const scrollToSection = (sectionId: string) => {
|
||||
const element = document.getElementById(sectionId);
|
||||
if (element) {
|
||||
const headerOffset = 80; // Account for fixed header
|
||||
const elementPosition = element.offsetTop - headerOffset;
|
||||
|
||||
window.scrollTo({
|
||||
top: elementPosition,
|
||||
behavior: 'smooth'
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const scrollToTop = () => {
|
||||
window.scrollTo({
|
||||
top: 0,
|
||||
behavior: 'smooth'
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<AnimatePresence>
|
||||
{isVisible && (
|
||||
<motion.div
|
||||
initial={{ opacity: 0, x: 100 }}
|
||||
animate={{ opacity: 1, x: 0 }}
|
||||
exit={{ opacity: 0, x: 100 }}
|
||||
transition={{ duration: 0.3 }}
|
||||
className={`fixed right-6 top-1/2 transform -translate-y-1/2 z-50 ${className}`}
|
||||
>
|
||||
<div className="glass-effect rounded-full p-2 border border-gray-600">
|
||||
{/* Section navigation dots */}
|
||||
{sections.map((section) => {
|
||||
const Icon = section.icon;
|
||||
const isActive = activeSection === section.id;
|
||||
|
||||
return (
|
||||
<Tooltip
|
||||
key={section.id}
|
||||
title={section.name}
|
||||
placement="left"
|
||||
overlayClassName="section-nav-tooltip"
|
||||
>
|
||||
<motion.button
|
||||
onClick={() => scrollToSection(section.id)}
|
||||
className={`block w-12 h-12 rounded-full mb-2 last:mb-0 flex items-center justify-center border-2 transition-all duration-300 ${
|
||||
isActive
|
||||
? 'border-white shadow-lg'
|
||||
: 'border-transparent hover:border-gray-400'
|
||||
}`}
|
||||
style={{
|
||||
backgroundColor: isActive ? `${section.color}20` : 'transparent'
|
||||
}}
|
||||
whileHover={{ scale: 1.1 }}
|
||||
whileTap={{ scale: 0.95 }}
|
||||
>
|
||||
<Icon
|
||||
size={20}
|
||||
style={{ color: isActive ? section.color : '#9ca3af' }}
|
||||
/>
|
||||
</motion.button>
|
||||
</Tooltip>
|
||||
);
|
||||
})}
|
||||
|
||||
{/* Scroll to top button */}
|
||||
<div className="border-t border-gray-600 mt-2 pt-2">
|
||||
<Tooltip title="Back to Top" placement="left">
|
||||
<motion.button
|
||||
onClick={scrollToTop}
|
||||
className="w-12 h-12 rounded-full flex items-center justify-center border-2 border-transparent hover:border-gray-400 transition-all duration-300"
|
||||
whileHover={{ scale: 1.1 }}
|
||||
whileTap={{ scale: 0.95 }}
|
||||
>
|
||||
<ChevronUpIcon size={20} className="text-gray-400" />
|
||||
</motion.button>
|
||||
</Tooltip>
|
||||
</div>
|
||||
</div>
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user