From 13f9a152814d6519821ce92e0139539afe25a1ef Mon Sep 17 00:00:00 2001 From: GitHub Actions Date: Mon, 16 Mar 2026 20:12:43 -0500 Subject: [PATCH 01/19] refactor(video-player): rewrite with robust autoplay and poster fallback - Replace loading spinner with poster image overlay that remains visible until video plays - Add useRef for video element and useCallback-based attemptPlay logic - Handle autoplay-blocked browsers with retry and graceful fallback - Add opacity transition for smooth reveal when video starts playing - Add playsInline attribute for mobile compatibility --- src/components/video-player.tsx | 75 +++++++++++++++++++++++++++------ 1 file changed, 61 insertions(+), 14 deletions(-) diff --git a/src/components/video-player.tsx b/src/components/video-player.tsx index 1a55a99..7392768 100644 --- a/src/components/video-player.tsx +++ b/src/components/video-player.tsx @@ -1,5 +1,5 @@ 'use client' -import React, { useState } from 'react' +import React, { useCallback, useEffect, useRef, useState } from 'react' interface VideoPlayerProps { mp4Src: string @@ -8,31 +8,78 @@ interface VideoPlayerProps { } const VideoPlayer: React.FC = ({ mp4Src, webmSrc, posterSrc }) => { - const [isLoading, setIsLoading] = useState(true) + const videoRef = useRef(null) + const [isPlaying, setIsPlaying] = useState(false) - const handleLoadedData = () => { - setIsLoading(false) - } + const attemptPlay = useCallback(async () => { + const video = videoRef.current + if (!video) return + + try { + // Reset to beginning if needed and ensure muted (required for autoplay) + video.muted = true + await video.play() + setIsPlaying(true) + } catch { + // Autoplay was blocked — retry once after a short delay + setTimeout(async () => { + try { + if (video.paused) { + await video.play() + setIsPlaying(true) + } + } catch { + // Autoplay truly blocked (e.g. strict browser policy). + // The poster image remains visible as fallback. + } + }, 500) + } + }, []) + + useEffect(() => { + const video = videoRef.current + if (!video) return + + const onPlaying = () => setIsPlaying(true) + const onCanPlay = () => { + if (video.paused) attemptPlay() + } + + video.addEventListener('playing', onPlaying) + video.addEventListener('canplay', onCanPlay) + + // If the video is already ready (cached), kick-start it immediately + if (video.readyState >= 3) { + attemptPlay() + } + + return () => { + video.removeEventListener('playing', onPlaying) + video.removeEventListener('canplay', onCanPlay) + } + }, [attemptPlay]) return (
- {isLoading && ( -
-
-
- )} + {/* Poster/still image — always rendered underneath, visible until video plays */} + + + {/* Video element — layered on top, fades in once playing */}
) From 99b3b003395e5f7cd4c0bd9cae80c253e192b4a9 Mon Sep 17 00:00:00 2001 From: GitHub Actions Date: Mon, 16 Mar 2026 20:12:57 -0500 Subject: [PATCH 02/19] style(globals.css): add separator-3 style, fix separator display, and add new utility classes - Add display:block to separator and separator-2 pseudo-elements - Add separator-3 with light-colored gradient for dark backgrounds - Add margin-bottom to separator-2/3 containers - Clean up comments and quote style for tailwind import - Add new CSS classes for about page (counters, progress bars, team sections) --- src/assets/css/globals.css | 105 ++++++++++++++++++++++++++++++++++--- 1 file changed, 98 insertions(+), 7 deletions(-) diff --git a/src/assets/css/globals.css b/src/assets/css/globals.css index 8cdf192..c19b4af 100644 --- a/src/assets/css/globals.css +++ b/src/assets/css/globals.css @@ -1,4 +1,4 @@ -@import "tailwindcss"; +@import 'tailwindcss'; :root { --background: #ffffff; @@ -27,12 +27,12 @@ body { .header { height: 74px; position: relative; - transition: all 0.5s ease; /* Smooth transition for the position */ - box-shadow: none; /* Optional: Add shadow if desired */ + transition: all 0.5s ease; + box-shadow: none; display: flex; align-items: center; - padding: 0 20px; /* Optional: Add padding */ - z-index: 1000; /* Ensure the header is on top */ + padding: 0; + z-index: 1000; } .header.sticky { @@ -71,6 +71,7 @@ body { } .separator:after { + display: block; height: 1px; background: #d1d1d1; background: -moz-linear-gradient( @@ -126,11 +127,13 @@ body { .separator-2, .separator-3 { width: 100%; + margin-bottom: 10px; position: relative; height: 1px; } .separator-2:after { + display: block; height: 1px; background: #d1d1d1; background: -moz-linear-gradient( @@ -202,7 +205,65 @@ body { background: linear-gradient(to right, #666666 0%, #555555 35%, #444444 70%, #373737 100%); } +.separator-3:after { + display: block; + height: 1px; + background: #d1d1d1; + background: linear-gradient( + to right, + transparent 0%, + rgba(0, 0, 0, 0.12) 25%, + rgba(0, 0, 0, 0.18) 50%, + rgba(0, 0, 0, 0.12) 75%, + transparent 100% + ); + position: absolute; + bottom: -1px; + left: 0; + content: ''; + width: 100%; +} + /*shadow*/ +.shadow-narrow { + position: relative; + background-color: #fafafa; +} + +.shadow-narrow * { + position: relative; + z-index: 3; +} + +.shadow-narrow:before { + position: absolute; + left: 0; + height: 60%; + bottom: 0; + width: 100%; + content: ''; + background-color: #fafafa; + z-index: 2; +} + +.shadow-narrow:after { + content: ''; + position: absolute; + height: 50%; + width: 90%; + left: 50%; + bottom: 2px; + margin-left: -45%; + box-shadow: 0 5px 7px #999999; + z-index: 1; + border-radius: 10%; + transition: all 0.3s ease-in-out; +} + +.shadow-narrow:hover:after { + bottom: 10px; +} + .shadow-1, .shadow-1-narrow { position: relative; @@ -334,8 +395,9 @@ body { width: 0; height: 0; overflow: hidden; - background: url('/images/hero-slides/slide-1-iphone.jpg'), - url('/images/hero-slides/slide-2-macbook.webp'), url('/images/hero-slides/slide-2-bitcoin.jpg'); + background: + url('/images/hero-slides/slide-1-iphone.avif'), url('/images/hero-slides/slide-2-macbook.avif'), + url('/images/hero-slides/slide-2-bitcoin.avif'); } .carousel-button { @@ -385,3 +447,32 @@ body { position: absolute; right: 18px; } + +/* Portfolio modal animations */ +@keyframes fadeIn { + from { + opacity: 0; + } + to { + opacity: 1; + } +} + +@keyframes slideUp { + from { + opacity: 0; + transform: translateY(30px); + } + to { + opacity: 1; + transform: translateY(0); + } +} + +.animate-fadeIn { + animation: fadeIn 0.2s ease-out forwards; +} + +.animate-slideUp { + animation: slideUp 0.3s ease-out forwards; +} From 4d2178008ef6542338ff977eced94890119e7feb Mon Sep 17 00:00:00 2001 From: GitHub Actions Date: Mon, 16 Mar 2026 20:13:05 -0500 Subject: [PATCH 03/19] feat(about): add Counter, ProgressBar, and TeamMember components - Counter: animated number counter with intersection observer trigger - ProgressBar: animated skill progress bar with percentage display - TeamMember: team member card with photo, bio, skills, and contact links --- src/components/about/Counter.tsx | 57 +++++++++ src/components/about/ProgressBar.tsx | 71 +++++++++++ src/components/about/TeamMember.tsx | 168 +++++++++++++++++++++++++++ 3 files changed, 296 insertions(+) create mode 100644 src/components/about/Counter.tsx create mode 100644 src/components/about/ProgressBar.tsx create mode 100644 src/components/about/TeamMember.tsx diff --git a/src/components/about/Counter.tsx b/src/components/about/Counter.tsx new file mode 100644 index 0000000..b7a49c1 --- /dev/null +++ b/src/components/about/Counter.tsx @@ -0,0 +1,57 @@ +'use client' + +import { useEffect, useRef, useState } from 'react' + +interface CounterProps { + to: number + duration?: number + className?: string +} + +export default function Counter({ to, duration = 5000, className = '' }: CounterProps) { + const [count, setCount] = useState(0) + const ref = useRef(null) + const hasStarted = useRef(false) + + useEffect(() => { + const observer = new IntersectionObserver( + ([entry]) => { + if (entry.isIntersecting && !hasStarted.current) { + hasStarted.current = true + const startTime = performance.now() + + const animate = (currentTime: number) => { + const elapsed = currentTime - startTime + const progress = Math.min(elapsed / duration, 1) + // Ease-out + const eased = 1 - Math.pow(1 - progress, 3) + const current = Math.floor(eased * to) + setCount(current) + + if (progress < 1) { + requestAnimationFrame(animate) + } else { + setCount(to) + } + } + + requestAnimationFrame(animate) + observer.unobserve(entry.target) + } + }, + { threshold: 0.3 } + ) + + if (ref.current) { + observer.observe(ref.current) + } + + return () => observer.disconnect() + }, [to, duration]) + + return ( + + {count.toLocaleString()} + + ) +} diff --git a/src/components/about/ProgressBar.tsx b/src/components/about/ProgressBar.tsx new file mode 100644 index 0000000..d149069 --- /dev/null +++ b/src/components/about/ProgressBar.tsx @@ -0,0 +1,71 @@ +'use client' + +import { useEffect, useRef, useState } from 'react' + +const BAR_COLORS = [ + { bg: 'linear-gradient(to right, #09afdf, #0bc5d4)', shadow: 'rgba(9,175,223,0.25)' }, + { bg: 'linear-gradient(to right, #0d8fb8, #09afdf)', shadow: 'rgba(13,143,184,0.25)' }, + { bg: 'linear-gradient(to right, #0bc5d4, #14d4b8)', shadow: 'rgba(11,197,212,0.25)' }, + { bg: 'linear-gradient(to right, #077da3, #0d8fb8)', shadow: 'rgba(7,125,163,0.25)' }, + { bg: 'linear-gradient(to right, #09afdf, #3dc0c8)', shadow: 'rgba(9,175,223,0.25)' }, + { bg: 'linear-gradient(to right, #14d4b8, #0bc5d4)', shadow: 'rgba(20,212,184,0.25)' }, + { bg: 'linear-gradient(135deg, #09afdf, #c8b84d22)', shadow: 'rgba(9,175,223,0.2)' }, + { bg: 'linear-gradient(135deg, #0d8fb8, #c8b84d22)', shadow: 'rgba(13,143,184,0.2)' }, +] + +interface ProgressBarProps { + label: string + percentage: number + index?: number +} + +export default function ProgressBar({ label, percentage, index = 0 }: ProgressBarProps) { + const [width, setWidth] = useState(0) + const [isVisible, setIsVisible] = useState(false) + const ref = useRef(null) + const color = BAR_COLORS[index % BAR_COLORS.length] + + useEffect(() => { + const observer = new IntersectionObserver( + ([entry]) => { + if (entry.isIntersecting) { + setIsVisible(true) + setTimeout(() => { + setWidth(percentage) + }, 200) + observer.unobserve(entry.target) + } + }, + { threshold: 0.1 } + ) + + if (ref.current) { + observer.observe(ref.current) + } + + return () => observer.disconnect() + }, [percentage]) + + return ( +
+
+
0 ? `0 2px 6px ${color.shadow}` : 'none', + }} + > + + {label} + +
+
+
+ ) +} diff --git a/src/components/about/TeamMember.tsx b/src/components/about/TeamMember.tsx new file mode 100644 index 0000000..97d51c1 --- /dev/null +++ b/src/components/about/TeamMember.tsx @@ -0,0 +1,168 @@ +'use client' + +import Image from 'next/image' +import Link from 'next/link' +import ProgressBar from './ProgressBar' +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' +import { faEnvelope, faPhone, faDiamond, faUsers, faMoneyBill, faCode } from '@fortawesome/free-solid-svg-icons' +import { faGithub, faLinkedin, faDiscord } from '@fortawesome/free-brands-svg-icons' +import { useEffect, useRef, useState } from 'react' +import 'animate.css' + +interface Skill { + label: string + percentage: number +} + +interface ContactLink { + icon: string + label: string + href: string + iconPack?: 'solid' | 'brands' +} + +interface TeamMemberProps { + name: string + title: string + bio: string + imageSrc: string + imageAlt: string + skills: Skill[] + contactLinks: ContactLink[] + index?: number +} + +export default function TeamMember({ + name, + title, + bio, + imageSrc, + imageAlt, + skills, + contactLinks, + index = 0, +}: TeamMemberProps) { + const sectionRef = useRef(null) + const [isVisible, setIsVisible] = useState(false) + + useEffect(() => { + const observer = new IntersectionObserver( + ([entry]) => { + if (entry.isIntersecting) { + setIsVisible(true) + observer.disconnect() + } + }, + { threshold: 0.12 } + ) + if (sectionRef.current) observer.observe(sectionRef.current) + return () => observer.disconnect() + }, []) + + // Alternate animation combos per member + const imageAnims = ['zoomIn', 'fadeInLeft', 'flipInY', 'rotateIn'] + const nameAnims = ['fadeInDown', 'lightSpeedInRight', 'fadeInLeft', 'fadeInUp'] + const bioAnims = ['fadeIn', 'fadeInUp', 'fadeIn', 'fadeInUp'] + const btnAnims = ['bounceIn', 'fadeInUp', 'zoomIn', 'bounceIn'] + const skillAnims = ['fadeInRight', 'fadeInUp', 'fadeInRight', 'fadeInLeft'] + + const idx = index % 4 + + return ( +
+
+ {/* Photo */} + + + {/* Bio */} +
+

+ {name} - {title} +

+
+

+ {bio} +

+
+ {contactLinks.map((link) => { + const icon = + link.label === 'Email' + ? faEnvelope + : link.label === 'Call' + ? faPhone + : link.label === 'GitHub' + ? faGithub + : link.label === 'LinkedIn' + ? faLinkedin + : link.label === 'Discord' + ? faDiscord + : faEnvelope + return ( + + + {link.label} + + ) + })} +
+
+ + {/* Skills */} +
+
+ {skills.map((skill, idx) => ( + + ))} +
+
+
+ ) +} From 4fdb8cce0f4a64cc6695fdb791bff550e2fb671b Mon Sep 17 00:00:00 2001 From: GitHub Actions Date: Mon, 16 Mar 2026 20:13:13 -0500 Subject: [PATCH 04/19] feat(portfolio): add PortfolioCard and PortfolioModal components - PortfolioCard: project card with hover overlay and category display - PortfolioModal: lightbox-style modal for portfolio project details --- src/components/portfolio/PortfolioCard.tsx | 94 +++++++++++ src/components/portfolio/PortfolioModal.tsx | 172 ++++++++++++++++++++ 2 files changed, 266 insertions(+) create mode 100644 src/components/portfolio/PortfolioCard.tsx create mode 100644 src/components/portfolio/PortfolioModal.tsx diff --git a/src/components/portfolio/PortfolioCard.tsx b/src/components/portfolio/PortfolioCard.tsx new file mode 100644 index 0000000..454d2af --- /dev/null +++ b/src/components/portfolio/PortfolioCard.tsx @@ -0,0 +1,94 @@ +'use client' + +import React, { useEffect, useRef, useState } from 'react' +import Image from 'next/image' +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' +import { faArrowRight } from '@fortawesome/free-solid-svg-icons' +import 'animate.css' + +interface PortfolioCardProps { + imageSrc: string + imageAlt: string + title: string + description: string + onReadMore: () => void + animationEffect?: string + animationDelay?: number +} + +const PortfolioCard: React.FC = ({ + imageSrc, + imageAlt, + title, + description, + onReadMore, + animationEffect = 'fadeInUp', + animationDelay = 0, +}) => { + const cardRef = useRef(null) + const [isVisible, setIsVisible] = useState(false) + + useEffect(() => { + const observer = new IntersectionObserver( + ([entry]) => { + if (entry.isIntersecting) { + setIsVisible(true) + observer.disconnect() + } + }, + { threshold: 0.15 } + ) + if (cardRef.current) observer.observe(cardRef.current) + return () => observer.disconnect() + }, []) + + return ( +
+ {/* Image area */} +
+ {imageAlt} +
+ {/* Body */} +
+

