mirror of
https://github.com/harivansh-afk/React-Portfolio.git
synced 2026-04-15 09:01:17 +00:00
chore: Enhance app structure by removing routing and social icons components. Implement full-page scrolling with section navigation. Update package dependencies and add new project details to content options.
This commit is contained in:
parent
c73262065d
commit
4ffb2ffc07
20 changed files with 19963 additions and 1775 deletions
16889
package-lock.json
generated
Normal file
16889
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load diff
17
package.json
17
package.json
|
|
@ -3,16 +3,26 @@
|
||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@studio-freight/lenis": "^1.0.42",
|
||||||
|
"@types/node": "^22.13.10",
|
||||||
|
"@types/react": "^19.0.11",
|
||||||
"bootstrap": "^5.2.3",
|
"bootstrap": "^5.2.3",
|
||||||
|
"clsx": "^2.1.1",
|
||||||
"emailjs-com": "^3.2.0",
|
"emailjs-com": "^3.2.0",
|
||||||
|
"framer-motion": "^12.5.0",
|
||||||
"react": "^18.2.0",
|
"react": "^18.2.0",
|
||||||
"react-bootstrap": "^2.7.0",
|
"react-bootstrap": "^2.7.0",
|
||||||
"react-dom": "^18.2.0",
|
"react-dom": "^18.2.0",
|
||||||
"react-helmet-async": "^1.0.7",
|
"react-helmet-async": "^1.0.7",
|
||||||
"react-icons": "^4.1.0",
|
"react-icons": "^4.12.0",
|
||||||
|
"react-intersection-observer": "^9.16.0",
|
||||||
"react-router-dom": "^6.6.2",
|
"react-router-dom": "^6.6.2",
|
||||||
"react-scripts": "^5.0.1",
|
"react-scripts": "^5.0.1",
|
||||||
"react-transition-group": "^4.4.2",
|
"react-transition-group": "^4.4.2",
|
||||||
|
"remixicon": "^4.6.0",
|
||||||
|
"remixicon-react": "^1.0.0",
|
||||||
|
"tailwind-merge": "^3.0.2",
|
||||||
|
"typescript": "^5.8.2",
|
||||||
"typewriter-effect": "^2.18.2",
|
"typewriter-effect": "^2.18.2",
|
||||||
"web-vitals": "^3.1.1"
|
"web-vitals": "^3.1.1"
|
||||||
},
|
},
|
||||||
|
|
@ -41,5 +51,10 @@
|
||||||
"last 1 firefox version",
|
"last 1 firefox version",
|
||||||
"last 1 safari version"
|
"last 1 safari version"
|
||||||
]
|
]
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"autoprefixer": "^10.4.21",
|
||||||
|
"postcss": "^8.5.3",
|
||||||
|
"tailwindcss": "^4.0.14"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
BIN
public/images/project6.jpg
Normal file
BIN
public/images/project6.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 174 KiB |
448
smooth-scroll-implementation-plan.md
Normal file
448
smooth-scroll-implementation-plan.md
Normal file
|
|
@ -0,0 +1,448 @@
|
||||||
|
# Revised Smooth Scrolling Implementation Plan for React Portfolio
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
Based on the issues encountered with the previous implementation, this revised plan takes a simpler but more reliable approach to create a smooth scrolling portfolio with perfectly contained full-page sections. We'll implement a fullpage.js-inspired solution with proper section sizing, positioning, and transitions.
|
||||||
|
|
||||||
|
## Technologies
|
||||||
|
|
||||||
|
- **React Fullpage**: A simplified custom implementation inspired by fullpage.js
|
||||||
|
- **React Intersection Observer**: To detect when sections enter/exit the viewport
|
||||||
|
- **Framer Motion**: For smooth animations and transitions
|
||||||
|
|
||||||
|
## Implementation Steps
|
||||||
|
|
||||||
|
### 1. Setup Dependencies
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm install framer-motion react-intersection-observer
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Create FullPage Component
|
||||||
|
|
||||||
|
#### Create `src/components/FullPage.js`
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
import React, { useState, useEffect, useRef } from 'react';
|
||||||
|
import { motion } from 'framer-motion';
|
||||||
|
|
||||||
|
const FullPage = ({ children }) => {
|
||||||
|
const [currentPage, setCurrentPage] = useState(0);
|
||||||
|
const [isScrolling, setIsScrolling] = useState(false);
|
||||||
|
const containerRef = useRef(null);
|
||||||
|
const pagesCount = React.Children.count(children);
|
||||||
|
|
||||||
|
// Handle page change
|
||||||
|
const goToPage = (pageIndex) => {
|
||||||
|
if (pageIndex >= 0 && pageIndex < pagesCount && !isScrolling) {
|
||||||
|
setIsScrolling(true);
|
||||||
|
setCurrentPage(pageIndex);
|
||||||
|
|
||||||
|
// Get the corresponding section ID to update URL
|
||||||
|
const sectionId = React.Children.toArray(children)[pageIndex].props.id;
|
||||||
|
window.history.replaceState(null, null, `#${sectionId}`);
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
setIsScrolling(false);
|
||||||
|
}, 1000); // Match this with your transition duration
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Handle wheel events
|
||||||
|
const handleWheel = (e) => {
|
||||||
|
if (isScrolling) return;
|
||||||
|
|
||||||
|
if (e.deltaY > 0) {
|
||||||
|
// Scrolling down
|
||||||
|
goToPage(currentPage + 1);
|
||||||
|
} else {
|
||||||
|
// Scrolling up
|
||||||
|
goToPage(currentPage - 1);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Handle key events
|
||||||
|
const handleKeyDown = (e) => {
|
||||||
|
if (isScrolling) return;
|
||||||
|
|
||||||
|
if (e.key === 'ArrowDown' || e.key === 'PageDown') {
|
||||||
|
goToPage(currentPage + 1);
|
||||||
|
} else if (e.key === 'ArrowUp' || e.key === 'PageUp') {
|
||||||
|
goToPage(currentPage - 1);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Initialize event listeners
|
||||||
|
useEffect(() => {
|
||||||
|
const container = containerRef.current;
|
||||||
|
if (container) {
|
||||||
|
container.addEventListener('wheel', handleWheel, { passive: false });
|
||||||
|
window.addEventListener('keydown', handleKeyDown);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
container.removeEventListener('wheel', handleWheel);
|
||||||
|
window.removeEventListener('keydown', handleKeyDown);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}, [currentPage, isScrolling]);
|
||||||
|
|
||||||
|
// Check for hash in URL on initial load
|
||||||
|
useEffect(() => {
|
||||||
|
const hash = window.location.hash.substring(1);
|
||||||
|
if (hash) {
|
||||||
|
const childrenArray = React.Children.toArray(children);
|
||||||
|
const initialPageIndex = childrenArray.findIndex(child => child.props.id === hash);
|
||||||
|
if (initialPageIndex !== -1) {
|
||||||
|
setCurrentPage(initialPageIndex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
ref={containerRef}
|
||||||
|
className="fullpage-container"
|
||||||
|
style={{ overflow: 'hidden', height: '100vh', position: 'relative' }}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className="fullpage-sections"
|
||||||
|
style={{
|
||||||
|
height: '100%',
|
||||||
|
width: '100%',
|
||||||
|
transition: 'transform 1s cubic-bezier(0.645, 0.045, 0.355, 1.000)',
|
||||||
|
transform: `translateY(-${currentPage * 100}vh)`
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{React.Children.map(children, (child, index) => (
|
||||||
|
<div
|
||||||
|
key={index}
|
||||||
|
className="fullpage-section"
|
||||||
|
style={{ height: '100vh', width: '100%' }}
|
||||||
|
>
|
||||||
|
{child}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Pagination Dots */}
|
||||||
|
<div className="fullpage-pagination">
|
||||||
|
{Array.from({ length: pagesCount }).map((_, index) => (
|
||||||
|
<motion.div
|
||||||
|
key={index}
|
||||||
|
className={`pagination-dot ${currentPage === index ? 'active' : ''}`}
|
||||||
|
initial={{ opacity: 0.5, scale: 0.8 }}
|
||||||
|
animate={{
|
||||||
|
opacity: currentPage === index ? 1 : 0.5,
|
||||||
|
scale: currentPage === index ? 1 : 0.8
|
||||||
|
}}
|
||||||
|
onClick={() => goToPage(index)}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default FullPage;
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. Create Section Component
|
||||||
|
|
||||||
|
#### Create `src/components/Section.js`
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
import React from 'react';
|
||||||
|
import { motion } from 'framer-motion';
|
||||||
|
|
||||||
|
const Section = ({ children, id, className = '' }) => {
|
||||||
|
return (
|
||||||
|
<motion.section
|
||||||
|
id={id}
|
||||||
|
className={`fullpage-section-content ${className}`}
|
||||||
|
initial={{ opacity: 0 }}
|
||||||
|
animate={{ opacity: 1 }}
|
||||||
|
exit={{ opacity: 0 }}
|
||||||
|
transition={{ duration: 0.5 }}
|
||||||
|
style={{
|
||||||
|
height: '100%',
|
||||||
|
width: '100%',
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
position: 'relative'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</motion.section>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Section;
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. Update Navigation Component
|
||||||
|
|
||||||
|
#### Enhance Header Component
|
||||||
|
|
||||||
|
Update the header to work with our new FullPage component by handling section navigation.
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// In src/header/index.js
|
||||||
|
|
||||||
|
import React, { useState, useContext } from 'react';
|
||||||
|
import "./style.css";
|
||||||
|
import { RiMenuLine, RiCloseLine } from "react-icons/ri";
|
||||||
|
import { logotext } from "../content_option";
|
||||||
|
import Themetoggle from "../components/themetoggle";
|
||||||
|
import { Socialicons } from "../components/socialicons";
|
||||||
|
|
||||||
|
const Headermain = () => {
|
||||||
|
const [isActive, setActive] = useState("false");
|
||||||
|
|
||||||
|
const handleToggle = () => {
|
||||||
|
setActive(!isActive);
|
||||||
|
document.body.classList.toggle("ovhidden");
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleNavClick = (e, targetId) => {
|
||||||
|
e.preventDefault();
|
||||||
|
handleToggle();
|
||||||
|
|
||||||
|
// Find all section elements
|
||||||
|
const sections = document.querySelectorAll('.fullpage-section');
|
||||||
|
const targetSection = Array.from(sections).findIndex(
|
||||||
|
section => section.querySelector(`#${targetId}`) !== null
|
||||||
|
);
|
||||||
|
|
||||||
|
// Trigger a custom event that FullPage component will listen for
|
||||||
|
if (targetSection !== -1) {
|
||||||
|
const customEvent = new CustomEvent('navigateTo', {
|
||||||
|
detail: { index: targetSection }
|
||||||
|
});
|
||||||
|
document.dispatchEvent(customEvent);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// The rest of the component remains the same
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5. Update App.js
|
||||||
|
|
||||||
|
Rewrite App.js to use our new FullPage and Section components.
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
import React from "react";
|
||||||
|
import "bootstrap/dist/css/bootstrap.min.css";
|
||||||
|
import AnimatedCursor from "../hooks/AnimatedCursor";
|
||||||
|
import Headermain from "../header";
|
||||||
|
import { Home } from "../pages/home";
|
||||||
|
import { About } from "../pages/about";
|
||||||
|
import { Portfolio } from "../pages/portfolio";
|
||||||
|
import { ContactUs } from "../pages/contact";
|
||||||
|
import { Socialicons } from "../components/socialicons";
|
||||||
|
import FullPage from "../components/FullPage";
|
||||||
|
import Section from "../components/Section";
|
||||||
|
import "./App.css";
|
||||||
|
|
||||||
|
export default function App() {
|
||||||
|
return (
|
||||||
|
<div className="app-container">
|
||||||
|
<div className="cursor__dot">
|
||||||
|
<AnimatedCursor
|
||||||
|
innerSize={15}
|
||||||
|
outerSize={15}
|
||||||
|
color="255, 255 ,255"
|
||||||
|
outerAlpha={0.4}
|
||||||
|
innerScale={0.7}
|
||||||
|
outerScale={5}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<Headermain />
|
||||||
|
<FullPage>
|
||||||
|
<Section id="home">
|
||||||
|
<Home />
|
||||||
|
</Section>
|
||||||
|
<Section id="about">
|
||||||
|
<About />
|
||||||
|
</Section>
|
||||||
|
<Section id="portfolio">
|
||||||
|
<Portfolio />
|
||||||
|
</Section>
|
||||||
|
<Section id="contact">
|
||||||
|
<ContactUs />
|
||||||
|
</Section>
|
||||||
|
</FullPage>
|
||||||
|
<Socialicons />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 6. Add Necessary CSS Styles
|
||||||
|
|
||||||
|
#### Update `src/app/App.css`
|
||||||
|
|
||||||
|
```css
|
||||||
|
/* Reset styles */
|
||||||
|
* {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
html, body {
|
||||||
|
overflow: hidden;
|
||||||
|
position: fixed;
|
||||||
|
height: 100%;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Full page styles */
|
||||||
|
.app-container {
|
||||||
|
position: relative;
|
||||||
|
width: 100%;
|
||||||
|
height: 100vh;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fullpage-container {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100vh;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fullpage-section {
|
||||||
|
position: relative;
|
||||||
|
width: 100%;
|
||||||
|
height: 100vh;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fullpage-section-content {
|
||||||
|
padding-top: 60px; /* Account for header */
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Navigation dots */
|
||||||
|
.fullpage-pagination {
|
||||||
|
position: fixed;
|
||||||
|
right: 20px;
|
||||||
|
top: 50%;
|
||||||
|
transform: translateY(-50%);
|
||||||
|
z-index: 100;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pagination-dot {
|
||||||
|
width: 10px;
|
||||||
|
height: 10px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background-color: var(--text-color, #fff);
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pagination-dot.active {
|
||||||
|
transform: scale(1.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Make sure header is above fullpage */
|
||||||
|
.site__header {
|
||||||
|
z-index: 1000;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Social icons positioning */
|
||||||
|
.stick_follow_icon {
|
||||||
|
position: fixed;
|
||||||
|
bottom: 0;
|
||||||
|
left: 0;
|
||||||
|
z-index: 1000;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Responsive styles */
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.fullpage-pagination {
|
||||||
|
right: 10px;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pagination-dot {
|
||||||
|
width: 8px;
|
||||||
|
height: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fullpage-section-content {
|
||||||
|
padding-top: 50px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 7. Modify FullPage Component to Listen for Navigation Events
|
||||||
|
|
||||||
|
Update the FullPage component to listen for custom navigation events from the header.
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// Add this useEffect in the FullPage component
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const handleNavigation = (e) => {
|
||||||
|
const { index } = e.detail;
|
||||||
|
goToPage(index);
|
||||||
|
};
|
||||||
|
|
||||||
|
document.addEventListener('navigateTo', handleNavigation);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
document.removeEventListener('navigateTo', handleNavigation);
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
```
|
||||||
|
|
||||||
|
## Implementation Timeline
|
||||||
|
|
||||||
|
1. **Setup (30 minutes)**:
|
||||||
|
- Install dependencies
|
||||||
|
- Create necessary files
|
||||||
|
|
||||||
|
2. **Core Components (2 hours)**:
|
||||||
|
- Implement FullPage component
|
||||||
|
- Implement Section component
|
||||||
|
- Setup basic navigation
|
||||||
|
|
||||||
|
3. **Styling and Refinement (1.5 hours)**:
|
||||||
|
- Implement CSS styles
|
||||||
|
- Add pagination dots
|
||||||
|
- Ensure responsive behavior
|
||||||
|
|
||||||
|
4. **Navigation Integration (1 hour)**:
|
||||||
|
- Connect header navigation with fullpage scrolling
|
||||||
|
- Implement URL hash updates
|
||||||
|
|
||||||
|
5. **Testing and Debugging (1 hour)**:
|
||||||
|
- Test on different browsers
|
||||||
|
- Ensure smooth scrolling on different devices
|
||||||
|
|
||||||
|
## Expected Results
|
||||||
|
|
||||||
|
- Clean, full-viewport sections that are perfectly contained
|
||||||
|
- Smooth transitions between sections
|
||||||
|
- Navigation dots for quick section access
|
||||||
|
- Header links that correctly navigate to sections
|
||||||
|
- URL hash updates for direct section access
|
||||||
|
- Keyboard navigation support
|
||||||
|
- Responsive design that works on all devices
|
||||||
|
|
||||||
|
## Future Enhancements
|
||||||
|
|
||||||
|
- Add touch swipe support for mobile devices
|
||||||
|
- Implement section-specific animations
|
||||||
|
- Add scroll progress indicator
|
||||||
|
- Create horizontal sliding sections for portfolio items
|
||||||
|
- Implement background parallax effects
|
||||||
157
src/app/App.css
157
src/app/App.css
|
|
@ -1,42 +1,139 @@
|
||||||
@media only screen and (max-width: 991px) {
|
/* Variables */
|
||||||
.s_c {
|
:root {
|
||||||
padding-top: 40px;
|
--bg-color-rgb: 33, 37, 41;
|
||||||
}
|
--primary-color: #ff014f;
|
||||||
}
|
}
|
||||||
|
|
||||||
.page-enter {
|
[data-theme="light"] {
|
||||||
transform: translateY(100%);
|
--bg-color-rgb: 255, 255, 255;
|
||||||
}
|
}
|
||||||
|
|
||||||
.page-enter-active {
|
/* Reset styles */
|
||||||
transform: translateY(0%);
|
* {
|
||||||
transition: all 400ms ease-out;
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
box-sizing: border-box;
|
||||||
}
|
}
|
||||||
|
|
||||||
.page-exit {
|
html, body {
|
||||||
transform: translateY(0%);
|
overflow: hidden;
|
||||||
position: absolute;
|
position: fixed;
|
||||||
left: 0;
|
height: 100%;
|
||||||
right: 0;
|
width: 100%;
|
||||||
top: 0;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.page-exit-active {
|
/* Full page styles */
|
||||||
position: absolute;
|
.app-container {
|
||||||
left: 0;
|
position: relative;
|
||||||
right: 0;
|
width: 100%;
|
||||||
top: 0;
|
height: 100vh;
|
||||||
transform: translateY(-130%);
|
overflow: hidden;
|
||||||
transition: all 400ms ease-out;
|
}
|
||||||
|
|
||||||
|
.fullpage-container {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100vh;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fullpage-section {
|
||||||
|
position: relative;
|
||||||
|
width: 100%;
|
||||||
|
height: 100vh;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fullpage-section-content {
|
||||||
|
padding-top: 80px; /* Increased to account for header with margin */
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
overflow-y: auto;
|
||||||
|
overflow-x: hidden;
|
||||||
|
-ms-overflow-style: none; /* Hide scrollbar IE and Edge */
|
||||||
|
scrollbar-width: none; /* Hide scrollbar Firefox */
|
||||||
|
}
|
||||||
|
|
||||||
|
.fullpage-section-content::-webkit-scrollbar {
|
||||||
|
display: none; /* Hide scrollbar Chrome, Safari, Opera */
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-scrollable-content {
|
||||||
|
width: 100%;
|
||||||
|
max-width: 1200px;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding-bottom: 40px; /* Add bottom padding for content */
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Navigation dots */
|
||||||
|
.fullpage-pagination {
|
||||||
|
position: fixed;
|
||||||
|
right: 20px;
|
||||||
|
top: 50%;
|
||||||
|
transform: translateY(-50%);
|
||||||
|
z-index: 100;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pagination-dot {
|
||||||
|
width: 10px;
|
||||||
|
height: 10px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background-color: var(--text-color, #fff);
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pagination-dot.active {
|
||||||
|
transform: scale(1.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Make sure header is above fullpage */
|
||||||
|
.site__header {
|
||||||
|
z-index: 1000;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Social icons positioning */
|
||||||
|
.stick_follow_icon {
|
||||||
|
position: fixed;
|
||||||
|
bottom: 0;
|
||||||
|
left: 0;
|
||||||
|
z-index: 1000;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Only allow scrolling on the active section */
|
||||||
|
.fullpage-section:not(.active-section) .fullpage-section-content {
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Responsive styles */
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.fullpage-pagination {
|
||||||
|
right: 10px;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pagination-dot {
|
||||||
|
width: 8px;
|
||||||
|
height: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fullpage-section-content {
|
||||||
|
padding-top: 50px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (min-width: 1400px) {
|
@media (min-width: 1400px) {
|
||||||
.container,
|
.container,
|
||||||
.container-lg,
|
.container-lg,
|
||||||
.container-md,
|
.container-md,
|
||||||
.container-sm,
|
.container-sm,
|
||||||
.container-xl,
|
.container-xl,
|
||||||
.container-xxl {
|
.container-xxl {
|
||||||
max-width: 1140px;
|
max-width: 1140px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,27 +1,18 @@
|
||||||
import React, { useEffect } from "react";
|
import React from "react";
|
||||||
import "bootstrap/dist/css/bootstrap.min.css";
|
import "bootstrap/dist/css/bootstrap.min.css";
|
||||||
import {
|
import AnimatedCursor from "../hooks/AnimatedCursor";
|
||||||
BrowserRouter as Router,
|
|
||||||
useLocation,
|
|
||||||
} from "react-router-dom";
|
|
||||||
import withRouter from "../hooks/withRouter";
|
|
||||||
import AppRoutes from "./routes";
|
|
||||||
import Headermain from "../header";
|
import Headermain from "../header";
|
||||||
import AnimatedCursor from "../hooks/AnimatedCursor";
|
import { Home } from "../pages/home";
|
||||||
|
import { About } from "../pages/about";
|
||||||
|
import { Portfolio } from "../pages/portfolio";
|
||||||
|
import { ContactUs } from "../pages/contact";
|
||||||
|
import FullPage from "../components/FullPage";
|
||||||
|
import Section from "../components/Section";
|
||||||
import "./App.css";
|
import "./App.css";
|
||||||
|
|
||||||
function _ScrollToTop(props) {
|
|
||||||
const { pathname } = useLocation();
|
|
||||||
useEffect(() => {
|
|
||||||
window.scrollTo(0, 0);
|
|
||||||
}, [pathname]);
|
|
||||||
return props.children;
|
|
||||||
}
|
|
||||||
const ScrollToTop = withRouter(_ScrollToTop);
|
|
||||||
|
|
||||||
export default function App() {
|
export default function App() {
|
||||||
return (
|
return (
|
||||||
<Router basename={process.env.PUBLIC_URL}>
|
<div className="app-container">
|
||||||
<div className="cursor__dot">
|
<div className="cursor__dot">
|
||||||
<AnimatedCursor
|
<AnimatedCursor
|
||||||
innerSize={15}
|
innerSize={15}
|
||||||
|
|
@ -32,10 +23,21 @@ export default function App() {
|
||||||
outerScale={5}
|
outerScale={5}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<ScrollToTop>
|
<Headermain />
|
||||||
<Headermain />
|
<FullPage>
|
||||||
<AppRoutes />
|
<Section id="home">
|
||||||
</ScrollToTop>
|
<Home />
|
||||||
</Router>
|
</Section>
|
||||||
|
<Section id="about">
|
||||||
|
<About />
|
||||||
|
</Section>
|
||||||
|
<Section id="portfolio">
|
||||||
|
<Portfolio />
|
||||||
|
</Section>
|
||||||
|
<Section id="contact">
|
||||||
|
<ContactUs />
|
||||||
|
</Section>
|
||||||
|
</FullPage>
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
67
src/app/layout.css
Normal file
67
src/app/layout.css
Normal file
|
|
@ -0,0 +1,67 @@
|
||||||
|
html, body {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
overflow-x: hidden;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
#root {
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
#root > div {
|
||||||
|
min-height: 100%;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.main-content {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
width: 100%;
|
||||||
|
padding-top: 60px; /* Account for fixed header */
|
||||||
|
}
|
||||||
|
|
||||||
|
.sections-container {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-fullscreen {
|
||||||
|
min-height: calc(100vh - 60px);
|
||||||
|
width: 100%;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
position: relative;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
#home {
|
||||||
|
height: calc(100vh - 60px);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
#home > div {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Responsive adjustments */
|
||||||
|
@media (max-width: 991px) {
|
||||||
|
.main-content {
|
||||||
|
padding-top: 40px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#home {
|
||||||
|
height: auto;
|
||||||
|
min-height: calc(100vh - 40px);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,42 +0,0 @@
|
||||||
import React from "react";
|
|
||||||
import { Route, Routes} from "react-router-dom";
|
|
||||||
import withRouter from "../hooks/withRouter"
|
|
||||||
import { Home } from "../pages/home";
|
|
||||||
import { Portfolio } from "../pages/portfolio";
|
|
||||||
import { ContactUs } from "../pages/contact";
|
|
||||||
import { About } from "../pages/about";
|
|
||||||
import { Socialicons } from "../components/socialicons";
|
|
||||||
import { CSSTransition, TransitionGroup } from "react-transition-group";
|
|
||||||
|
|
||||||
const AnimatedRoutes = withRouter(({ location }) => (
|
|
||||||
<TransitionGroup>
|
|
||||||
<CSSTransition
|
|
||||||
key={location.key}
|
|
||||||
timeout={{
|
|
||||||
enter: 400,
|
|
||||||
exit: 400,
|
|
||||||
}}
|
|
||||||
classNames="page"
|
|
||||||
unmountOnExit
|
|
||||||
>
|
|
||||||
<Routes location={location}>
|
|
||||||
<Route exact path="/" element={<Home />} />
|
|
||||||
<Route path="/about" element={<About />} />
|
|
||||||
<Route path="/portfolio" element={<Portfolio />} />
|
|
||||||
<Route path="/contact" element={<ContactUs />} />
|
|
||||||
<Route path="*" element={<Home />} />
|
|
||||||
</Routes>
|
|
||||||
</CSSTransition>
|
|
||||||
</TransitionGroup>
|
|
||||||
));
|
|
||||||
|
|
||||||
function AppRoutes() {
|
|
||||||
return (
|
|
||||||
<div className="s_c">
|
|
||||||
<AnimatedRoutes />
|
|
||||||
<Socialicons />
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default AppRoutes;
|
|
||||||
155
src/components/FullPage.js
Normal file
155
src/components/FullPage.js
Normal file
|
|
@ -0,0 +1,155 @@
|
||||||
|
import React, { useState, useEffect, useRef } from 'react';
|
||||||
|
import { motion } from 'framer-motion';
|
||||||
|
|
||||||
|
const FullPage = ({ children }) => {
|
||||||
|
const [currentPage, setCurrentPage] = useState(0);
|
||||||
|
const [isScrolling, setIsScrolling] = useState(false);
|
||||||
|
const containerRef = useRef(null);
|
||||||
|
const pagesCount = React.Children.count(children);
|
||||||
|
|
||||||
|
// Handle page change
|
||||||
|
const goToPage = (pageIndex) => {
|
||||||
|
if (pageIndex >= 0 && pageIndex < pagesCount && !isScrolling) {
|
||||||
|
setIsScrolling(true);
|
||||||
|
setCurrentPage(pageIndex);
|
||||||
|
|
||||||
|
// Get the corresponding section ID to update URL
|
||||||
|
const sectionId = React.Children.toArray(children)[pageIndex].props.id;
|
||||||
|
window.history.replaceState(null, null, `#${sectionId}`);
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
setIsScrolling(false);
|
||||||
|
}, 1000); // Match this with your transition duration
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Handle wheel events with scrolling detection for active section
|
||||||
|
const handleWheel = (e) => {
|
||||||
|
if (isScrolling) return;
|
||||||
|
|
||||||
|
// Get the active section content element
|
||||||
|
const activeSection = document.querySelector('.active-section .fullpage-section-content');
|
||||||
|
|
||||||
|
if (activeSection) {
|
||||||
|
const { scrollTop, scrollHeight, clientHeight } = activeSection;
|
||||||
|
|
||||||
|
// Check if we're at the top or bottom of the section's content
|
||||||
|
const isAtTop = scrollTop === 0;
|
||||||
|
const isAtBottom = Math.abs(scrollHeight - scrollTop - clientHeight) < 5; // Allow small margin of error
|
||||||
|
|
||||||
|
// Only prevent default and change pages if we're at the top or bottom of content
|
||||||
|
if ((e.deltaY > 0 && isAtBottom) || (e.deltaY < 0 && isAtTop)) {
|
||||||
|
e.preventDefault();
|
||||||
|
|
||||||
|
if (e.deltaY > 0) {
|
||||||
|
// Scrolling down at bottom of content
|
||||||
|
goToPage(currentPage + 1);
|
||||||
|
} else {
|
||||||
|
// Scrolling up at top of content
|
||||||
|
goToPage(currentPage - 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Handle key events
|
||||||
|
const handleKeyDown = (e) => {
|
||||||
|
if (isScrolling) return;
|
||||||
|
|
||||||
|
if (e.key === 'ArrowDown' || e.key === 'PageDown') {
|
||||||
|
goToPage(currentPage + 1);
|
||||||
|
} else if (e.key === 'ArrowUp' || e.key === 'PageUp') {
|
||||||
|
goToPage(currentPage - 1);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Initialize event listeners
|
||||||
|
useEffect(() => {
|
||||||
|
const container = containerRef.current;
|
||||||
|
if (container) {
|
||||||
|
container.addEventListener('wheel', handleWheel, { passive: false });
|
||||||
|
window.addEventListener('keydown', handleKeyDown);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
container.removeEventListener('wheel', handleWheel);
|
||||||
|
window.removeEventListener('keydown', handleKeyDown);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}, [currentPage, isScrolling]);
|
||||||
|
|
||||||
|
// Check for hash in URL on initial load
|
||||||
|
useEffect(() => {
|
||||||
|
const hash = window.location.hash.substring(1);
|
||||||
|
if (hash) {
|
||||||
|
const childrenArray = React.Children.toArray(children);
|
||||||
|
const initialPageIndex = childrenArray.findIndex(child => child.props.id === hash);
|
||||||
|
if (initialPageIndex !== -1) {
|
||||||
|
setCurrentPage(initialPageIndex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// Listen for navigation events from header
|
||||||
|
useEffect(() => {
|
||||||
|
const handleNavigation = (e) => {
|
||||||
|
const { index } = e.detail;
|
||||||
|
goToPage(index);
|
||||||
|
};
|
||||||
|
|
||||||
|
document.addEventListener('navigateTo', handleNavigation);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
document.removeEventListener('navigateTo', handleNavigation);
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
ref={containerRef}
|
||||||
|
className="fullpage-container"
|
||||||
|
style={{ overflow: 'hidden', height: '100vh', position: 'relative' }}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className="fullpage-sections"
|
||||||
|
style={{
|
||||||
|
height: '100%',
|
||||||
|
width: '100%',
|
||||||
|
transition: 'transform 1s cubic-bezier(0.645, 0.045, 0.355, 1.000)',
|
||||||
|
transform: `translateY(-${currentPage * 100}vh)`
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{React.Children.map(children, (child, index) => {
|
||||||
|
// Add a class to the current page to allow scrolling only on active page
|
||||||
|
const isCurrentPage = index === currentPage;
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
key={index}
|
||||||
|
className={`fullpage-section ${isCurrentPage ? 'active-section' : ''}`}
|
||||||
|
style={{ height: '100vh', width: '100%' }}
|
||||||
|
>
|
||||||
|
{child}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Pagination Dots */}
|
||||||
|
<div className="fullpage-pagination">
|
||||||
|
{Array.from({ length: pagesCount }).map((_, index) => (
|
||||||
|
<motion.div
|
||||||
|
key={index}
|
||||||
|
className={`pagination-dot ${currentPage === index ? 'active' : ''}`}
|
||||||
|
initial={{ opacity: 0.5, scale: 0.8 }}
|
||||||
|
animate={{
|
||||||
|
opacity: currentPage === index ? 1 : 0.5,
|
||||||
|
scale: currentPage === index ? 1 : 0.8
|
||||||
|
}}
|
||||||
|
onClick={() => goToPage(index)}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default FullPage;
|
||||||
74
src/components/ScrollProgress.js
Normal file
74
src/components/ScrollProgress.js
Normal file
|
|
@ -0,0 +1,74 @@
|
||||||
|
import React, { useEffect, useState, useMemo } from 'react';
|
||||||
|
import { useSmoothScroll } from '../hooks/useSmoothScroll';
|
||||||
|
import { motion } from 'framer-motion';
|
||||||
|
|
||||||
|
const ScrollProgress = () => {
|
||||||
|
const [activeSection, setActiveSection] = useState('home');
|
||||||
|
const [scrollProgress, setScrollProgress] = useState(0);
|
||||||
|
const { lenis } = useSmoothScroll();
|
||||||
|
const sections = useMemo(() => ['home', 'about', 'portfolio', 'contact'], []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!lenis) return;
|
||||||
|
|
||||||
|
const handleScroll = ({ scroll, limit }) => {
|
||||||
|
// Calculate overall scroll progress
|
||||||
|
const progress = scroll / limit;
|
||||||
|
setScrollProgress(progress);
|
||||||
|
|
||||||
|
// Determine active section
|
||||||
|
const viewportHeight = window.innerHeight;
|
||||||
|
const currentPosition = scroll + viewportHeight / 2;
|
||||||
|
|
||||||
|
sections.forEach(sectionId => {
|
||||||
|
const section = document.getElementById(sectionId);
|
||||||
|
if (!section) return;
|
||||||
|
|
||||||
|
const sectionTop = section.offsetTop;
|
||||||
|
const sectionHeight = section.offsetHeight;
|
||||||
|
|
||||||
|
if (
|
||||||
|
currentPosition >= sectionTop &&
|
||||||
|
currentPosition <= sectionTop + sectionHeight
|
||||||
|
) {
|
||||||
|
setActiveSection(sectionId);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
lenis.on('scroll', handleScroll);
|
||||||
|
return () => {
|
||||||
|
lenis.off('scroll', handleScroll);
|
||||||
|
};
|
||||||
|
}, [lenis, sections]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="scroll-progress-container">
|
||||||
|
<div className="scroll-indicators">
|
||||||
|
{sections.map((section) => (
|
||||||
|
<motion.div
|
||||||
|
key={section}
|
||||||
|
className={`scroll-indicator ${activeSection === section ? 'active' : ''}`}
|
||||||
|
animate={{
|
||||||
|
scale: activeSection === section ? 1.2 : 1,
|
||||||
|
opacity: activeSection === section ? 1 : 0.5
|
||||||
|
}}
|
||||||
|
transition={{ duration: 0.3 }}
|
||||||
|
onClick={() => {
|
||||||
|
lenis.scrollTo(`#${section}`, {
|
||||||
|
duration: 1.2,
|
||||||
|
easing: (t) => Math.min(1, 1.001 - Math.pow(2, -10 * t))
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
<motion.div
|
||||||
|
className="progress-bar"
|
||||||
|
style={{ scaleX: scrollProgress }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ScrollProgress;
|
||||||
30
src/components/Section.js
Normal file
30
src/components/Section.js
Normal file
|
|
@ -0,0 +1,30 @@
|
||||||
|
import React from 'react';
|
||||||
|
import { motion } from 'framer-motion';
|
||||||
|
|
||||||
|
const Section = ({ children, id, className = '' }) => {
|
||||||
|
return (
|
||||||
|
<motion.section
|
||||||
|
id={id}
|
||||||
|
className={`fullpage-section-content ${className}`}
|
||||||
|
initial={{ opacity: 0 }}
|
||||||
|
animate={{ opacity: 1 }}
|
||||||
|
exit={{ opacity: 0 }}
|
||||||
|
transition={{ duration: 0.5 }}
|
||||||
|
style={{
|
||||||
|
height: '100%',
|
||||||
|
width: '100%',
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'flex-start',
|
||||||
|
justifyContent: 'center',
|
||||||
|
position: 'relative',
|
||||||
|
padding: '0 20px'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div className="section-scrollable-content">
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
</motion.section>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Section;
|
||||||
40
src/components/SectionWrapper.js
Normal file
40
src/components/SectionWrapper.js
Normal file
|
|
@ -0,0 +1,40 @@
|
||||||
|
import React, { useEffect } from 'react';
|
||||||
|
import { motion } from 'framer-motion';
|
||||||
|
import { useInView } from 'react-intersection-observer';
|
||||||
|
import { useSmoothScroll } from '../hooks/useSmoothScroll';
|
||||||
|
|
||||||
|
const SectionWrapper = ({ children, id, className = '' }) => {
|
||||||
|
const { lenis } = useSmoothScroll();
|
||||||
|
const { ref, inView } = useInView({
|
||||||
|
threshold: 0.4,
|
||||||
|
triggerOnce: false
|
||||||
|
});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (inView && lenis) {
|
||||||
|
// Update URL hash without scrolling
|
||||||
|
window.history.replaceState(null, null, `#${id}`);
|
||||||
|
}
|
||||||
|
}, [inView, id, lenis]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<motion.section
|
||||||
|
id={id}
|
||||||
|
ref={ref}
|
||||||
|
className={`section-fullscreen ${className} ${inView ? 'active' : ''}`}
|
||||||
|
initial={{ opacity: 0.5 }}
|
||||||
|
animate={{
|
||||||
|
opacity: inView ? 1 : 0.5,
|
||||||
|
scale: inView ? 1 : 0.98
|
||||||
|
}}
|
||||||
|
transition={{
|
||||||
|
duration: 0.8,
|
||||||
|
ease: [0.33, 1, 0.68, 1] // Cubic bezier for Apple-like easing
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</motion.section>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default SectionWrapper;
|
||||||
|
|
@ -1,49 +0,0 @@
|
||||||
import React from "react";
|
|
||||||
import "./style.css";
|
|
||||||
import {
|
|
||||||
FaGithub,
|
|
||||||
FaTwitter,
|
|
||||||
FaFacebookF,
|
|
||||||
FaLinkedin,
|
|
||||||
FaYoutube,
|
|
||||||
FaTwitch,
|
|
||||||
FaInstagram,
|
|
||||||
FaSnapchatGhost,
|
|
||||||
FaTiktok,
|
|
||||||
FaFileAlt
|
|
||||||
} from "react-icons/fa";
|
|
||||||
import { socialprofils } from "../../content_option";
|
|
||||||
|
|
||||||
const ICON_MAPPING = {
|
|
||||||
default: FaFileAlt,
|
|
||||||
facebook: FaFacebookF,
|
|
||||||
github: FaGithub,
|
|
||||||
instagram: FaInstagram,
|
|
||||||
linkedin: FaLinkedin,
|
|
||||||
snapchat: FaSnapchatGhost,
|
|
||||||
tiktok: FaTiktok,
|
|
||||||
twitter: FaTwitter,
|
|
||||||
twitch: FaTwitch,
|
|
||||||
youtube: FaYoutube,
|
|
||||||
resume: FaFileAlt
|
|
||||||
};
|
|
||||||
|
|
||||||
export const Socialicons = (params) => {
|
|
||||||
return (
|
|
||||||
<div className="stick_follow_icon">
|
|
||||||
<ul>
|
|
||||||
{Object.entries(socialprofils).map(([platform, url]) => {
|
|
||||||
const IconComponent = ICON_MAPPING[platform] || ICON_MAPPING.default;
|
|
||||||
return (
|
|
||||||
<li key={platform}>
|
|
||||||
<a href={url}>
|
|
||||||
<IconComponent />
|
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</ul>
|
|
||||||
<p>Follow Me</p>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
@ -1,101 +0,0 @@
|
||||||
.stick_follow_icon {
|
|
||||||
top: 50%;
|
|
||||||
left: 30px;
|
|
||||||
width: 20px;
|
|
||||||
height: 200px;
|
|
||||||
position: fixed;
|
|
||||||
margin-top: -100px;
|
|
||||||
z-index: 10;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-width: 1200px) {
|
|
||||||
.stick_follow_icon {
|
|
||||||
left: 15px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-width: 1100px) {
|
|
||||||
.stick_follow_icon {
|
|
||||||
left: 10px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.stick_follow_icon ul {
|
|
||||||
list-style: none;
|
|
||||||
padding: 0;
|
|
||||||
margin: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.stick_follow_icon svg {
|
|
||||||
width: 1.3em;
|
|
||||||
height: 1.3em;
|
|
||||||
fill: var(--text-color);
|
|
||||||
}
|
|
||||||
|
|
||||||
.stick_follow_icon p {
|
|
||||||
top: 70px;
|
|
||||||
left: -24px;
|
|
||||||
width: 68px;
|
|
||||||
height: 20px;
|
|
||||||
color: var(--text-color);
|
|
||||||
font-size: 12px;
|
|
||||||
font-weight: 600;
|
|
||||||
line-height: 1.2;
|
|
||||||
white-space: nowrap;
|
|
||||||
position: relative;
|
|
||||||
transform: rotate(-90deg);
|
|
||||||
}
|
|
||||||
|
|
||||||
.stick_follow_icon ul li {
|
|
||||||
display: block;
|
|
||||||
font-size: 12px;
|
|
||||||
text-align: center;
|
|
||||||
margin-bottom: 10px;
|
|
||||||
transition: all 0.3s;
|
|
||||||
}
|
|
||||||
|
|
||||||
.stick_follow_icon p:after {
|
|
||||||
top: 9px;
|
|
||||||
right: -48px;
|
|
||||||
width: 40px;
|
|
||||||
height: 1px;
|
|
||||||
content: "";
|
|
||||||
display: block;
|
|
||||||
position: absolute;
|
|
||||||
background-color: var(--text-color);
|
|
||||||
}
|
|
||||||
|
|
||||||
@media only screen and (max-width: 991px) {
|
|
||||||
.stick_follow_icon {
|
|
||||||
width: 100%;
|
|
||||||
height: auto;
|
|
||||||
position: static;
|
|
||||||
margin-top: 40px;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row-reverse;
|
|
||||||
justify-content: center;
|
|
||||||
padding: 20px 0;
|
|
||||||
align-items: center;
|
|
||||||
background: var(--background-color);
|
|
||||||
z-index: 1;
|
|
||||||
}
|
|
||||||
.stick_follow_icon p {
|
|
||||||
top: unset;
|
|
||||||
left: unset;
|
|
||||||
width: unset;
|
|
||||||
height: unset;
|
|
||||||
white-space: nowrap;
|
|
||||||
position: relative;
|
|
||||||
transform: unset;
|
|
||||||
font-size: 17px;
|
|
||||||
margin-right: 65px;
|
|
||||||
}
|
|
||||||
.stick_follow_icon ul {
|
|
||||||
margin-bottom: 20px;
|
|
||||||
}
|
|
||||||
.stick_follow_icon ul li {
|
|
||||||
display: inline;
|
|
||||||
margin-bottom: 29px;
|
|
||||||
margin-right: 10px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -126,6 +126,11 @@ const services = [{
|
||||||
];
|
];
|
||||||
|
|
||||||
const dataportfolio = [{
|
const dataportfolio = [{
|
||||||
|
img: "/images/project6.jpg",
|
||||||
|
description: "AI-Powered Real Estate Investment Analysis. Make data-driven property investment decisions with comprehensive analytics, risk assessment, and market insights powered by artificial intelligence.",
|
||||||
|
link: "https://estate-ai-eight.vercel.app/landing",
|
||||||
|
},
|
||||||
|
{
|
||||||
img: "/images/project5.jpg",
|
img: "/images/project5.jpg",
|
||||||
description: "Founded SolveX, a gamified algorithm practice platform featuring real-time battles, AI-powered problem generation, and competitive matchmaking",
|
description: "Founded SolveX, a gamified algorithm practice platform featuring real-time battles, AI-powered problem generation, and competitive matchmaking",
|
||||||
link: "https://solvex-pi.vercel.app/",
|
link: "https://solvex-pi.vercel.app/",
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,8 @@
|
||||||
import React, { useState } from "react";
|
import React, { useState } from "react";
|
||||||
import "./style.css";
|
import "./style.css";
|
||||||
import { RiMenuLine, RiCloseLine } from "react-icons/ri";
|
import { RiMenuLine, RiCloseLine } from "react-icons/ri";
|
||||||
import { Link } from "react-router-dom";
|
import { logotext } from "../content_option";
|
||||||
import { logotext ,socialprofils } from "../content_option";
|
|
||||||
import Themetoggle from "../components/themetoggle";
|
import Themetoggle from "../components/themetoggle";
|
||||||
import { Socialicons } from "../components/socialicons";
|
|
||||||
|
|
||||||
const Headermain = () => {
|
const Headermain = () => {
|
||||||
const [isActive, setActive] = useState("false");
|
const [isActive, setActive] = useState("false");
|
||||||
|
|
@ -14,18 +12,41 @@ const Headermain = () => {
|
||||||
document.body.classList.toggle("ovhidden");
|
document.body.classList.toggle("ovhidden");
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleNavClick = (e, targetId) => {
|
||||||
|
e.preventDefault();
|
||||||
|
handleToggle();
|
||||||
|
|
||||||
|
// Find all section elements
|
||||||
|
const sections = document.querySelectorAll('.fullpage-section');
|
||||||
|
const targetSection = Array.from(sections).findIndex(
|
||||||
|
section => section.querySelector(`#${targetId}`) !== null
|
||||||
|
);
|
||||||
|
|
||||||
|
// Trigger a custom event that FullPage component will listen for
|
||||||
|
if (targetSection !== -1) {
|
||||||
|
const customEvent = new CustomEvent('navigateTo', {
|
||||||
|
detail: { index: targetSection }
|
||||||
|
});
|
||||||
|
document.dispatchEvent(customEvent);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<header className="fixed-top site__header">
|
<header className="fixed-top site__header">
|
||||||
<div className="d-flex align-items-center justify-content-between">
|
<div className="d-flex align-items-center justify-content-between">
|
||||||
<Link className="navbar-brand nav_ac" to="/">
|
<a
|
||||||
|
className="navbar-brand nav_ac"
|
||||||
|
href="#home"
|
||||||
|
onClick={(e) => handleNavClick(e, 'home')}
|
||||||
|
>
|
||||||
{logotext}
|
{logotext}
|
||||||
</Link>
|
</a>
|
||||||
<div className="d-flex align-items-center">
|
<div className="d-flex align-items-center">
|
||||||
<Themetoggle />
|
<Themetoggle />
|
||||||
<button className="menu__button nav_ac" onClick={handleToggle}>
|
<button className="menu__button nav_ac" onClick={handleToggle}>
|
||||||
{!isActive ? <RiCloseLine /> : <RiMenuLine />}
|
{!isActive ? <RiCloseLine /> : <RiMenuLine />}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -34,34 +55,44 @@ const Headermain = () => {
|
||||||
<div className="menu__wrapper">
|
<div className="menu__wrapper">
|
||||||
<div className="menu__container p-3">
|
<div className="menu__container p-3">
|
||||||
<ul className="the_menu">
|
<ul className="the_menu">
|
||||||
<li className="menu_item ">
|
<li className="menu_item">
|
||||||
<Link onClick={handleToggle} to="/" className="my-3">Home</Link>
|
<a
|
||||||
|
href="#home"
|
||||||
|
className="my-3"
|
||||||
|
onClick={(e) => handleNavClick(e, 'home')}
|
||||||
|
>Home</a>
|
||||||
</li>
|
</li>
|
||||||
<li className="menu_item">
|
<li className="menu_item">
|
||||||
<Link onClick={handleToggle} to="/portfolio" className="my-3"> Portfolio</Link>
|
<a
|
||||||
|
href="#portfolio"
|
||||||
|
className="my-3"
|
||||||
|
onClick={(e) => handleNavClick(e, 'portfolio')}
|
||||||
|
>Portfolio</a>
|
||||||
</li>
|
</li>
|
||||||
<li className="menu_item">
|
<li className="menu_item">
|
||||||
<Link onClick={handleToggle} to="/about" className="my-3">About</Link>
|
<a
|
||||||
|
href="#about"
|
||||||
|
className="my-3"
|
||||||
|
onClick={(e) => handleNavClick(e, 'about')}
|
||||||
|
>About</a>
|
||||||
</li>
|
</li>
|
||||||
<li className="menu_item">
|
<li className="menu_item">
|
||||||
<Link onClick={handleToggle} to="/contact" className="my-3"> Contact</Link>
|
<a
|
||||||
|
href="#contact"
|
||||||
|
className="my-3"
|
||||||
|
onClick={(e) => handleNavClick(e, 'contact')}
|
||||||
|
>Contact</a>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="menu_footer d-flex flex-column flex-md-row justify-content-center align-items-md-center position-absolute w-100 p-3">
|
|
||||||
<div className="d-flex">
|
|
||||||
<Socialicons />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
<div className="br-top"></div>
|
<div className="br-top"></div>
|
||||||
<div className="br-bottom"></div>
|
<div className="br-bottom"></div>
|
||||||
<div className="br-left"></div>
|
<div className="br-left"></div>
|
||||||
<div className="br-right"></div>
|
<div className="br-right"></div>
|
||||||
|
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
||||||
47
src/hooks/useSmoothScroll.js
Normal file
47
src/hooks/useSmoothScroll.js
Normal file
|
|
@ -0,0 +1,47 @@
|
||||||
|
import { useEffect, useState, createContext, useContext } from 'react';
|
||||||
|
import Lenis from '@studio-freight/lenis';
|
||||||
|
|
||||||
|
const SmoothScrollContext = createContext({
|
||||||
|
lenis: null,
|
||||||
|
isReady: false
|
||||||
|
});
|
||||||
|
|
||||||
|
export const SmoothScrollProvider = ({ children }) => {
|
||||||
|
const [lenis, setLenis] = useState(null);
|
||||||
|
const [isReady, setIsReady] = useState(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const lenisInstance = new Lenis({
|
||||||
|
duration: 1.2,
|
||||||
|
easing: (t) => Math.min(1, 1.001 - Math.pow(2, -10 * t)),
|
||||||
|
direction: 'vertical',
|
||||||
|
gestureDirection: 'vertical',
|
||||||
|
smooth: true,
|
||||||
|
smoothTouch: false,
|
||||||
|
touchMultiplier: 2
|
||||||
|
});
|
||||||
|
|
||||||
|
function raf(time) {
|
||||||
|
lenisInstance.raf(time);
|
||||||
|
requestAnimationFrame(raf);
|
||||||
|
}
|
||||||
|
|
||||||
|
requestAnimationFrame(raf);
|
||||||
|
setLenis(lenisInstance);
|
||||||
|
setIsReady(true);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
lenisInstance.destroy();
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<SmoothScrollContext.Provider value={{ lenis, isReady }}>
|
||||||
|
{children}
|
||||||
|
</SmoothScrollContext.Provider>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useSmoothScroll = () => {
|
||||||
|
return useContext(SmoothScrollContext);
|
||||||
|
};
|
||||||
|
|
@ -3,7 +3,13 @@ import "./style.css";
|
||||||
import { Helmet, HelmetProvider } from "react-helmet-async";
|
import { Helmet, HelmetProvider } from "react-helmet-async";
|
||||||
import Typewriter from "typewriter-effect";
|
import Typewriter from "typewriter-effect";
|
||||||
import { introdata, meta } from "../../content_option";
|
import { introdata, meta } from "../../content_option";
|
||||||
import { Link } from "react-router-dom";
|
import {
|
||||||
|
RiGithubFill,
|
||||||
|
RiLinkedinFill,
|
||||||
|
RiFilePdfFill,
|
||||||
|
RiTwitterXFill
|
||||||
|
} from "react-icons/ri";
|
||||||
|
import { socialprofils } from "../../content_option";
|
||||||
|
|
||||||
export const Home = () => {
|
export const Home = () => {
|
||||||
const [needsScroll, setNeedsScroll] = useState(false);
|
const [needsScroll, setNeedsScroll] = useState(false);
|
||||||
|
|
@ -57,7 +63,7 @@ export const Home = () => {
|
||||||
style={{ backgroundImage: `url(/assets/images/new_headshot.png)` }}
|
style={{ backgroundImage: `url(/assets/images/new_headshot.png)` }}
|
||||||
></div>
|
></div>
|
||||||
<h2 className="mb-1x">{introdata.title}</h2>
|
<h2 className="mb-1x">{introdata.title}</h2>
|
||||||
<h1 className="fluidz-48 mb-1x">
|
<h1 className="fluidz-48 mb-3">
|
||||||
<Typewriter
|
<Typewriter
|
||||||
options={{
|
options={{
|
||||||
strings: [
|
strings: [
|
||||||
|
|
@ -71,24 +77,70 @@ export const Home = () => {
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</h1>
|
</h1>
|
||||||
|
<div className="social-icons mb-3">
|
||||||
|
<a
|
||||||
|
href={socialprofils.github}
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
className="icon-link"
|
||||||
|
aria-label="GitHub"
|
||||||
|
>
|
||||||
|
<RiGithubFill />
|
||||||
|
</a>
|
||||||
|
<a
|
||||||
|
href={socialprofils.linkedin}
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
className="icon-link"
|
||||||
|
aria-label="LinkedIn"
|
||||||
|
>
|
||||||
|
<RiLinkedinFill />
|
||||||
|
</a>
|
||||||
|
<a
|
||||||
|
href={socialprofils.resume}
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
className="icon-link"
|
||||||
|
aria-label="Resume"
|
||||||
|
>
|
||||||
|
<RiFilePdfFill />
|
||||||
|
</a>
|
||||||
|
<a
|
||||||
|
href="https://x.com/HarivanshRathi"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
className="icon-link"
|
||||||
|
aria-label="X (Twitter)"
|
||||||
|
>
|
||||||
|
<RiTwitterXFill />
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
<p className="mb-1x">{introdata.description}</p>
|
<p className="mb-1x">{introdata.description}</p>
|
||||||
<div className="intro_btn-action pb-5">
|
<div className="intro_btn-action pb-5">
|
||||||
<Link to="/portfolio" className="text_2">
|
<div className="buttons-container">
|
||||||
<div id="button_p" className="ac_btn btn ">
|
<a href="#portfolio" className="text_2" onClick={(e) => {
|
||||||
My Portfolio
|
e.preventDefault();
|
||||||
<div className="ring one"></div>
|
document.getElementById('portfolio').scrollIntoView({ behavior: 'smooth' });
|
||||||
<div className="ring two"></div>
|
}}>
|
||||||
<div className="ring three"></div>
|
<div id="button_p" className="ac_btn btn ">
|
||||||
</div>
|
My Portfolio
|
||||||
</Link>
|
<div className="ring one"></div>
|
||||||
<Link to="/contact">
|
<div className="ring two"></div>
|
||||||
<div id="button_h" className="ac_btn btn">
|
<div className="ring three"></div>
|
||||||
Contact Me
|
</div>
|
||||||
<div className="ring one"></div>
|
</a>
|
||||||
<div className="ring two"></div>
|
<a href="#contact" onClick={(e) => {
|
||||||
<div className="ring three"></div>
|
e.preventDefault();
|
||||||
</div>
|
document.getElementById('contact').scrollIntoView({ behavior: 'smooth' });
|
||||||
</Link>
|
}}>
|
||||||
|
<div id="button_h" className="ac_btn btn">
|
||||||
|
Contact Me
|
||||||
|
<div className="ring one"></div>
|
||||||
|
<div className="ring two"></div>
|
||||||
|
<div className="ring three"></div>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -90,6 +90,16 @@ section {
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 15px;
|
gap: 15px;
|
||||||
|
width: 100%;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.intro_btn-action .buttons-container {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
gap: 15px;
|
||||||
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.ac_btn {
|
.ac_btn {
|
||||||
|
|
@ -103,6 +113,9 @@ section {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
width: 100%;
|
||||||
|
max-width: 100%;
|
||||||
|
padding: 0 20px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.intro_sec .text p {
|
.intro_sec .text p {
|
||||||
|
|
@ -110,6 +123,11 @@ section {
|
||||||
max-width: 90%;
|
max-width: 90%;
|
||||||
margin: 0 auto 20px;
|
margin: 0 auto 20px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.social-icons {
|
||||||
|
justify-content: center;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Extra small devices (phones, 600px and down) */
|
/* Extra small devices (phones, 600px and down) */
|
||||||
|
|
@ -127,6 +145,11 @@ section {
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
padding: 10px 20px;
|
padding: 10px 20px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.ac_btn {
|
||||||
|
width: 180px; /* Slightly smaller on very small screens */
|
||||||
|
padding: 10px 20px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.intro_sec .text,
|
.intro_sec .text,
|
||||||
|
|
@ -330,4 +353,71 @@ section {
|
||||||
background-color: transparent;
|
background-color: transparent;
|
||||||
border: 0.5px solid var(--secondary-color);
|
border: 0.5px solid var(--secondary-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.intro_btn-action {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: flex-start;
|
||||||
|
gap: 15px;
|
||||||
|
width: 100%;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.intro_btn-action .buttons-container {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 15px;
|
||||||
|
align-items: flex-start;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ac_btn {
|
||||||
|
margin-right: 0; /* Remove default right margin */
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.social-icons {
|
||||||
|
display: flex;
|
||||||
|
gap: 12px;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: flex-start;
|
||||||
|
padding: 0;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.social-icons .icon-link {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 32px;
|
||||||
|
height: 32px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: rgba(var(--bg-color-rgb, 0, 0, 0), 0.1);
|
||||||
|
backdrop-filter: blur(10px);
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||||
|
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.social-icons .icon-link:hover {
|
||||||
|
background: rgba(var(--primary-color-rgb, 255, 1, 79), 0.1);
|
||||||
|
transform: translateY(-2px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.social-icons .icon-link svg {
|
||||||
|
width: 18px;
|
||||||
|
height: 18px;
|
||||||
|
fill: var(--text-color);
|
||||||
|
opacity: 0.8;
|
||||||
|
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.social-icons .icon-link:hover svg {
|
||||||
|
opacity: 1;
|
||||||
|
fill: var(--primary-color, #ff014f);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Mobile responsive adjustments */
|
||||||
|
@media (max-width: 991.98px) {
|
||||||
|
.social-icons {
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue