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.
Files changed (91) hide show
  1. package/bin/portosaurus.mjs +14 -327
  2. package/package.json +16 -11
  3. package/src/cli/build.mjs +43 -0
  4. package/src/cli/dev.mjs +31 -0
  5. package/src/cli/init.mjs +135 -0
  6. package/src/cli/serve.mjs +30 -0
  7. package/src/core/buildDocuConfig.mjs +664 -0
  8. package/src/core/{themePlugin.mjs → plugins/themePlugin.mjs} +1 -1
  9. package/src/template/.github/workflows/deploy.yml +52 -0
  10. package/src/template/.nojekyll +0 -0
  11. package/src/template/README.md +58 -0
  12. package/src/template/blog/authors.yml +1 -1
  13. package/src/template/blog/welcome.md +1 -1
  14. package/src/template/config.js +40 -23
  15. package/src/template/package.json +20 -0
  16. package/src/template/static/img/svg/icon-blog.svg +2 -0
  17. package/src/template/static/img/svg/icon-note.svg +2 -0
  18. package/src/{components → theme/components}/AboutSection/index.js +22 -13
  19. package/src/{components → theme/components}/AboutSection/styles.module.css +59 -48
  20. package/src/{components → theme/components}/ContactSection/index.js +31 -24
  21. package/src/{components → theme/components}/ContactSection/styles.module.css +31 -26
  22. package/src/{components → theme/components}/ExperienceSection/index.js +12 -7
  23. package/src/{components → theme/components}/ExperienceSection/styles.module.css +23 -20
  24. package/src/{components → theme/components}/HeroSection/index.js +9 -11
  25. package/src/{components → theme/components}/HeroSection/styles.module.css +44 -32
  26. package/src/{components → theme/components}/NoteIndex/index.js +10 -3
  27. package/src/{components → theme/components}/Preview/components/PreviewHeader.js +14 -8
  28. package/src/{components → theme/components}/Preview/components/Triggers/Pv.js +32 -7
  29. package/src/{components → theme/components}/Preview/components/Triggers/SrcPv.js +1 -5
  30. package/src/theme/components/Preview/index.js +3 -0
  31. package/src/{components → theme/components}/ProjectsSection/index.js +279 -224
  32. package/src/{components → theme/components}/ProjectsSection/styles.module.css +21 -17
  33. package/src/{components → theme/components}/ScrollToTop/index.js +18 -21
  34. package/src/{components → theme/components}/ScrollToTop/styles.module.css +10 -9
  35. package/src/theme/components/SocialLinks/index.js +125 -0
  36. package/src/{components → theme/components}/SocialLinks/styles.module.css +9 -7
  37. package/src/{components → theme/components}/Tooltip/index.js +4 -1
  38. package/src/theme/config/iconMappings.js +465 -0
  39. package/src/theme/config/metaTags.js +239 -0
  40. package/src/theme/config/prism.js +179 -0
  41. package/src/theme/config/sidebar.js +17 -0
  42. package/src/{css → theme/css}/bootstrap.css +0 -1
  43. package/src/theme/css/catppuccin.css +618 -0
  44. package/src/{css → theme/css}/custom.css +3 -9
  45. package/src/{css → theme/css}/tasks.css +43 -37
  46. package/src/theme/{MDXComponents.js → overrides/MDXComponents.js} +3 -3
  47. package/src/theme/{Root.js → overrides/Root.js} +2 -4
  48. package/src/{pages → theme/pages}/index.js +23 -39
  49. package/src/theme/pages/notes.js +83 -0
  50. package/src/{pages → theme/pages}/tasks.js +115 -56
  51. package/src/{core/client-utils → theme/utils}/HashNavigation.js +60 -49
  52. package/src/{core/client-utils → theme/utils}/updateTitle.js +21 -25
  53. package/src/{core/build-utils → utils/build}/cssUtils.mjs +5 -3
  54. package/src/{core/build-utils → utils/build}/generateFavicon.mjs +44 -12
  55. package/src/{core/build-utils → utils/build}/generateRobotsTxt.mjs +4 -3
  56. package/src/{core/build-utils → utils/build}/iconExtractor.mjs +7 -3
  57. package/src/utils/build/imageDownloader.mjs +159 -0
  58. package/src/{core/build-utils → utils/build}/imageProcessor.mjs +5 -6
  59. package/src/utils/helpers.mjs +153 -0
  60. package/src/utils/logger.mjs +53 -0
  61. package/src/utils/packageManager.mjs +88 -0
  62. package/src/components/Preview/index.js +0 -3
  63. package/src/components/SocialLinks/index.js +0 -130
  64. package/src/config/iconMappings.js +0 -329
  65. package/src/config/metaTags.js +0 -240
  66. package/src/config/prism.js +0 -179
  67. package/src/config/sidebar.js +0 -20
  68. package/src/core/build-utils/imageDownloader.mjs +0 -98
  69. package/src/core/createDocuConf.mjs +0 -490
  70. package/src/core/defaults.mjs +0 -67
  71. package/src/core/logger.mjs +0 -17
  72. package/src/core/packageManager.mjs +0 -72
  73. package/src/css/catppuccin.css +0 -632
  74. package/src/pages/notes.js +0 -87
  75. /package/src/template/notes/{welcome.md → welcome.mdx} +0 -0
  76. /package/src/{components → theme/components}/NoteIndex/styles.module.css +0 -0
  77. /package/src/{components → theme/components}/Preview/components/FeedbackStates.js +0 -0
  78. /package/src/{components → theme/components}/Preview/components/FileTabs.js +0 -0
  79. /package/src/{components → theme/components}/Preview/components/Triggers/index.js +0 -0
  80. /package/src/{components → theme/components}/Preview/components/ViewerWindow.js +0 -0
  81. /package/src/{components → theme/components}/Preview/hooks/useDeepLinkHash.js +0 -0
  82. /package/src/{components → theme/components}/Preview/hooks/useDockLayout.js +0 -0
  83. /package/src/{components → theme/components}/Preview/hooks/useFileFetch.js +0 -0
  84. /package/src/{components → theme/components}/Preview/renderers/CodeRenderer.js +0 -0
  85. /package/src/{components → theme/components}/Preview/renderers/ImageRenderer.js +0 -0
  86. /package/src/{components → theme/components}/Preview/renderers/PdfRenderer.js +0 -0
  87. /package/src/{components → theme/components}/Preview/renderers/WebRenderer.js +0 -0
  88. /package/src/{components → theme/components}/Preview/state/index.js +0 -0
  89. /package/src/{components → theme/components}/Preview/styles.module.css +0 -0
  90. /package/src/{components → theme/components}/Preview/utils/index.js +0 -0
  91. /package/src/{components → theme/components}/Tooltip/styles.module.css +0 -0