{title}

+ {/* Separator */} +
+

{description}

+
+ +
+
+
+ ) +} + +export default PortfolioCard diff --git a/src/components/portfolio/PortfolioModal.tsx b/src/components/portfolio/PortfolioModal.tsx new file mode 100644 index 0000000..8b775d4 --- /dev/null +++ b/src/components/portfolio/PortfolioModal.tsx @@ -0,0 +1,172 @@ +'use client' + +import React, { useEffect, useRef, useState } from 'react' +import Image from 'next/image' +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' +import { faCheck, faTimes } from '@fortawesome/free-solid-svg-icons' +import { Raleway } from '@/app/fonts' + +interface AccordionItem { + title: string + body: string +} + +interface PortfolioModalProps { + isOpen: boolean + onClose: () => void + title: string + imageSrc: string + imageAlt: string + founded: string + clientSince: string + description: string + link?: { href: string; label: string } + projects: AccordionItem[] +} + +const PortfolioModal: React.FC = ({ + isOpen, + onClose, + title, + imageSrc, + imageAlt, + founded, + clientSince, + description, + link, + projects, +}) => { + const [openAccordion, setOpenAccordion] = useState(null) + const backdropRef = useRef(null) + + useEffect(() => { + if (isOpen) { + document.body.style.overflow = 'hidden' + setOpenAccordion(null) + } else { + document.body.style.overflow = '' + } + return () => { + document.body.style.overflow = '' + } + }, [isOpen]) + + useEffect(() => { + const handleEsc = (e: KeyboardEvent) => { + if (e.key === 'Escape') onClose() + } + if (isOpen) window.addEventListener('keydown', handleEsc) + return () => window.removeEventListener('keydown', handleEsc) + }, [isOpen, onClose]) + + if (!isOpen) return null + + const handleBackdropClick = (e: React.MouseEvent) => { + if (e.target === backdropRef.current) onClose() + } + + return ( +
+
+ {/* Modal Header */} +
+

