portosaurus 2.0.2 → 2.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/portosaurus.mjs +14 -327
- package/package.json +16 -11
- package/src/cli/build.mjs +43 -0
- package/src/cli/dev.mjs +31 -0
- package/src/cli/init.mjs +135 -0
- package/src/cli/serve.mjs +30 -0
- package/src/core/buildDocuConfig.mjs +664 -0
- package/src/core/{themePlugin.mjs → plugins/themePlugin.mjs} +1 -1
- package/src/template/.github/workflows/deploy.yml +52 -0
- package/src/template/.nojekyll +0 -0
- package/src/template/README.md +58 -0
- package/src/template/blog/authors.yml +1 -1
- package/src/template/blog/welcome.md +1 -1
- package/src/template/config.js +40 -23
- package/src/template/package.json +20 -0
- package/src/template/static/img/svg/icon-blog.svg +2 -0
- package/src/template/static/img/svg/icon-note.svg +2 -0
- package/src/{components → theme/components}/AboutSection/index.js +22 -13
- package/src/{components → theme/components}/AboutSection/styles.module.css +59 -48
- package/src/{components → theme/components}/ContactSection/index.js +31 -24
- package/src/{components → theme/components}/ContactSection/styles.module.css +31 -26
- package/src/{components → theme/components}/ExperienceSection/index.js +12 -7
- package/src/{components → theme/components}/ExperienceSection/styles.module.css +23 -20
- package/src/{components → theme/components}/HeroSection/index.js +9 -11
- package/src/{components → theme/components}/HeroSection/styles.module.css +44 -32
- package/src/{components → theme/components}/NoteIndex/index.js +10 -3
- package/src/{components → theme/components}/Preview/components/PreviewHeader.js +14 -8
- package/src/{components → theme/components}/Preview/components/Triggers/Pv.js +32 -7
- package/src/{components → theme/components}/Preview/components/Triggers/SrcPv.js +1 -5
- package/src/theme/components/Preview/index.js +3 -0
- package/src/{components → theme/components}/ProjectsSection/index.js +279 -224
- package/src/{components → theme/components}/ProjectsSection/styles.module.css +21 -17
- package/src/{components → theme/components}/ScrollToTop/index.js +18 -21
- package/src/{components → theme/components}/ScrollToTop/styles.module.css +10 -9
- package/src/theme/components/SocialLinks/index.js +125 -0
- package/src/{components → theme/components}/SocialLinks/styles.module.css +9 -7
- package/src/{components → theme/components}/Tooltip/index.js +4 -1
- package/src/theme/config/iconMappings.js +465 -0
- package/src/theme/config/metaTags.js +239 -0
- package/src/theme/config/prism.js +179 -0
- package/src/theme/config/sidebar.js +17 -0
- package/src/{css → theme/css}/bootstrap.css +0 -1
- package/src/theme/css/catppuccin.css +618 -0
- package/src/{css → theme/css}/custom.css +3 -9
- package/src/{css → theme/css}/tasks.css +43 -37
- package/src/theme/{MDXComponents.js → overrides/MDXComponents.js} +3 -3
- package/src/theme/{Root.js → overrides/Root.js} +2 -4
- package/src/{pages → theme/pages}/index.js +23 -39
- package/src/theme/pages/notes.js +83 -0
- package/src/{pages → theme/pages}/tasks.js +115 -56
- package/src/{core/client-utils → theme/utils}/HashNavigation.js +60 -49
- package/src/{core/client-utils → theme/utils}/updateTitle.js +21 -25
- package/src/{core/build-utils → utils/build}/cssUtils.mjs +5 -3
- package/src/{core/build-utils → utils/build}/generateFavicon.mjs +44 -12
- package/src/{core/build-utils → utils/build}/generateRobotsTxt.mjs +4 -3
- package/src/{core/build-utils → utils/build}/iconExtractor.mjs +7 -3
- package/src/utils/build/imageDownloader.mjs +159 -0
- package/src/{core/build-utils → utils/build}/imageProcessor.mjs +5 -6
- package/src/utils/helpers.mjs +153 -0
- package/src/utils/logger.mjs +53 -0
- package/src/utils/packageManager.mjs +88 -0
- package/src/components/Preview/index.js +0 -3
- package/src/components/SocialLinks/index.js +0 -130
- package/src/config/iconMappings.js +0 -329
- package/src/config/metaTags.js +0 -240
- package/src/config/prism.js +0 -179
- package/src/config/sidebar.js +0 -20
- package/src/core/build-utils/imageDownloader.mjs +0 -98
- package/src/core/createDocuConf.mjs +0 -490
- package/src/core/defaults.mjs +0 -67
- package/src/core/logger.mjs +0 -17
- package/src/core/packageManager.mjs +0 -72
- package/src/css/catppuccin.css +0 -632
- package/src/pages/notes.js +0 -87
- /package/src/template/notes/{welcome.md → welcome.mdx} +0 -0
- /package/src/{components → theme/components}/NoteIndex/styles.module.css +0 -0
- /package/src/{components → theme/components}/Preview/components/FeedbackStates.js +0 -0
- /package/src/{components → theme/components}/Preview/components/FileTabs.js +0 -0
- /package/src/{components → theme/components}/Preview/components/Triggers/index.js +0 -0
- /package/src/{components → theme/components}/Preview/components/ViewerWindow.js +0 -0
- /package/src/{components → theme/components}/Preview/hooks/useDeepLinkHash.js +0 -0
- /package/src/{components → theme/components}/Preview/hooks/useDockLayout.js +0 -0
- /package/src/{components → theme/components}/Preview/hooks/useFileFetch.js +0 -0
- /package/src/{components → theme/components}/Preview/renderers/CodeRenderer.js +0 -0
- /package/src/{components → theme/components}/Preview/renderers/ImageRenderer.js +0 -0
- /package/src/{components → theme/components}/Preview/renderers/PdfRenderer.js +0 -0
- /package/src/{components → theme/components}/Preview/renderers/WebRenderer.js +0 -0
- /package/src/{components → theme/components}/Preview/state/index.js +0 -0
- /package/src/{components → theme/components}/Preview/styles.module.css +0 -0
- /package/src/{components → theme/components}/Preview/utils/index.js +0 -0
- /package/src/{components → theme/components}/Tooltip/styles.module.css +0 -0
|
@@ -1,8 +1,15 @@
|
|
|
1
|
-
import { useRef, useState, useEffect, useCallback, useMemo } from
|
|
1
|
+
import { useRef, useState, useEffect, useCallback, useMemo } from "react";
|
|
2
2
|
import Slider from "react-slick";
|
|
3
|
-
import {
|
|
3
|
+
import {
|
|
4
|
+
FaGithub,
|
|
5
|
+
FaGlobe,
|
|
6
|
+
FaPlay,
|
|
7
|
+
FaChevronLeft,
|
|
8
|
+
FaChevronRight,
|
|
9
|
+
FaStar,
|
|
10
|
+
} from "react-icons/fa";
|
|
4
11
|
import useDocusaurusContext from "@docusaurus/useDocusaurusContext";
|
|
5
|
-
import styles from
|
|
12
|
+
import styles from "./styles.module.css";
|
|
6
13
|
|
|
7
14
|
// Import slick carousel css
|
|
8
15
|
import "slick-carousel/slick/slick.css";
|
|
@@ -23,102 +30,119 @@ export default function ProjectsSection({ id, className, title, subtitle }) {
|
|
|
23
30
|
const activeDotRef = useRef(null);
|
|
24
31
|
const dotsContainerRef = useRef(null);
|
|
25
32
|
|
|
26
|
-
// Default Settings
|
|
33
|
+
// Default Settings
|
|
27
34
|
const projectDefaults = {
|
|
28
35
|
title: "Future Project",
|
|
29
|
-
desc:
|
|
36
|
+
desc: "Coming soon...",
|
|
30
37
|
image: "img/project-blank.png",
|
|
31
38
|
state: "active",
|
|
32
|
-
tags:
|
|
39
|
+
tags: ["planned"],
|
|
33
40
|
};
|
|
34
41
|
|
|
42
|
+
const createPlaceholders = useCallback(
|
|
43
|
+
(count, existingProjects) => {
|
|
44
|
+
if (existingProjects.length === 0) return [];
|
|
35
45
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
}, [projectDefaults]);
|
|
46
|
+
return [
|
|
47
|
+
...existingProjects,
|
|
48
|
+
...Array.from({ length: count }, (_, i) => ({
|
|
49
|
+
...projectDefaults,
|
|
50
|
+
|
|
51
|
+
// Dummy card config
|
|
52
|
+
state: "n/a",
|
|
53
|
+
title: `Project ${existingProjects.length + i + 1}`,
|
|
54
|
+
description: projectDefaults.desc,
|
|
55
|
+
image: projectDefaults.image,
|
|
56
|
+
id: `placeholder-${i}`,
|
|
57
|
+
tags: null,
|
|
58
|
+
})),
|
|
59
|
+
];
|
|
60
|
+
},
|
|
61
|
+
[projectDefaults],
|
|
62
|
+
);
|
|
54
63
|
|
|
55
64
|
// Get current slidesToShow based on screen width
|
|
56
65
|
const getVisibleSlidesPerView = useCallback(() => {
|
|
57
|
-
if (typeof window ===
|
|
58
|
-
|
|
66
|
+
if (typeof window === "undefined") return 3;
|
|
67
|
+
|
|
59
68
|
const width = window.innerWidth;
|
|
60
69
|
if (width <= 600) return 1;
|
|
61
70
|
if (width <= 1024) return 2;
|
|
62
71
|
return 3;
|
|
63
72
|
}, []);
|
|
64
|
-
|
|
65
|
-
const prepareProjects = useCallback(
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
73
|
+
|
|
74
|
+
const prepareProjects = useCallback(
|
|
75
|
+
(projectList, slides) => {
|
|
76
|
+
if (!projectList?.length) return { projects: [], totalPages: 0 };
|
|
77
|
+
|
|
78
|
+
// Sort featured first
|
|
79
|
+
const sortedProjects = [...projectList]
|
|
80
|
+
.sort((a, b) => (a.featured ? -1 : 0) - (b.featured ? -1 : 0))
|
|
81
|
+
.map((project) => {
|
|
82
|
+
// Apply defaults if value not null
|
|
83
|
+
const processedProject = {
|
|
84
|
+
...project,
|
|
85
|
+
description:
|
|
86
|
+
project.desc === undefined ? projectDefaults.desc : project.desc,
|
|
87
|
+
image:
|
|
88
|
+
project.image === undefined
|
|
89
|
+
? projectDefaults.image
|
|
90
|
+
: project.image,
|
|
91
|
+
tags:
|
|
92
|
+
project.tags === undefined
|
|
93
|
+
? [...projectDefaults.tags]
|
|
94
|
+
: project.tags,
|
|
95
|
+
state:
|
|
96
|
+
project.state === undefined
|
|
97
|
+
? projectDefaults.state
|
|
98
|
+
: project.state,
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
// Add ID
|
|
102
|
+
if (!processedProject.id) {
|
|
103
|
+
processedProject.id = processedProject.title
|
|
104
|
+
.toLowerCase()
|
|
105
|
+
.replace(/[^\w\s-]/g, "")
|
|
106
|
+
.replace(/\s+/g, "-")
|
|
107
|
+
.replace(/-+/g, "-");
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
return processedProject;
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
// Calculate pagination and placeholder needs
|
|
114
|
+
const totalPages = Math.ceil(sortedProjects.length / slides);
|
|
115
|
+
const slotsPerPage = slides;
|
|
116
|
+
const totalSlots = totalPages * slotsPerPage;
|
|
117
|
+
const placeholderCount = totalSlots - sortedProjects.length;
|
|
118
|
+
|
|
119
|
+
// Return prepared data
|
|
120
|
+
return {
|
|
121
|
+
projects:
|
|
122
|
+
placeholderCount > 0
|
|
123
|
+
? createPlaceholders(placeholderCount, sortedProjects)
|
|
124
|
+
: sortedProjects,
|
|
125
|
+
totalPages,
|
|
80
126
|
};
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
processedProject.id = processedProject.title
|
|
85
|
-
.toLowerCase()
|
|
86
|
-
.replace(/[^\w\s-]/g, '')
|
|
87
|
-
.replace(/\s+/g, '-')
|
|
88
|
-
.replace(/-+/g, '-');
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
return processedProject;
|
|
92
|
-
});
|
|
93
|
-
|
|
94
|
-
// Calculate pagination and placeholder needs
|
|
95
|
-
const totalPages = Math.ceil(sortedProjects.length / slides);
|
|
96
|
-
const slotsPerPage = slides;
|
|
97
|
-
const totalSlots = totalPages * slotsPerPage;
|
|
98
|
-
const placeholderCount = totalSlots - sortedProjects.length;
|
|
99
|
-
|
|
100
|
-
// Return prepared data
|
|
101
|
-
return {
|
|
102
|
-
projects: placeholderCount > 0
|
|
103
|
-
? createPlaceholders(placeholderCount, sortedProjects)
|
|
104
|
-
: sortedProjects,
|
|
105
|
-
totalPages
|
|
106
|
-
};
|
|
107
|
-
}, [createPlaceholders, projectDefaults]);
|
|
127
|
+
},
|
|
128
|
+
[createPlaceholders, projectDefaults],
|
|
129
|
+
);
|
|
108
130
|
|
|
109
131
|
// Load and set up projects on initial load and on resize
|
|
110
132
|
useEffect(() => {
|
|
111
133
|
const projectShelf = siteConfig.customFields?.projects;
|
|
112
|
-
const configuredProjects = projectShelf?.enable
|
|
113
|
-
|
|
134
|
+
const configuredProjects = projectShelf?.enable
|
|
135
|
+
? projectShelf?.projects || []
|
|
136
|
+
: [];
|
|
137
|
+
|
|
114
138
|
const handleLayout = () => {
|
|
115
139
|
const newSlidesToShow = getVisibleSlidesPerView();
|
|
116
|
-
|
|
140
|
+
|
|
117
141
|
if (newSlidesToShow !== slidesToShow || !projects.length) {
|
|
118
142
|
setSlidesToShow(newSlidesToShow);
|
|
119
|
-
const { projects: newProjects, totalPages: newTotalPages } =
|
|
143
|
+
const { projects: newProjects, totalPages: newTotalPages } =
|
|
120
144
|
prepareProjects(configuredProjects, newSlidesToShow);
|
|
121
|
-
|
|
145
|
+
|
|
122
146
|
setProjects(newProjects);
|
|
123
147
|
setTotalPages(newTotalPages);
|
|
124
148
|
setAtEnd(newProjects.length <= newSlidesToShow);
|
|
@@ -127,124 +151,135 @@ export default function ProjectsSection({ id, className, title, subtitle }) {
|
|
|
127
151
|
|
|
128
152
|
// Initial setup
|
|
129
153
|
handleLayout();
|
|
130
|
-
|
|
154
|
+
|
|
131
155
|
// Resize handler
|
|
132
|
-
window.addEventListener(
|
|
133
|
-
return () => window.removeEventListener(
|
|
134
|
-
}, [
|
|
156
|
+
window.addEventListener("resize", handleLayout);
|
|
157
|
+
return () => window.removeEventListener("resize", handleLayout);
|
|
158
|
+
}, [
|
|
159
|
+
siteConfig,
|
|
160
|
+
getVisibleSlidesPerView,
|
|
161
|
+
prepareProjects,
|
|
162
|
+
slidesToShow,
|
|
163
|
+
projects.length,
|
|
164
|
+
]);
|
|
135
165
|
|
|
136
166
|
// Method to go to a specific slide
|
|
137
|
-
const goToSlide = useCallback(
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
}
|
|
144
|
-
|
|
167
|
+
const goToSlide = useCallback(
|
|
168
|
+
(index) => {
|
|
169
|
+
if (sliderRef.current) {
|
|
170
|
+
sliderRef.current.slickGoTo(index * slidesToShow);
|
|
171
|
+
setCurrentSlide(index);
|
|
172
|
+
}
|
|
173
|
+
},
|
|
174
|
+
[slidesToShow],
|
|
175
|
+
);
|
|
145
176
|
|
|
146
177
|
useEffect(() => {
|
|
147
178
|
const scrollTimeout = setTimeout(() => {
|
|
148
|
-
|
|
149
179
|
if (activeDotRef.current && dotsContainerRef.current) {
|
|
150
180
|
const container = dotsContainerRef.current;
|
|
151
181
|
const activeDot = activeDotRef.current;
|
|
152
|
-
|
|
153
|
-
try {
|
|
154
182
|
|
|
183
|
+
try {
|
|
155
184
|
// For first few
|
|
156
185
|
const adaptiveThreshold = Math.max(1, Math.floor(totalPages * 0.1));
|
|
157
186
|
if (currentSlide <= adaptiveThreshold) {
|
|
158
187
|
container.scrollTo({
|
|
159
188
|
left: 0,
|
|
160
|
-
behavior:
|
|
189
|
+
behavior: "smooth",
|
|
161
190
|
});
|
|
162
191
|
return;
|
|
163
192
|
}
|
|
164
|
-
|
|
193
|
+
|
|
165
194
|
// For last few
|
|
166
195
|
if (currentSlide >= totalPages - 2) {
|
|
167
196
|
const scrollMax = container.scrollWidth - container.clientWidth;
|
|
168
197
|
container.scrollTo({
|
|
169
198
|
left: scrollMax,
|
|
170
|
-
behavior:
|
|
199
|
+
behavior: "smooth",
|
|
171
200
|
});
|
|
172
201
|
return;
|
|
173
202
|
}
|
|
174
|
-
|
|
203
|
+
|
|
175
204
|
// Center the active dot at mobile
|
|
176
205
|
const dotRect = activeDot.getBoundingClientRect();
|
|
177
206
|
const containerRect = container.getBoundingClientRect();
|
|
178
|
-
|
|
207
|
+
|
|
179
208
|
// Check if dot is within visible area with margins
|
|
180
209
|
const isOutsideLeft = dotRect.left < containerRect.left + 20;
|
|
181
210
|
const isOutsideRight = dotRect.right > containerRect.right - 20;
|
|
182
|
-
|
|
211
|
+
|
|
183
212
|
if (isOutsideLeft || isOutsideRight) {
|
|
184
213
|
const dotPosition = activeDot.offsetLeft;
|
|
185
214
|
const dotWidth = activeDot.clientWidth;
|
|
186
215
|
const containerWidth = container.clientWidth;
|
|
187
|
-
const scrollPosition =
|
|
188
|
-
|
|
216
|
+
const scrollPosition =
|
|
217
|
+
dotPosition - containerWidth / 2 + dotWidth / 2;
|
|
218
|
+
|
|
189
219
|
// Disable smooth scroll if not supported
|
|
190
|
-
if (
|
|
220
|
+
if ("scrollBehavior" in document.documentElement.style) {
|
|
191
221
|
container.scrollTo({
|
|
192
222
|
left: Math.max(0, scrollPosition),
|
|
193
|
-
behavior:
|
|
223
|
+
behavior: "smooth",
|
|
194
224
|
});
|
|
195
225
|
} else {
|
|
196
226
|
container.scrollLeft = Math.max(0, scrollPosition);
|
|
197
227
|
}
|
|
198
228
|
}
|
|
199
229
|
} catch (error) {
|
|
200
|
-
console.warn(
|
|
230
|
+
console.warn("Dot scrolling error:", error);
|
|
201
231
|
}
|
|
202
232
|
}
|
|
203
233
|
}, 50);
|
|
204
|
-
|
|
234
|
+
|
|
205
235
|
return () => clearTimeout(scrollTimeout);
|
|
206
236
|
}, [currentSlide, totalPages]);
|
|
207
237
|
|
|
208
238
|
// Carousel settings
|
|
209
|
-
const settings = useMemo(
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
239
|
+
const settings = useMemo(
|
|
240
|
+
() => ({
|
|
241
|
+
dots: false,
|
|
242
|
+
infinite: false,
|
|
243
|
+
speed: 600,
|
|
244
|
+
slidesToShow: Math.min(projects.length, slidesToShow),
|
|
245
|
+
slidesToScroll: slidesToShow,
|
|
246
|
+
autoplay: false,
|
|
247
|
+
adaptiveHeight: false,
|
|
248
|
+
centerPadding: "20px",
|
|
249
|
+
centerMode: false,
|
|
250
|
+
variableWidth: false,
|
|
251
|
+
swipeToSlide: false,
|
|
252
|
+
focusOnSelect: false,
|
|
253
|
+
responsive: [
|
|
254
|
+
{
|
|
255
|
+
breakpoint: 1024,
|
|
256
|
+
settings: {
|
|
257
|
+
slidesToShow: Math.min(projects.length, 2),
|
|
258
|
+
slidesToScroll: 2,
|
|
259
|
+
dots: false,
|
|
260
|
+
},
|
|
261
|
+
},
|
|
262
|
+
{
|
|
263
|
+
breakpoint: 600,
|
|
264
|
+
settings: {
|
|
265
|
+
slidesToShow: 1,
|
|
266
|
+
slidesToScroll: 1,
|
|
267
|
+
dots: false,
|
|
268
|
+
arrows: false,
|
|
269
|
+
},
|
|
270
|
+
},
|
|
271
|
+
],
|
|
272
|
+
className: styles.projectsCarousel,
|
|
273
|
+
beforeChange: (current, next) => {
|
|
274
|
+
setAtBeginning(next === 0);
|
|
275
|
+
setCurrentSlide(Math.floor(next / slidesToShow));
|
|
276
|
+
setAtEnd(
|
|
277
|
+
next + Math.min(projects.length, slidesToShow) >= projects.length,
|
|
278
|
+
);
|
|
230
279
|
},
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
slidesToShow: 1,
|
|
235
|
-
slidesToScroll: 1,
|
|
236
|
-
dots: false,
|
|
237
|
-
arrows: false,
|
|
238
|
-
}
|
|
239
|
-
}
|
|
240
|
-
],
|
|
241
|
-
className: styles.projectsCarousel,
|
|
242
|
-
beforeChange: (current, next) => {
|
|
243
|
-
setAtBeginning(next === 0);
|
|
244
|
-
setCurrentSlide(Math.floor(next / slidesToShow));
|
|
245
|
-
setAtEnd(next + Math.min(projects.length, slidesToShow) >= projects.length);
|
|
246
|
-
},
|
|
247
|
-
}), [projects, slidesToShow]);
|
|
280
|
+
}),
|
|
281
|
+
[projects, slidesToShow],
|
|
282
|
+
);
|
|
248
283
|
|
|
249
284
|
// Navigation handlers
|
|
250
285
|
const goToNext = useCallback(() => {
|
|
@@ -264,10 +299,10 @@ export default function ProjectsSection({ id, className, title, subtitle }) {
|
|
|
264
299
|
if (!url) return null;
|
|
265
300
|
|
|
266
301
|
return (
|
|
267
|
-
<a
|
|
268
|
-
href={url}
|
|
269
|
-
target="_blank"
|
|
270
|
-
rel="noopener noreferrer"
|
|
302
|
+
<a
|
|
303
|
+
href={url}
|
|
304
|
+
target="_blank"
|
|
305
|
+
rel="noopener noreferrer"
|
|
271
306
|
className={styles.projectLink}
|
|
272
307
|
aria-label={ariaLabel}
|
|
273
308
|
>
|
|
@@ -279,48 +314,48 @@ export default function ProjectsSection({ id, className, title, subtitle }) {
|
|
|
279
314
|
|
|
280
315
|
// Get state label and class
|
|
281
316
|
const getProjectStateInfo = useCallback((state) => {
|
|
282
|
-
switch(state?.toLowerCase()) {
|
|
317
|
+
switch (state?.toLowerCase()) {
|
|
283
318
|
// For projects currently in active development
|
|
284
|
-
case
|
|
285
|
-
return {
|
|
286
|
-
label:
|
|
319
|
+
case "active":
|
|
320
|
+
return {
|
|
321
|
+
label: "Active",
|
|
287
322
|
className: styles.stateActive,
|
|
288
323
|
};
|
|
289
324
|
// For finished projects
|
|
290
|
-
case
|
|
291
|
-
return {
|
|
292
|
-
label:
|
|
325
|
+
case "completed":
|
|
326
|
+
return {
|
|
327
|
+
label: "Completed",
|
|
293
328
|
className: styles.stateCompleted,
|
|
294
329
|
};
|
|
295
330
|
// For projects receiving updates/maintenance
|
|
296
|
-
case
|
|
297
|
-
return {
|
|
298
|
-
label:
|
|
331
|
+
case "maintenance":
|
|
332
|
+
return {
|
|
333
|
+
label: "Maintenance",
|
|
299
334
|
className: styles.stateMaintenance,
|
|
300
335
|
};
|
|
301
336
|
// For temporarily paused development
|
|
302
|
-
case
|
|
303
|
-
return {
|
|
304
|
-
label:
|
|
337
|
+
case "paused":
|
|
338
|
+
return {
|
|
339
|
+
label: "Paused",
|
|
305
340
|
className: styles.statePaused,
|
|
306
341
|
};
|
|
307
342
|
// For projects no longer maintained
|
|
308
|
-
case
|
|
309
|
-
return {
|
|
310
|
-
label:
|
|
343
|
+
case "archived":
|
|
344
|
+
return {
|
|
345
|
+
label: "Archived",
|
|
311
346
|
className: styles.stateArchived,
|
|
312
347
|
};
|
|
313
348
|
// For future projects in planning stage
|
|
314
|
-
case
|
|
315
|
-
return {
|
|
316
|
-
label:
|
|
349
|
+
case "planned":
|
|
350
|
+
return {
|
|
351
|
+
label: "Planned",
|
|
317
352
|
className: styles.statePlanned,
|
|
318
353
|
};
|
|
319
354
|
// Default state when not specified
|
|
320
|
-
case
|
|
355
|
+
case "n/a":
|
|
321
356
|
default:
|
|
322
|
-
return {
|
|
323
|
-
label:
|
|
357
|
+
return {
|
|
358
|
+
label: "N/A",
|
|
324
359
|
className: styles.stateNA,
|
|
325
360
|
};
|
|
326
361
|
}
|
|
@@ -332,9 +367,9 @@ export default function ProjectsSection({ id, className, title, subtitle }) {
|
|
|
332
367
|
|
|
333
368
|
// Determine if we should use scrollable or centered layout
|
|
334
369
|
const fewDots = totalPages <= 5;
|
|
335
|
-
|
|
370
|
+
|
|
336
371
|
return (
|
|
337
|
-
<div
|
|
372
|
+
<div
|
|
338
373
|
className={`${styles.navDotsContainer} ${fewDots ? styles.centerDots : styles.scrollDots}`}
|
|
339
374
|
role="tablist"
|
|
340
375
|
aria-label="Project carousel navigation"
|
|
@@ -342,7 +377,7 @@ export default function ProjectsSection({ id, className, title, subtitle }) {
|
|
|
342
377
|
{Array.from({ length: totalPages }, (_, i) => (
|
|
343
378
|
<button
|
|
344
379
|
key={i}
|
|
345
|
-
className={`${styles.navDot} ${currentSlide === i ? styles.activeDot :
|
|
380
|
+
className={`${styles.navDot} ${currentSlide === i ? styles.activeDot : ""}`}
|
|
346
381
|
onClick={() => goToSlide(i)}
|
|
347
382
|
aria-label={`Go to slide ${i + 1} of ${totalPages}`}
|
|
348
383
|
aria-selected={currentSlide === i}
|
|
@@ -356,17 +391,21 @@ export default function ProjectsSection({ id, className, title, subtitle }) {
|
|
|
356
391
|
}, [currentSlide, totalPages, goToSlide]);
|
|
357
392
|
|
|
358
393
|
return (
|
|
359
|
-
<div
|
|
394
|
+
<div
|
|
395
|
+
id={id}
|
|
396
|
+
className={`${styles.projectsSection} ${className || ""}`}
|
|
397
|
+
role="region"
|
|
398
|
+
aria-label="Projects section"
|
|
399
|
+
>
|
|
360
400
|
<div className={styles.projectsContainer}>
|
|
361
401
|
<div className={styles.projectsHeader}>
|
|
362
|
-
<h2 className={styles.projectsTitle}>
|
|
363
|
-
{title || "My Projects"}
|
|
364
|
-
</h2>
|
|
402
|
+
<h2 className={styles.projectsTitle}>{title || "My Projects"}</h2>
|
|
365
403
|
<p className={styles.projectsSubtitle}>
|
|
366
|
-
{subtitle ||
|
|
404
|
+
{subtitle ||
|
|
405
|
+
"A collection of all my works, with featured projects highlighted"}
|
|
367
406
|
</p>
|
|
368
407
|
</div>
|
|
369
|
-
|
|
408
|
+
|
|
370
409
|
{projects.length === 0 ? (
|
|
371
410
|
<div className={styles.noProjects}>
|
|
372
411
|
<p>No projects to display.</p>
|
|
@@ -374,8 +413,8 @@ export default function ProjectsSection({ id, className, title, subtitle }) {
|
|
|
374
413
|
) : (
|
|
375
414
|
<div className={styles.carouselContainer}>
|
|
376
415
|
{/* Desktop navigation buttons (sides) */}
|
|
377
|
-
<button
|
|
378
|
-
className={`${styles.carouselControl} ${styles.prevButton} ${styles.desktopOnly} ${atBeginning ? styles.disabledButton :
|
|
416
|
+
<button
|
|
417
|
+
className={`${styles.carouselControl} ${styles.prevButton} ${styles.desktopOnly} ${atBeginning ? styles.disabledButton : ""}`}
|
|
379
418
|
onClick={goToPrev}
|
|
380
419
|
aria-label="View previous projects"
|
|
381
420
|
aria-disabled={atBeginning}
|
|
@@ -384,25 +423,33 @@ export default function ProjectsSection({ id, className, title, subtitle }) {
|
|
|
384
423
|
>
|
|
385
424
|
<FaChevronLeft aria-hidden="true" />
|
|
386
425
|
</button>
|
|
387
|
-
|
|
388
|
-
<div
|
|
426
|
+
|
|
427
|
+
<div
|
|
428
|
+
className={styles.carouselWrapper}
|
|
429
|
+
aria-roledescription="carousel"
|
|
430
|
+
aria-label="Projects carousel"
|
|
431
|
+
>
|
|
389
432
|
<Slider ref={sliderRef} {...settings}>
|
|
390
433
|
{projects.map((project, index) => (
|
|
391
|
-
<div
|
|
392
|
-
key={project.id || project.title + index}
|
|
393
|
-
className={styles.carouselSlide}
|
|
434
|
+
<div
|
|
435
|
+
key={project.id || project.title + index}
|
|
436
|
+
className={styles.carouselSlide}
|
|
394
437
|
data-project-id={project.id}
|
|
395
|
-
aria-roledescription="slide"
|
|
438
|
+
aria-roledescription="slide"
|
|
396
439
|
aria-label={`Project ${index + 1} of ${projects.length}: ${project.title}`}
|
|
397
440
|
>
|
|
398
|
-
<div
|
|
441
|
+
<div
|
|
442
|
+
className={`${styles.carouselCard} ${project.featured ? styles.featuredCard : ""}`}
|
|
443
|
+
>
|
|
399
444
|
{/* Project state badge */}
|
|
400
445
|
{project.state && (
|
|
401
|
-
<div
|
|
446
|
+
<div
|
|
402
447
|
className={styles.projectStateBadge}
|
|
403
448
|
title={`Project status: ${getProjectStateInfo(project.state).label}`}
|
|
404
449
|
>
|
|
405
|
-
<span
|
|
450
|
+
<span
|
|
451
|
+
className={`${styles.projectStateLabel} ${getProjectStateInfo(project.state).className}`}
|
|
452
|
+
>
|
|
406
453
|
{getProjectStateInfo(project.state).label}
|
|
407
454
|
</span>
|
|
408
455
|
</div>
|
|
@@ -410,74 +457,82 @@ export default function ProjectsSection({ id, className, title, subtitle }) {
|
|
|
410
457
|
|
|
411
458
|
<div className={styles.projectImageContainer}>
|
|
412
459
|
{project.image && (
|
|
413
|
-
<img
|
|
414
|
-
src={project.image}
|
|
415
|
-
alt={project.title}
|
|
416
|
-
className={styles.projectImage}
|
|
460
|
+
<img
|
|
461
|
+
src={project.image}
|
|
462
|
+
alt={project.title}
|
|
463
|
+
className={styles.projectImage}
|
|
417
464
|
loading="lazy"
|
|
418
465
|
/>
|
|
419
466
|
)}
|
|
420
|
-
|
|
467
|
+
|
|
421
468
|
{/* Featured badge */}
|
|
422
469
|
{project.featured && (
|
|
423
|
-
<div
|
|
470
|
+
<div
|
|
471
|
+
className={styles.featuredBadge}
|
|
472
|
+
title="Featured Project"
|
|
473
|
+
aria-label="Featured project"
|
|
474
|
+
>
|
|
424
475
|
<FaStar aria-hidden="true" />
|
|
425
476
|
</div>
|
|
426
477
|
)}
|
|
427
478
|
</div>
|
|
428
|
-
|
|
479
|
+
|
|
429
480
|
<div className={styles.projectContent}>
|
|
430
481
|
<h3 className={styles.projectTitle}>{project.title}</h3>
|
|
431
|
-
|
|
482
|
+
|
|
432
483
|
{project.tags?.length > 0 && (
|
|
433
484
|
<div className={styles.projectTags}>
|
|
434
|
-
{project.tags.map(tag => (
|
|
435
|
-
<span key={tag} className={styles.projectTag}>
|
|
485
|
+
{project.tags.map((tag) => (
|
|
486
|
+
<span key={tag} className={styles.projectTag}>
|
|
487
|
+
{tag}
|
|
488
|
+
</span>
|
|
436
489
|
))}
|
|
437
490
|
</div>
|
|
438
491
|
)}
|
|
439
|
-
|
|
440
|
-
<p className={styles.projectDescription}>
|
|
492
|
+
|
|
493
|
+
<p className={styles.projectDescription}>
|
|
494
|
+
{project.description}
|
|
495
|
+
</p>
|
|
441
496
|
</div>
|
|
442
|
-
|
|
497
|
+
|
|
443
498
|
<div className={styles.projectLinks}>
|
|
444
499
|
{renderProjectLink(
|
|
445
|
-
project.website,
|
|
446
|
-
FaGlobe,
|
|
447
|
-
"Website",
|
|
448
|
-
`Visit ${project.title} website
|
|
500
|
+
project.website,
|
|
501
|
+
FaGlobe,
|
|
502
|
+
"Website",
|
|
503
|
+
`Visit ${project.title} website`,
|
|
449
504
|
)}
|
|
450
|
-
|
|
505
|
+
|
|
451
506
|
{renderProjectLink(
|
|
452
|
-
project.github,
|
|
453
|
-
FaGithub,
|
|
454
|
-
"Source",
|
|
455
|
-
`Repository with source code
|
|
507
|
+
project.github,
|
|
508
|
+
FaGithub,
|
|
509
|
+
"Source",
|
|
510
|
+
`Repository with source code`,
|
|
456
511
|
)}
|
|
457
|
-
|
|
512
|
+
|
|
458
513
|
{renderProjectLink(
|
|
459
|
-
project.Demo,
|
|
460
|
-
FaPlay,
|
|
461
|
-
"Demo",
|
|
462
|
-
`Live demo for ${project.title}
|
|
514
|
+
project.Demo,
|
|
515
|
+
FaPlay,
|
|
516
|
+
"Demo",
|
|
517
|
+
`Live demo for ${project.title}`,
|
|
463
518
|
)}
|
|
464
519
|
</div>
|
|
465
520
|
</div>
|
|
466
521
|
</div>
|
|
467
522
|
))}
|
|
468
523
|
</Slider>
|
|
469
|
-
|
|
524
|
+
|
|
470
525
|
{/* Desktop navigation dots */}
|
|
471
526
|
<div className={styles.desktopDotsContainer}>
|
|
472
527
|
{renderNavigationDots()}
|
|
473
528
|
</div>
|
|
474
|
-
|
|
529
|
+
|
|
475
530
|
{/* Mobile navigation controls (bottom) */}
|
|
476
531
|
<div className={styles.mobileNavigationControls}>
|
|
477
532
|
{totalPages > 1 && (
|
|
478
533
|
<>
|
|
479
|
-
<button
|
|
480
|
-
className={`${styles.carouselControl} ${styles.prevButton} ${atBeginning ? styles.disabledButton :
|
|
534
|
+
<button
|
|
535
|
+
className={`${styles.carouselControl} ${styles.prevButton} ${atBeginning ? styles.disabledButton : ""}`}
|
|
481
536
|
onClick={goToPrev}
|
|
482
537
|
aria-label="View previous projects"
|
|
483
538
|
aria-disabled={atBeginning}
|
|
@@ -486,17 +541,17 @@ export default function ProjectsSection({ id, className, title, subtitle }) {
|
|
|
486
541
|
>
|
|
487
542
|
<FaChevronLeft aria-hidden="true" />
|
|
488
543
|
</button>
|
|
489
|
-
|
|
544
|
+
|
|
490
545
|
{/* Mobile navigation dots */}
|
|
491
|
-
<div
|
|
546
|
+
<div
|
|
492
547
|
className={styles.dotsScrollContainer}
|
|
493
548
|
ref={dotsContainerRef}
|
|
494
549
|
>
|
|
495
550
|
{renderNavigationDots()}
|
|
496
551
|
</div>
|
|
497
|
-
|
|
498
|
-
<button
|
|
499
|
-
className={`${styles.carouselControl} ${styles.nextButton} ${atEnd ? styles.disabledButton :
|
|
552
|
+
|
|
553
|
+
<button
|
|
554
|
+
className={`${styles.carouselControl} ${styles.nextButton} ${atEnd ? styles.disabledButton : ""}`}
|
|
500
555
|
onClick={goToNext}
|
|
501
556
|
aria-label="View next projects"
|
|
502
557
|
aria-disabled={atEnd}
|
|
@@ -509,10 +564,10 @@ export default function ProjectsSection({ id, className, title, subtitle }) {
|
|
|
509
564
|
)}
|
|
510
565
|
</div>
|
|
511
566
|
</div>
|
|
512
|
-
|
|
567
|
+
|
|
513
568
|
{/* Desktop navigation button (right side) */}
|
|
514
|
-
<button
|
|
515
|
-
className={`${styles.carouselControl} ${styles.nextButton} ${styles.desktopOnly} ${atEnd ? styles.disabledButton :
|
|
569
|
+
<button
|
|
570
|
+
className={`${styles.carouselControl} ${styles.nextButton} ${styles.desktopOnly} ${atEnd ? styles.disabledButton : ""}`}
|
|
516
571
|
onClick={goToNext}
|
|
517
572
|
aria-label="View next projects"
|
|
518
573
|
aria-disabled={atEnd}
|