@@ -1,8 +1,15 @@
1
- import { useRef, useState, useEffect, useCallback, useMemo } from 'react';
1
+ import { useRef, useState, useEffect, useCallback, useMemo } from "react";
2
2
  import Slider from "react-slick";
3
- import { FaGithub, FaGlobe, FaPlay, FaChevronLeft, FaChevronRight, FaStar } from 'react-icons/fa';
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 './styles.module.css';
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: "Coming soon...",
36
+ desc: "Coming soon...",
30
37
  image: "img/project-blank.png",
31
38
  state: "active",
32
- tags: ["planned"]
39
+ tags: ["planned"],
33
40
  };
34
41
 
42
+ const createPlaceholders = useCallback(
43
+ (count, existingProjects) => {
44
+ if (existingProjects.length === 0) return [];
35
45
 
36
- const createPlaceholders = useCallback((count, existingProjects) => {
37
- if (existingProjects.length === 0) return [];
38
-
39
- return [
40
- ...existingProjects,
41
- ...Array.from({ length: count }, (_, i) => ({
42
- ...projectDefaults,
43
-
44
- // Dummy card config
45
- state: "n/a",
46
- title: `Project ${existingProjects.length + i + 1}`,
47
- description: projectDefaults.desc,
48
- image: projectDefaults.image,
49
- id: `placeholder-${i}`,
50
- tags: null
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 === 'undefined') return 3;
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((projectList, slides) => {
66
- if (!projectList?.length) return { projects: [], totalPages: 0 };
67
-
68
- // Sort featured first
69
- const sortedProjects = [...projectList].sort((a, b) =>
70
- (a.featured ? -1 : 0) - (b.featured ? -1 : 0)
71
- ).map(project => {
72
-
73
- // Apply defaults if value not null
74
- const processedProject = {
75
- ...project,
76
- description: project.desc === undefined ? projectDefaults.desc : project.desc,
77
- image: project.image === undefined ? projectDefaults.image : project.image,
78
- tags: project.tags === undefined ? [...projectDefaults.tags] : project.tags,
79
- state: project.state === undefined ? projectDefaults.state : project.state
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
- // Add ID
83
- if (!processedProject.id) {
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 ? projectShelf?.projects || [] : [];
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('resize', handleLayout);
133
- return () => window.removeEventListener('resize', handleLayout);
134
- }, [siteConfig, getVisibleSlidesPerView, prepareProjects, slidesToShow, projects.length]);
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((index) => {
138
- if (sliderRef.current) {
139
-
140
- sliderRef.current.slickGoTo(index * slidesToShow);
141
- setCurrentSlide(index);
142
-
143
- }
144
- }, [slidesToShow]);
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: 'smooth'
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: 'smooth'
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 = dotPosition - (containerWidth / 2) + (dotWidth / 2);
188
-
216
+ const scrollPosition =
217
+ dotPosition - containerWidth / 2 + dotWidth / 2;
218
+
189
219
  // Disable smooth scroll if not supported
190
- if ('scrollBehavior' in document.documentElement.style) {
220
+ if ("scrollBehavior" in document.documentElement.style) {
191
221
  container.scrollTo({
192
222
  left: Math.max(0, scrollPosition),
193
- behavior: 'smooth'
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('Dot scrolling error:', error);
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
- dots: false,
211
- infinite: false,
212
- speed: 600,
213
- slidesToShow: Math.min(projects.length, slidesToShow),
214
- slidesToScroll: slidesToShow,
215
- autoplay: false,
216
- adaptiveHeight: false,
217
- centerPadding: '20px',
218
- centerMode: false,
219
- variableWidth: false,
220
- swipeToSlide: false,
221
- focusOnSelect: false,
222
- responsive: [
223
- {
224
- breakpoint: 1024,
225
- settings: {
226
- slidesToShow: Math.min(projects.length, 2),
227
- slidesToScroll: 2,
228
- dots: false,
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
- breakpoint: 600,
233
- settings: {
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 'active':
285
- return {
286
- label: 'Active',
319
+ case "active":
320
+ return {
321
+ label: "Active",
287
322
  className: styles.stateActive,
288
323
  };
289
324
  // For finished projects
290
- case 'completed':
291
- return {
292
- label: 'Completed',
325
+ case "completed":
326
+ return {
327
+ label: "Completed",
293
328
  className: styles.stateCompleted,
294
329
  };
295
330
  // For projects receiving updates/maintenance
296
- case 'maintenance':
297
- return {
298
- label: 'Maintenance',
331
+ case "maintenance":
332
+ return {
333
+ label: "Maintenance",
299
334
  className: styles.stateMaintenance,
300
335
  };
301
336
  // For temporarily paused development
302
- case 'paused':
303
- return {
304
- label: 'Paused',
337
+ case "paused":
338
+ return {
339
+ label: "Paused",
305
340
  className: styles.statePaused,
306
341
  };
307
342
  // For projects no longer maintained
308
- case 'archived':
309
- return {
310
- label: 'Archived',
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 'planned':
315
- return {
316
- label: 'Planned',
349
+ case "planned":
350
+ return {
351
+ label: "Planned",
317
352
  className: styles.statePlanned,
318
353
  };
319
354
  // Default state when not specified
320
- case 'n/a':
355
+ case "n/a":
321
356
  default:
322
- return {
323
- label: 'N/A',
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 id={id} className={`${styles.projectsSection} ${className || ''}`} role="region" aria-label="Projects section">
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 || "A collection of all my works, with featured projects highlighted"}
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 className={styles.carouselWrapper} aria-roledescription="carousel" aria-label="Projects carousel">
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 className={`${styles.carouselCard} ${project.featured ? styles.featuredCard : ''}`}>
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 className={`${styles.projectStateLabel} ${getProjectStateInfo(project.state).className}`}>
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 className={styles.featuredBadge} title="Featured Project" aria-label="Featured project">
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}>{tag}</span>
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}>{project.description}</p>
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}