Skip to content

Commit 5b67657

Browse files
committed
feat: animations
1 parent 599c810 commit 5b67657

File tree

7 files changed

+241
-31
lines changed

7 files changed

+241
-31
lines changed

src/App.js

Lines changed: 12 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import AnimatedRoutes from './components/AnimatedRoutes';
55
import { ToastProvider } from './components/ToastProvider';
66
import ScrollToTop from './components/ScrollToTop';
77
import ContactModal from './components/ContactModal';
8+
import { AnimationProvider } from './context/AnimationContext'; // Import AnimationProvider
89

910
function App() {
1011
const [isModalOpen, setIsModalOpen] = useState(false);
@@ -19,15 +20,17 @@ function App() {
1920
};
2021

2122
return (
22-
<Router>
23-
<ScrollToTop />
24-
<ToastProvider>
25-
<Layout toggleModal={toggleModal} isSearchVisible={isSearchVisible} toggleSearch={toggleSearch}>
26-
<AnimatedRoutes />
27-
</Layout>
28-
<ContactModal isOpen={isModalOpen} onClose={toggleModal} />
29-
</ToastProvider>
30-
</Router>
23+
<AnimationProvider> {/* Wrap the entire app with AnimationProvider */}
24+
<Router>
25+
<ScrollToTop />
26+
<ToastProvider>
27+
<Layout toggleModal={toggleModal} isSearchVisible={isSearchVisible} toggleSearch={toggleSearch}>
28+
<AnimatedRoutes />
29+
</Layout>
30+
<ContactModal isOpen={isModalOpen} onClose={toggleModal} />
31+
</ToastProvider>
32+
</Router>
33+
</AnimationProvider>
3134
);
3235
}
3336

src/components/AnimatedRoutes.js

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ import PasswordGeneratorPage from '../pages/apps/PasswordGeneratorPage';
3737
import JsonFormatterPage from '../pages/apps/JsonFormatterPage';
3838
import ColorContrastCheckerPage from '../pages/apps/ColorContrastCheckerPage';
3939
import QrCodeGeneratorPage from '../pages/apps/QrCodeGeneratorPage';
40+
import SettingsPage from '../pages/SettingsPage'; // Import SettingsPage
4041

4142
import UsefulLinksPage from '../pages/UsefulLinksPage';
4243