{title}

+ +
+ + {/* Modal Body */} +
+ {/* Client Info Row */} +
+
+ {imageAlt} +
+
+

+ Founded - {founded} · Client Since - {clientSince} +

+

+ {link ? ( + <> + {description.split(link.label)[0]} + + {link.label} + + {description.split(link.label)[1]} + + ) : ( + description + )} +

+
+
+ + {/* Projects Accordion */} +

Projects

+
+ {projects.map((project, index) => ( +
+ +
+
+ {project.body} +
+
+
+ ))} +
+
+ + {/* Modal Footer */} +
+ +
+
+
+ ) +} + +export default PortfolioModal From d79dea538bb966f516d83f4b6e63cd464c91acf8 Mon Sep 17 00:00:00 2001 From: GitHub Actions Date: Mon, 16 Mar 2026 20:13:21 -0500 Subject: [PATCH 05/19] refactor(layout): add shared ContactSection banner component - Move contact banner from index-specific to layout for reuse across pages - CTA section with gradient background and contact/services links --- src/components/layout/contact-banner.tsx | 110 +++++++++++++++++++++++ 1 file changed, 110 insertions(+) create mode 100644 src/components/layout/contact-banner.tsx diff --git a/src/components/layout/contact-banner.tsx b/src/components/layout/contact-banner.tsx new file mode 100644 index 0000000..1d11ff4 --- /dev/null +++ b/src/components/layout/contact-banner.tsx @@ -0,0 +1,110 @@ +import React, { useState, useEffect } from 'react' +import { motion } from 'motion/react' +import { Raleway } from 'next/font/google' +import { useInView } from 'react-intersection-observer' + +const raleway = Raleway({ subsets: ['latin'] }) + +const ContactSection: React.FC = () => { + const [hasAnimated, setHasAnimated] = useState(false) + const [showLargeText, setShowLargeText] = useState(true) + const [isHovered, setIsHovered] = useState(false) + + const { ref, inView } = useInView({ + triggerOnce: true, + threshold: 0.5, + }) + + useEffect(() => { + if (inView && !hasAnimated) { + // Trigger the initial animation with a delay + const initialAnimationTimeout = setTimeout(() => { + setHasAnimated(true) + setShowLargeText(false) // Switch to smaller text + + // Revert back to the larger text after 2 seconds + const revertTimeout = setTimeout(() => { + setShowLargeText(true) + }, 2000) + + return () => clearTimeout(revertTimeout) + }, 500) + + return () => clearTimeout(initialAnimationTimeout) + } + }, [inView, hasAnimated]) + + const handleHoverStart = () => { + setIsHovered(true) + setShowLargeText(false) // Show smaller text on hover + } + + const handleHoverEnd = () => { + setIsHovered(false) + setShowLargeText(true) // Revert to larger text when hover ends + } + + return ( +
+ +
+ {/* Text Container */} +
+ {/* Large Text */} + + Reach out and get in touch with us today! + + + {/* Smaller Text */} + + Don't hesitate—seize the moment! + +
+ + {/* Contact Button */} + +
+
+
+ ) +} + +export default ContactSection From 917d7ff5b4602365574deb8342504eee6557bf59 Mon Sep 17 00:00:00 2001 From: GitHub Actions Date: Mon, 16 Mar 2026 20:13:29 -0500 Subject: [PATCH 06/19] feat(header): add built-in route loading progress bar and navigation improvements - Replace next-nprogress-bar with custom loading bar integrated into header - Add route change detection via usePathname to animate progress - Intercept internal link clicks to trigger loading animation - Add active link highlighting based on current pathname - Add new navigation links (Services, Portfolio, About, Contact) --- src/components/layout/header.tsx | 127 +++++++++++++++++++++++++------ 1 file changed, 103 insertions(+), 24 deletions(-) diff --git a/src/components/layout/header.tsx b/src/components/layout/header.tsx index c97fd29..2a99a19 100644 --- a/src/components/layout/header.tsx +++ b/src/components/layout/header.tsx @@ -1,16 +1,21 @@ 'use client' -import React, { useEffect, useState, useRef } from 'react' +import React, { useEffect, useState, useRef, useCallback } from 'react' import 'animate.css' import { Pacifico, Roboto } from '@/app/fonts' import Link from 'next/link' -import { AppProgressBar as ProgressBar } from 'next-nprogress-bar' +import { usePathname } from 'next/navigation' const StickyHeader: React.FC = () => { const [isSticky, setIsSticky] = useState(false) const [mobileMenuOpen, setMobileMenuOpen] = useState(false) + const [loadingProgress, setLoadingProgress] = useState(0) + const [isLoading, setIsLoading] = useState(false) const menuRef = useRef(null) const buttonRef = useRef(null) + const pathname = usePathname() + const prevPathRef = useRef(pathname) + const progressTimerRef = useRef | null>(null) const handleScroll = () => { const scrollTop = window.scrollY @@ -22,7 +27,6 @@ const StickyHeader: React.FC = () => { } const handleClickOutside = (event: MouseEvent) => { - // Check if click is outside the menu and hamburger button if ( menuRef.current && !menuRef.current.contains(event.target as Node) && @@ -33,6 +37,79 @@ const StickyHeader: React.FC = () => { } } + // Start loading animation on link click + const startLoading = useCallback(() => { + setIsLoading(true) + setLoadingProgress(0) + if (progressTimerRef.current) clearInterval(progressTimerRef.current) + + let progress = 0 + progressTimerRef.current = setInterval(() => { + progress += Math.random() * 12 + 3 + if (progress >= 85) { + progress = 85 // Stall at 85% until navigation completes + if (progressTimerRef.current) clearInterval(progressTimerRef.current) + } + setLoadingProgress(progress) + }, 100) + }, []) + + // Complete loading animation + const completeLoading = useCallback(() => { + if (progressTimerRef.current) clearInterval(progressTimerRef.current) + setLoadingProgress(100) + // Keep at 100% for a moment before hiding + setTimeout(() => { + setIsLoading(false) + setLoadingProgress(0) + }, 400) + }, []) + + // Detect route changes + useEffect(() => { + if (pathname !== prevPathRef.current) { + prevPathRef.current = pathname + completeLoading() + } + }, [pathname, completeLoading]) + + // Intercept link clicks to start loading bar + useEffect(() => { + const handleClick = (e: MouseEvent) => { + const target = e.target as HTMLElement + const anchor = target.closest('a') + if (anchor && anchor.href) { + const url = new URL(anchor.href, window.location.origin) + // Only trigger for internal navigation + if (url.origin === window.location.origin && url.pathname !== pathname) { + startLoading() + } + } + } + document.addEventListener('click', handleClick) + return () => document.removeEventListener('click', handleClick) + }, [pathname, startLoading]) + + // Show loading bar on initial page load with minimum display time + useEffect(() => { + setIsLoading(true) + setLoadingProgress(0) + let progress = 0 + const timer = setInterval(() => { + progress += Math.random() * 18 + 8 + if (progress >= 100) { + progress = 100 + clearInterval(timer) + setTimeout(() => { + setIsLoading(false) + setLoadingProgress(0) + }, 300) + } + setLoadingProgress(progress) + }, 80) + return () => clearInterval(timer) + }, []) + useEffect(() => { window.addEventListener('scroll', handleScroll) document.addEventListener('mousedown', handleClickOutside) @@ -47,7 +124,7 @@ const StickyHeader: React.FC = () => {
{/* Logo and Text Link to Home Page */} @@ -58,9 +135,7 @@ const StickyHeader: React.FC = () => {

CodeBuilder

 

Inc.

-
+
Software Engineering Solutions
@@ -80,19 +155,19 @@ const StickyHeader: React.FC = () => { About Services Portfolio Contact @@ -162,26 +237,26 @@ const StickyHeader: React.FC = () => { About Services - Portfolio - - + Contact - +
- {/* Loading Progress Bar */} -
- +
From 5d3bf5f090b68edbffc348252a3f04cedaae1c15 Mon Sep 17 00:00:00 2001 From: GitHub Actions Date: Mon, 16 Mar 2026 20:13:39 -0500 Subject: [PATCH 07/19] redesign(footer): dark theme with social links, contact info, and sitemap - Switch from light gray to dark gradient background - Add structured social media links (Facebook, Twitter, LinkedIn, Reddit, etc.) - Add contact info section with email, phone, and address - Add sitemap links column with all main pages - Use Next.js Link for internal navigation --- src/components/layout/footer.tsx | 246 +++++++++++++++---------------- 1 file changed, 116 insertions(+), 130 deletions(-) diff --git a/src/components/layout/footer.tsx b/src/components/layout/footer.tsx index 225caa6..a95d3f4 100644 --- a/src/components/layout/footer.tsx +++ b/src/components/layout/footer.tsx @@ -1,155 +1,141 @@ import React from 'react' +import Link from 'next/link' import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' -import { - faTwitter, - faFacebookSquare, - faDribbble, - faGithub, -} from '@fortawesome/free-brands-svg-icons' +import { faFacebook, faTwitter, faGooglePlus, faSkype, faLinkedin, faReddit } from '@fortawesome/free-brands-svg-icons' +import { faEnvelope, faPhone, faMapMarkerAlt } from '@fortawesome/free-solid-svg-icons' const Footer: React.FC = () => { + const socialLinks = [ + { icon: faFacebook, href: 'http://www.facebook.com/codebuilder.us', title: 'Facebook', color: '#4267B2' }, + { icon: faTwitter, href: 'http://www.twitter.com/codebuilderio', title: 'Twitter', color: '#1DA1F2' }, + { + icon: faGooglePlus, + href: 'https://plus.google.com/u/1/108752322274477001531', + title: 'Google+', + color: '#E05E53', + }, + { icon: faSkype, href: 'skype:andrew.c.corbin', title: 'Skype', color: '#05ACF2' }, + { icon: faLinkedin, href: 'https://www.linkedin.com/company/25053333/', title: 'LinkedIn', color: '#0077B5' }, + { icon: faReddit, href: 'https://www.reddit.com/user/codebuilderus', title: 'Reddit', color: '#7FC2FE' }, + ] + return ( -