diff --git a/components/FeatureCard.tsx b/components/FeatureCard.tsx index b57c6bb..b1798a4 100644 --- a/components/FeatureCard.tsx +++ b/components/FeatureCard.tsx @@ -1,51 +1,91 @@ -import React from 'react'; -import { LucideIcon, ExternalLink } from 'lucide-react'; +import React, { CSSProperties, ReactNode, forwardRef } from 'react'; import Link from 'next/link'; interface FeatureCardProps { - icon: LucideIcon; - title: string; - description: string; + title: ReactNode; + description: ReactNode; + ctaHref: string; + ctaText: string; + media: ReactNode; className?: string; - link?: string; + isSlideChanging?: boolean; + leftWrapperStyle?: CSSProperties; + leftContentStyle?: CSSProperties; + leftContentRef?: React.Ref; + children?: ReactNode; } -const DEFAULT_DOCS_URL = 'https://opsimate.vercel.app/docs/'; - -const FeatureCard: React.FC = ({ - icon: Icon, +const FeatureCard = forwardRef(({ title, description, + ctaHref, + ctaText, + media, className = '', - link, -}) => { - const resolvedLink = link ?? DEFAULT_DOCS_URL; + isSlideChanging = false, + leftWrapperStyle, + leftContentStyle, + leftContentRef, + children, +}, ref) => { + const isExternal = /^https?:\/\//.test(ctaHref); return ( -
-
-
- +
+
+
+
+
+
+ + O P S I M A T E +
+
+
+

+ {title} +

+

+ {description} + + {ctaText} + + +

+
+
+
+ +
+ {media}
-

{title}

-

{description}

- {resolvedLink && ( - - Learn more - {resolvedLink.startsWith('https') && } - - )} + {children}
); -}; +}); + +FeatureCard.displayName = 'FeatureCard'; export default FeatureCard; diff --git a/components/FeaturesSection.tsx b/components/FeaturesSection.tsx index 6e8ca29..b5ba91c 100644 --- a/components/FeaturesSection.tsx +++ b/components/FeaturesSection.tsx @@ -1,94 +1,286 @@ -import React from 'react'; -import { - Monitor, - Bell, - Settings, - Database, - Activity, - GitBranch, - Server -} from 'lucide-react'; +import React, { ReactNode, SetStateAction, useEffect, useRef, useState } from 'react'; +import Image from 'next/image'; import FeatureCard from './FeatureCard'; +interface SlideSpec { + heading: string; + description: ReactNode; + ctaHref: string; + ctaText: string; + imageSrc: string; + imageAlt: string; + mediaDurationMs?: number; +} + +const SLIDES: SlideSpec[] = [ + { + heading: 'Unified Monitoring', + description: 'Bring bare metal, Kubernetes, Lambda, and every third-party integration into one observability command center. Correlate metrics, logs, alerts, and automations from every tool to see the full impact of changes in real time.', + ctaHref: 'https://opsimate.vercel.app/docs/core-features', + ctaText: 'Learn more', + imageSrc: '/images/unifiedMonitoring.jpg', + imageAlt: 'Unified monitoring dashboard showing cross-platform telemetry', + mediaDurationMs: 5000, + }, + { + heading: 'Infrastructure Management', + description: 'Treat each provider as a living inventory of servers, clusters, and containers, and manage them from one menu. Drill into any provider to supervise the services it hosts, regardless of whether it is systemd, Docker, or Kubernetes based.', + ctaHref: 'https://opsimate.vercel.app/docs/providers-services/overview', + ctaText: 'Learn more', + imageSrc: '/images/infrastructureManagement.jpg', + imageAlt: 'Infrastructure management inventory view listing providers and services', + mediaDurationMs: 5000, + }, + { + heading: 'Real-time Metrics', + description: 'Use the main dashboard to watch health snapshots, search services, and launch controls without leaving the page. Real-time performance cards, logs, and filters keep every workload observable and actionable at a glance.', + ctaHref: 'https://opsimate.vercel.app/docs/dashboards/overview', + ctaText: 'Learn more', + imageSrc: '/images/real-timeMetrics.jpg', + imageAlt: 'Real-time performance metrics charts with live service data', + mediaDurationMs: 5000, + }, + { + heading: 'Smart Alerts', + description: 'Sync alert rules from Grafana and other providers, map them to service tags, and surface active states right inside the service menu. Alerts follow the same lifecycle as your external tools, so operators never miss a warning or acknowledgment.', + ctaHref: 'https://opsimate.vercel.app/docs/alerts/adding-alerts', + ctaText: 'Learn more', + imageSrc: '/images/smartAlerts.jpg', + imageAlt: 'Smart alerts panel with status badges and acknowledgment timeline', + mediaDurationMs: 5000, + }, + { + heading: 'Log Aggregation', + description: 'Wire up Grafana, Kibana, Datadog, or any preferred observability stack to stream monitoring data, logs, and actions into OpsiMate. Use integrations to pivot between dashboards, deep links, and remediation steps without context switching.', + ctaHref: 'https://opsimate.vercel.app/docs/integrations/overview', + ctaText: 'Learn more', + imageSrc: '/images/logAggregation.jpg', + imageAlt: 'Log aggregation feed combining events from multiple observability tools', + mediaDurationMs: 5000, + }, + { + heading: 'Service Discovery', + description: 'Start from any provider, pop open the contextual menu, and add the services you want under management. Automatic discovery plus guided selection keeps every container, systemd unit, or pod tied to the right owner and monitoring policy.', + ctaHref: 'https://opsimate.vercel.app/docs/providers-services/services/add-services', + ctaText: 'Learn more', + imageSrc: '/images/serviceDiscovery.jpg', + imageAlt: 'Service discovery workflow for selecting containers and pods', + mediaDurationMs: 5000, + }, + { + heading: 'Automated Actions', + description: 'The service menu brings start, stop, restart, force stop, log access, health checks, and scripted actions to a single drawer. Tailor controls per service type—systemd, Docker, or Kubernetes—to resolve incidents without hopping between shells.', + ctaHref: 'https://opsimate.vercel.app/docs/dashboards/service-menu', + ctaText: 'Learn more', + imageSrc: '/images/automatedActions.jpg', + imageAlt: 'Automated actions drawer with start stop restart service controls', + mediaDurationMs: 5000, + }, + { + heading: 'Open Source', + description: 'Clone the repo, install dependencies with pnpm, and run both client and server locally to extend the platform. Tests, linting, and a transparent workflow make it straightforward to contribute features or adapt OpsiMate to your stack.', + ctaHref: 'https://opsimate.vercel.app/docs/development', + ctaText: 'Learn more', + imageSrc: '/images/openSource.jpg', + imageAlt: 'Open source contributor guide highlighting repo setup steps', + mediaDurationMs: 5000, + }, +]; + const FeaturesSection: React.FC = () => { - const features = [ - { - icon: Monitor, - title: 'Unified Monitoring', - description: 'Monitor your entire infrastructure from a single dashboard with real-time metrics and health status.', - link: 'https://opsimate.vercel.app/docs/core-features', - }, - { - icon: Server, - title: 'Infrastructure Management', - description: 'Manage VMs and Kubernetes clusters seamlessly via SSH without agent installation.', - link: 'https://opsimate.vercel.app/docs/providers-services/overview', - }, - { - icon: Activity, - title: 'Real-time Metrics', - description: 'Track system performance with instant visibility into CPU, memory, disk, and network usage.', - link: 'https://opsimate.vercel.app/docs/dashboards/overview', - }, - { - icon: Bell, - title: 'Smart Alerts', - description: 'Get intelligent notifications before issues impact users with custom thresholds.', - link: 'https://opsimate.vercel.app/docs/alerts/adding-alerts', - }, - { - icon: Database, - title: 'Log Aggregation', - description: 'Centralize logs from all services with powerful search and pattern analysis.', - link: 'https://opsimate.vercel.app/docs/integrations/overview', - }, - { - icon: GitBranch, - title: 'Service Discovery', - description: 'Auto-discover systemd services and containers without manual configuration.', - link: 'https://opsimate.vercel.app/docs/providers-services/services/add-services', - }, - { - icon: Settings, - title: 'Automated Actions', - description: 'Create automated responses with workflows that restart services and scale resources.', - link: 'https://opsimate.vercel.app/docs/dashboards/service-menu', - }, - { - icon: GitBranch, - title: 'Open Source', - description: 'Fully open source with transparent development. Contribute and customize freely.', - link: 'https://opsimate.vercel.app/docs/development', + const [slide, setSlide] = useState(0); + const [isPlaying, setIsPlaying] = useState(false); + const timerRef = useRef | null>(null); + const progressRef = useRef | null>(null); + const transitionRef = useRef | null>(null); + const [progress, setProgress] = useState(0); + const [isSlideChanging, setIsSlideChanging] = useState(false); + const heroRef = useRef(null); + const leftContentRef = useRef(null); + const [leftScale, setLeftScale] = useState(1); + const [leftScaledHeight, setLeftScaledHeight] = useState(null); + + useEffect(() => { + if (typeof window === 'undefined') return; + const mediaQuery = window.matchMedia('(min-width: 1024px)'); + const syncPlayback = (event: MediaQueryListEvent) => { + setIsPlaying(event.matches); + }; + + setIsPlaying(mediaQuery.matches); + + if (typeof mediaQuery.addEventListener === 'function') { + mediaQuery.addEventListener('change', syncPlayback); + return () => mediaQuery.removeEventListener('change', syncPlayback); } - ]; - return ( -
-
- {/* Section Header */} -
-

- Everything You Need to{' '} - Manage Infrastructure -

-

- OpsiMate provides comprehensive tools to monitor, manage, and optimize - your infrastructure from a single platform. -

-
+ const previousOnChange = mediaQuery.onchange; + const handleChange = (event: MediaQueryListEvent) => { + previousOnChange?.call(mediaQuery, event); + syncPlayback(event); + }; + + mediaQuery.onchange = handleChange; + return () => { + if (mediaQuery.onchange === handleChange) { + mediaQuery.onchange = previousOnChange ?? null; + } + }; + }, []); + + const copy = SLIDES[slide]; + const slideCount = SLIDES.length; + const slideDurationMs = copy.mediaDurationMs ?? 5000; + + useEffect(() => { + if (timerRef.current) clearInterval(timerRef.current); + if (progressRef.current) clearInterval(progressRef.current); + if (!isPlaying) return; + setProgress(0); + const started = Date.now(); + progressRef.current = setInterval(() => { + const t = Math.min(1, (Date.now() - started) / slideDurationMs); + setProgress(t); + }, 50); + timerRef.current = setInterval(() => { + setProgress(0); + setIsSlideChanging(true); + setSlide((s) => (s + 1) % slideCount); + if (transitionRef.current) clearTimeout(transitionRef.current); + transitionRef.current = setTimeout(() => setIsSlideChanging(false), 250); + }, slideDurationMs); + return () => { + if (timerRef.current) clearInterval(timerRef.current); + if (progressRef.current) clearInterval(progressRef.current); + }; + }, [isPlaying, slide, slideCount, slideDurationMs]); - {/* Features Grid */} -
- {features.map((feature, index) => ( - - ))} + const runSlideTransition = (action: SetStateAction) => { + setIsSlideChanging(true); + setSlide(action); + setProgress(0); + if (transitionRef.current) clearTimeout(transitionRef.current); + transitionRef.current = setTimeout(() => setIsSlideChanging(false), 250); + }; + + const jumpTo = (i: number) => { + runSlideTransition(() => i); + }; + + const goToPrevious = () => { + runSlideTransition((s) => (s + slideCount - 1) % slideCount); + }; + + const goToNext = () => { + runSlideTransition((s) => (s + 1) % slideCount); + }; + + useEffect(() => { + return () => { + if (transitionRef.current) clearTimeout(transitionRef.current); + }; + }, []); + + useEffect(() => { + const recompute = () => { + const hero = heroRef.current; + const inner = leftContentRef.current; + if (!hero || !inner) return; + const styles = window.getComputedStyle(hero); + const padT = parseFloat(styles.paddingTop || '0'); + const padB = parseFloat(styles.paddingBottom || '0'); + const availableH = hero.clientHeight - padT - padB - 8; + const neededH = inner.scrollHeight; + const sRaw = Math.min(1, availableH / neededH); + const s = Number.isFinite(sRaw) && sRaw > 0 ? sRaw : 1; + setLeftScale(s); + setLeftScaledHeight(Math.round(neededH * s)); + }; + const onResize = () => { + requestAnimationFrame(recompute); + }; + + requestAnimationFrame(recompute); + + window.addEventListener('resize', onResize); + return () => { + window.removeEventListener('resize', onResize); + }; + }, []); + + return ( +
+
+
+
+ + Feature Catalogue +
+ +
+
+ {copy.imageAlt} +
+
+ )} + > +
+ +
+ +
+ {SLIDES.map((feature, i) => ( + + ))} +
+
+ +
+
); diff --git a/public/gifs/demo.gif b/public/gifs/demo.gif new file mode 100644 index 0000000..bbfaa23 Binary files /dev/null and b/public/gifs/demo.gif differ diff --git a/public/images/automatedActions.jpg b/public/images/automatedActions.jpg new file mode 100644 index 0000000..4d0c49b Binary files /dev/null and b/public/images/automatedActions.jpg differ diff --git a/public/images/infrastructureManagement.jpg b/public/images/infrastructureManagement.jpg new file mode 100644 index 0000000..7a099ef Binary files /dev/null and b/public/images/infrastructureManagement.jpg differ diff --git a/public/images/logAggregation.jpg b/public/images/logAggregation.jpg new file mode 100644 index 0000000..9648b57 Binary files /dev/null and b/public/images/logAggregation.jpg differ diff --git a/public/images/openSource.jpg b/public/images/openSource.jpg new file mode 100644 index 0000000..0bae311 Binary files /dev/null and b/public/images/openSource.jpg differ diff --git a/public/images/real-timeMetrics.jpg b/public/images/real-timeMetrics.jpg new file mode 100644 index 0000000..802e916 Binary files /dev/null and b/public/images/real-timeMetrics.jpg differ diff --git a/public/images/serviceDiscovery.jpg b/public/images/serviceDiscovery.jpg new file mode 100644 index 0000000..9f92558 Binary files /dev/null and b/public/images/serviceDiscovery.jpg differ diff --git a/public/images/smartAlerts.jpg b/public/images/smartAlerts.jpg new file mode 100644 index 0000000..099e9ce Binary files /dev/null and b/public/images/smartAlerts.jpg differ diff --git a/public/images/unifiedMonitoring.jpg b/public/images/unifiedMonitoring.jpg new file mode 100644 index 0000000..8a30953 Binary files /dev/null and b/public/images/unifiedMonitoring.jpg differ diff --git a/styles/globals.css b/styles/globals.css index 23d6d54..d87e512 100644 --- a/styles/globals.css +++ b/styles/globals.css @@ -1,3 +1,5 @@ +@import url('https://fonts.googleapis.com/css2?family=Playfair+Display:wght@400;500;600;700&family=Source+Sans+3:wght@400;500;600&display=swap'); + @tailwind base; @tailwind components; @tailwind utilities; @@ -144,4 +146,4 @@ padding: 0 0.75rem !important; max-width: 100% !important; } -} \ No newline at end of file +} diff --git a/tailwind.config.js b/tailwind.config.js index 748c75e..808a46c 100644 --- a/tailwind.config.js +++ b/tailwind.config.js @@ -11,6 +11,8 @@ module.exports = { fontFamily: { 'inter': ['Inter', 'sans-serif'], 'sans': ['Inter', 'sans-serif'], + 'playfair': ['"Playfair Display"', 'serif'], + 'source-sans': ['"Source Sans 3"', 'sans-serif'], }, colors: { primary: {