@@ -176,6 +177,20 @@ function AnimatedRoutes() {
176177
</motion.div>
177178
}
178179
/>
180+
<Route
181+
path="/settings" // New route for SettingsPage
182+
element={
183+
<motion.div
184+
initial="initial"
185+
animate="in"
186+
exit="out"
187+
variants={pageVariants}
188+
transition={pageTransition}
189+
>
190+
<SettingsPage />
191+
</motion.div>
192+
}
193+
/>
179194
<Route
180195
path="/logs"
181196
element={

src/components/PostItem.js

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,11 @@
11
import React from 'react';
2-
import { Link } from 'react-router-dom';
2+
import { Link, useLocation } from 'react-router-dom'; // Import useLocation
3+
import { useAnimation } from '../context/AnimationContext'; // Import useAnimation
34

45
const PostItem = ({ slug, title, date, updatedDate, category, series, seriesIndex, isSeries }) => {
6+
const { isAnimationEnabled, showAnimationsHomepage, showAnimationsInnerPages } = useAnimation(); // Use the animation context
7+
const location = useLocation(); // Get current location
8+
59
// Format the date to a shorter format: Month Day, Year
610
const formattedDate = new Date(date).toLocaleDateString('en-US', {
711
month: 'short',
@@ -54,10 +58,12 @@ const PostItem = ({ slug, title, date, updatedDate, category, series, seriesInde
5458
? 'group-hover:text-[var(--title-hover-dnd)]'
5559
: 'group-hover:text-[var(--title-hover-takes)]';
5660

61+
const shouldAnimate = isAnimationEnabled && ((location.pathname === '/' && showAnimationsHomepage) || (location.pathname !== '/' && showAnimationsInnerPages));
62+
5763
return (
5864
<Link
5965
to={isSeries ? `/blog/${slug}` : `/blog/${slug}`}
60-
className={`block p-8 my-4 border border-gray-700/50 rounded-lg shadow-lg cursor-pointer transition-colors group relative overflow-hidden ${postBackgroundColorClass} ${postHoverBackgroundColorClass} animated-grid-bg`}
66+
className={`block p-8 my-4 border border-gray-700/50 rounded-lg shadow-lg cursor-pointer transition-colors group relative overflow-hidden ${postBackgroundColorClass} ${postHoverBackgroundColorClass} ${shouldAnimate ? 'animated-grid-bg' : ''}`}
6167
>
6268
<article>
6369
<div className="flex items-center">
@@ -94,4 +100,4 @@ const PostItem = ({ slug, title, date, updatedDate, category, series, seriesInde
94100
);
95101
};
96102

97-
export default PostItem;
103+
export default PostItem;

src/components/ProjectCard.js

Lines changed: 24 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
import React, { useState, useEffect, useRef, useCallback } from 'react';
2-
import { Link } from 'react-router-dom';
2+
import { Link, useLocation } from 'react-router-dom'; // Import useLocation
33
import { FaExternalLinkAlt } from 'react-icons/fa';
4-
import Dot from './Dot'; // Import the Dot component
4+
import Dot from './Dot';
5+
import { useAnimation } from '../context/AnimationContext'; // Import useAnimation
56

67
const ProjectCard = ({ project, size = 1 }) => {
78
const colSpanClass =
@@ -10,31 +11,36 @@ const ProjectCard = ({ project, size = 1 }) => {
1011
const [dots, setDots] = useState([]);
1112
const cardRef = useRef(null);
1213
const dotIdRef = useRef(0);
14+
const { isAnimationEnabled, showAnimationsHomepage, showAnimationsInnerPages } = useAnimation(); // Use the animation context
15+
const location = useLocation(); // Get current location
1316

1417
const handleAnimationEnd = useCallback((id) => {
1518
setDots((prevDots) => prevDots.filter((dot) => dot.id !== id));
1619
}, []);
1720

1821
useEffect(() => {
19-
const spawnDot = () => {
20-
if (cardRef.current && dots.length < 20) {
21-
const cardRect = cardRef.current.getBoundingClientRect();
22-
const newDot = {
23-
id: dotIdRef.current++,
24-
size: Math.floor(Math.random() * 5) + 3,
25-
color: `hsl(0, ${Math.floor(Math.random() * 30) + 70}%, ${Math.floor(Math.random() * 20) + 60}%)`, // Tones of red
26-
initialX: Math.random() * cardRect.width,
27-
initialY: -5, // Start slightly off-screen top
28-
animationDuration: Math.random() * 3 + 2, // Duration between 2 and 5 seconds
29-
};
30-
setDots((prevDots) => [...prevDots, newDot]);
31-
}
32-
};
22+
let interval;
23+
if (isAnimationEnabled && ((location.pathname === '/' && showAnimationsHomepage) || (location.pathname !== '/' && showAnimationsInnerPages))) { // Only spawn dots if animations are enabled and on homepage or everywhere
24+
const spawnDot = () => {
25+
if (cardRef.current && dots.length < 10) {
26+
const cardRect = cardRef.current.getBoundingClientRect();
27+
const newDot = {
28+
id: dotIdRef.current++,
29+
size: Math.floor(Math.random() * 4) + 5, // Size between 5 and 8
30+
color: `hsl(0, ${Math.floor(Math.random() * 30) + 70}%, ${Math.floor(Math.random() * 20) + 60}%)`, // Tones of red
31+
initialX: Math.random() * cardRect.width,
32+
initialY: -5, // Start slightly off-screen top
33+
animationDuration: Math.random() * 3 + 2, // Duration between 2 and 5 seconds
34+
};
35+
setDots((prevDots) => [...prevDots, newDot]);
36+
}
37+
};
3338

34-
const interval = setInterval(spawnDot, 500); // Spawn a new dot every 0.5 seconds
39+
interval = setInterval(spawnDot, 500); // Spawn a new dot every 0.5 seconds
40+
}
3541

3642
return () => clearInterval(interval);
37-
}, [dots.length]);
43+
}, [dots.length, isAnimationEnabled, location.pathname, showAnimationsHomepage, showAnimationsInnerPages]);
3844

3945
return (
4046
<div

src/components/Sidebar.js

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import React, { useState, useEffect } from 'react';
22

3-
import { NavLink, Link, useLocation } from 'react-router-dom';
3+
import { NavLink, Link, useLocation, useNavigate } from 'react-router-dom'; // Import useNavigate
44

55
import { motion } from 'framer-motion';
66

@@ -27,11 +27,13 @@ import {
2727
EnvelopeSimpleIcon,
2828
RssIcon,
2929
SquaresFourIcon,
30+
GearSix, // Import GearSix icon
3031
} from '@phosphor-icons/react';
3132

3233
import Fez from './Fez';
3334

3435
import { version } from '../version';
36+
import { useAnimation } from '../context/AnimationContext'; // Import useAnimation
3537

3638
const Sidebar = ({ isOpen, toggleSidebar, toggleModal }) => {
3739
const [isMainOpen, setIsMainOpen] = useState(true);
@@ -41,6 +43,8 @@ const Sidebar = ({ isOpen, toggleSidebar, toggleModal }) => {
4143
const [isGamesOpen, setIsGamesOpen] = useState(false);
4244
const [isExternalLinksOpen, setIsExternalLinksOpen] = useState(false);
4345
const [allSectionsOpen, setAllSectionsOpen] = useState(true); // New state for collapse all
46+
const { isAnimationEnabled, toggleAnimation } = useAnimation(); // Use the animation context
47+
const navigate = useNavigate(); // Initialize useNavigate
4448

4549
const location = useLocation();
4650

@@ -62,6 +66,10 @@ const Sidebar = ({ isOpen, toggleSidebar, toggleModal }) => {
6266
setIsExternalLinksOpen(newState);
6367
};
6468

69+
const handleSettingsClick = () => {
70+
navigate('/settings');
71+
};
72+
6573
const getLinkClass = ({ isActive }) =>
6674
`flex items-center space-x-3 px-3 py-1 rounded-md transition-colors ${
6775
isActive
@@ -338,6 +346,13 @@ const Sidebar = ({ isOpen, toggleSidebar, toggleModal }) => {
338346
className={`ml-3 transition-transform ${allSectionsOpen ? 'transform rotate-180' : ''}`}
339347
/>
340348
</button>
349+
<button
350+
onClick={handleSettingsClick}
351+
className="flex items-center justify-center w-full text-sm font-normal tracking-wider mb-4 focus:outline-none bg-gray-800 text-white hover:bg-gray-700 rounded-md p-2 font-sans"
352+
>
353+
<span>Settings</span>
354+
<GearSix size={20} className="ml-3" />
355+
</button>
341356
<hr className="border-gray-700 my-4" />
342357

343358
<div className="flex space-x-2 font-sans">

src/context/AnimationContext.js

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
import React, { createContext, useContext, useState, useEffect } from 'react';
2+
3+
const AnimationContext = createContext();
4+
5+
export const AnimationProvider = ({ children }) => {
6+
// Initialize state from localStorage, or default to true
7+
const [isAnimationEnabled, setIsAnimationEnabled] = useState(() => {
8+
try {
9+
const storedValue = localStorage.getItem('isAnimationEnabled');
10+
return storedValue ? JSON.parse(storedValue) : true;
11+
} catch (error) {
12+
console.error("Error reading 'isAnimationEnabled' from localStorage", error);
13+
return true; // Default to true if localStorage is not accessible
14+
}
15+
});
16+
17+
const [showAnimationsHomepage, setShowAnimationsHomepage] = useState(() => {
18+
try {
19+
const storedValue = localStorage.getItem('showAnimationsHomepage');
20+
return storedValue ? JSON.parse(storedValue) : true; // Default to true
21+
} catch (error) {
22+
console.error("Error reading 'showAnimationsHomepage' from localStorage", error);
23+
return true; // Default to true if localStorage is not accessible
24+
}
25+
});
26+
27+
const [showAnimationsInnerPages, setShowAnimationsInnerPages] = useState(() => {
28+
try {
29+
const storedValue = localStorage.getItem('showAnimationsInnerPages');
30+
return storedValue ? JSON.parse(storedValue) : false; // Default to false
31+
} catch (error) {
32+
console.error("Error reading 'showAnimationsInnerPages' from localStorage", error);
33+
return false; // Default to false if localStorage is not accessible
34+
}
35+
});
36+
37+
// Update localStorage whenever isAnimationEnabled changes
38+
useEffect(() => {
39+
try {
40+
localStorage.setItem('isAnimationEnabled', JSON.stringify(isAnimationEnabled));
41+
} catch (error) {
42+
console.error("Error writing 'isAnimationEnabled' to localStorage", error);
43+
}
44+
}, [isAnimationEnabled]);
45+
46+
// Update localStorage whenever showAnimationsHomepage changes
47+
useEffect(() => {
48+
try {
49+
localStorage.setItem('showAnimationsHomepage', JSON.stringify(showAnimationsHomepage));
50+
} catch (error) {
51+
console.error("Error writing 'showAnimationsHomepage' to localStorage", error);
52+
}
53+
}, [showAnimationsHomepage]);
54+
55+
// Update localStorage whenever showAnimationsInnerPages changes
56+
useEffect(() => {
57+
try {
58+
localStorage.setItem('showAnimationsInnerPages', JSON.stringify(showAnimationsInnerPages));
59+
} catch (error) {
60+
console.error("Error writing 'showAnimationsInnerPages' to localStorage", error);
61+
}
62+
}, [showAnimationsInnerPages]);
63+
64+
const toggleAnimation = () => {
65+
setIsAnimationEnabled((prev) => !prev);
66+
};
67+
68+
const toggleShowAnimationsHomepage = () => {
69+
setShowAnimationsHomepage((prev) => !prev);
70+
};
71+
72+
const toggleShowAnimationsInnerPages = () => {
73+
setShowAnimationsInnerPages((prev) => !prev);
74+
};
75+
76+
return (
77+
<AnimationContext.Provider value={{ isAnimationEnabled, toggleAnimation, showAnimationsHomepage, toggleShowAnimationsHomepage, showAnimationsInnerPages, toggleShowAnimationsInnerPages }}>
78+
{children}
79+
</AnimationContext.Provider>
80+
);
81+
};
82+
83+
export const useAnimation = () => {
84+
return useContext(AnimationContext);
85+
};

src/pages/SettingsPage.js

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
import React from 'react';
2+
import { useAnimation } from '../context/AnimationContext';
3+
import usePageTitle from '../utils/usePageTitle';
4+
5+
const SettingsPage = () => {
6+
usePageTitle('Settings');
7+
const {
8+
isAnimationEnabled,
9+
toggleAnimation,
10+
showAnimationsHomepage,
11+
toggleShowAnimationsHomepage,
12+
showAnimationsInnerPages,
13+
toggleShowAnimationsInnerPages,
14+
} = useAnimation();
15+
16+
return (
17+
<div className="py-16 sm:py-24">
18+
<div className="mx-auto max-w-7xl px-6 lg:px-8">
19+
<div className="mx-auto max-w-2xl text-center">
20+
<h1 className="text-4xl font-semibold tracking-tight text-white sm:text-6xl">
21+
Settings
22+
</h1>
23+
<p className="mt-6 text-lg leading-8 text-gray-300">
24+
Manage your application preferences.
25+
</p>
26+
</div>
27+
28+
<div className="mt-16 max-w-xl mx-auto bg-gray-800/50 p-8 rounded-lg shadow-lg">
29+
<div className="flex items-center justify-between mb-6">
30+
<label htmlFor="enable-animations" className="text-white text-lg cursor-pointer">
31+
Enable Animations
32+
</label>
33+
<input
34+
type="checkbox"
35+
id="enable-animations"
36+
checked={isAnimationEnabled}
37+
onChange={toggleAnimation}
38+
className="toggle toggle-primary"
39+
/>
40+
</div>
41+
42+
<div className="flex items-center justify-between mb-6">
43+
<label htmlFor="show-animations-homepage" className="text-white text-lg cursor-pointer">
44+
Show animations in homepage
45+
</label>
46+
<input
47+
type="checkbox"
48+
id="show-animations-homepage"
49+
checked={showAnimationsHomepage}
50+
onChange={toggleShowAnimationsHomepage}
51+
className="toggle toggle-primary"
52+
disabled={!isAnimationEnabled} // Disable if animations are generally disabled
53+
/>
54+
</div>
55+
56+
<div className="flex items-center justify-between">
57+
<label htmlFor="show-animations-inner-pages" className="text-white text-lg cursor-pointer">
58+
Show animations in inner pages
59+
</label>
60+
<input
61+
type="checkbox"
62+
id="show-animations-inner-pages"
63+
checked={showAnimationsInnerPages}
64+
onChange={toggleShowAnimationsInnerPages}
65+
className="toggle toggle-primary"
66+
disabled={!isAnimationEnabled} // Disable if animations are generally disabled
67+
/>
68+
</div>
69+
{!isAnimationEnabled && (
70+
<p className="text-sm text-gray-400 mt-2">
71+
Animation options are disabled because "Enable Animations" is off.
72+
</p>
73+
)}
74+
</div>
75+
</div>
76+
</div>
77+
);
78+
};
79+
80+
export default SettingsPage;

0 commit comments

Comments
 (0)