portosaurus 0.14.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.

Potentially problematic release.


This version of portosaurus might be problematic. Click here for more details.

Files changed (64) hide show
  1. package/.vscode/snippets.code-snippets +79 -0
  2. package/AGENTS.md +37 -0
  3. package/GG/config.js +233 -0
  4. package/GG/package.json +14 -0
  5. package/GG/static/.nojekyll +0 -0
  6. package/GG/static/docusaurus-snippet.css +3 -0
  7. package/GG/static/img/icon-bg.png +0 -0
  8. package/GG/static/img/icon-old.png +0 -0
  9. package/GG/static/img/icon.png +0 -0
  10. package/GG/static/img/project-blank.png +0 -0
  11. package/GG/static/img/social-card.jpeg +0 -0
  12. package/LICENSE +674 -0
  13. package/README.md +57 -0
  14. package/bin/portosaurus.js +136 -0
  15. package/package.json +36 -0
  16. package/src/config/iconMappings.js +329 -0
  17. package/src/config/metaTags.js +240 -0
  18. package/src/config/prism.js +179 -0
  19. package/src/config/sidebar.js +20 -0
  20. package/src/configLoader.js +99 -0
  21. package/src/index.js +79 -0
  22. package/src/pages/index.js +98 -0
  23. package/src/pages/notes.js +88 -0
  24. package/src/pages/tasks.js +251 -0
  25. package/src/theme/components/AboutSection/index.js +67 -0
  26. package/src/theme/components/AboutSection/styles.module.css +492 -0
  27. package/src/theme/components/ContactSection/index.js +87 -0
  28. package/src/theme/components/ContactSection/styles.module.css +327 -0
  29. package/src/theme/components/ExperienceSection/index.js +25 -0
  30. package/src/theme/components/ExperienceSection/styles.module.css +180 -0
  31. package/src/theme/components/HeroSection/index.js +63 -0
  32. package/src/theme/components/HeroSection/styles.module.css +471 -0
  33. package/src/theme/components/NoteIndex/index.js +119 -0
  34. package/src/theme/components/NoteIndex/styles.module.css +143 -0
  35. package/src/theme/components/ProjectsSection/index.js +529 -0
  36. package/src/theme/components/ProjectsSection/styles.module.css +830 -0
  37. package/src/theme/components/ScrollToTop/index.js +98 -0
  38. package/src/theme/components/ScrollToTop/styles.module.css +96 -0
  39. package/src/theme/components/SocialLinks/index.js +129 -0
  40. package/src/theme/components/SocialLinks/styles.module.css +55 -0
  41. package/src/theme/components/Tooltip/index.js +30 -0
  42. package/src/theme/components/Tooltip/styles.module.css +92 -0
  43. package/src/theme/css/bootstrap.css +6 -0
  44. package/src/theme/css/catppuccin.css +632 -0
  45. package/src/theme/css/custom.css +186 -0
  46. package/src/theme/css/tasks.css +868 -0
  47. package/src/theme/staticLink/.nojekyll +0 -0
  48. package/src/theme/staticLink/docusaurus-snippet.css +3 -0
  49. package/src/theme/staticLink/img/icon-bg.png +0 -0
  50. package/src/theme/staticLink/img/icon-old.png +0 -0
  51. package/src/theme/staticLink/img/icon.png +0 -0
  52. package/src/theme/staticLink/img/project-blank.png +0 -0
  53. package/src/theme/staticLink/img/social-card.jpeg +0 -0
  54. package/src/utils/HashNavigation.js +250 -0
  55. package/src/utils/appVersion.js +27 -0
  56. package/src/utils/cssUtils.js +99 -0
  57. package/src/utils/filterEnabledItems.js +21 -0
  58. package/src/utils/generateFavicon.js +256 -0
  59. package/src/utils/generateRobotsTxt.js +97 -0
  60. package/src/utils/iconExtractor.js +159 -0
  61. package/src/utils/imageDownloader.js +88 -0
  62. package/src/utils/imageProcessor.js +134 -0
  63. package/src/utils/linkShortner.js +0 -0
  64. package/src/utils/updateTitle.js +107 -0
@@ -0,0 +1,529 @@
1
+ import { useRef, useState, useEffect, useCallback, useMemo } from 'react';
2
+ import Slider from "react-slick";
3
+ import { FaGithub, FaGlobe, FaPlay, FaChevronLeft, FaChevronRight, FaStar } from 'react-icons/fa';
4
+ import useDocusaurusContext from "@docusaurus/useDocusaurusContext";
5
+ import styles from './styles.module.css';
6
+
7
+ // Import slick carousel css
8
+ import "slick-carousel/slick/slick.css";
9
+ import "slick-carousel/slick/slick-theme.css";
10
+
11
+ /// PART OF THIS COMPONENT IS AI GENERATED
12
+
13
+ export default function ProjectsSection({ id, className, title, subtitle }) {
14
+ const { siteConfig } = useDocusaurusContext();
15
+
16
+ const [projects, setProjects] = useState([]);
17
+ const sliderRef = useRef(null);
18
+ const [atBeginning, setAtBeginning] = useState(true);
19
+ const [atEnd, setAtEnd] = useState(false);
20
+ const [slidesToShow, setSlidesToShow] = useState(3);
21
+ const [currentSlide, setCurrentSlide] = useState(0);
22
+ const [totalPages, setTotalPages] = useState(1);
23
+ const activeDotRef = useRef(null);
24
+ const dotsContainerRef = useRef(null);
25
+
26
+ // Default Settings
27
+ const projectDefaults = {
28
+ title: "Future Project",
29
+ desc: "Coming soon...",
30
+ image: "img/project-blank.png",
31
+ state: "active",
32
+ tags: ["planned"]
33
+ };
34
+
35
+
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]);
54
+
55
+ // Get current slidesToShow based on screen width
56
+ const getVisibleSlidesPerView = useCallback(() => {
57
+ if (typeof window === 'undefined') return 3;
58
+
59
+ const width = window.innerWidth;
60
+ if (width <= 600) return 1;
61
+ if (width <= 1024) return 2;
62
+ return 3;
63
+ }, []);
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
80
+ };
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]);
108
+
109
+ // Load and set up projects on initial load and on resize
110
+ useEffect(() => {
111
+ const projectShelf = siteConfig.customFields?.projects;
112
+ const configuredProjects = projectShelf?.enable ? projectShelf?.projects || [] : [];
113
+
114
+ const handleLayout = () => {
115
+ const newSlidesToShow = getVisibleSlidesPerView();
116
+
117
+ if (newSlidesToShow !== slidesToShow || !projects.length) {
118
+ setSlidesToShow(newSlidesToShow);
119
+ const { projects: newProjects, totalPages: newTotalPages } =
120
+ prepareProjects(configuredProjects, newSlidesToShow);
121
+
122
+ setProjects(newProjects);
123
+ setTotalPages(newTotalPages);
124
+ setAtEnd(newProjects.length <= newSlidesToShow);
125
+ }
126
+ };
127
+
128
+ // Initial setup
129
+ handleLayout();
130
+
131
+ // Resize handler
132
+ window.addEventListener('resize', handleLayout);
133
+ return () => window.removeEventListener('resize', handleLayout);
134
+ }, [siteConfig, getVisibleSlidesPerView, prepareProjects, slidesToShow, projects.length]);
135
+
136
+ // 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]);
145
+
146
+ useEffect(() => {
147
+ const scrollTimeout = setTimeout(() => {
148
+
149
+ if (activeDotRef.current && dotsContainerRef.current) {
150
+ const container = dotsContainerRef.current;
151
+ const activeDot = activeDotRef.current;
152
+
153
+ try {
154
+
155
+ // For first few
156
+ const adaptiveThreshold = Math.max(1, Math.floor(totalPages * 0.1));
157
+ if (currentSlide <= adaptiveThreshold) {
158
+ container.scrollTo({
159
+ left: 0,
160
+ behavior: 'smooth'
161
+ });
162
+ return;
163
+ }
164
+
165
+ // For last few
166
+ if (currentSlide >= totalPages - 2) {
167
+ const scrollMax = container.scrollWidth - container.clientWidth;
168
+ container.scrollTo({
169
+ left: scrollMax,
170
+ behavior: 'smooth'
171
+ });
172
+ return;
173
+ }
174
+
175
+ // Center the active dot at mobile
176
+ const dotRect = activeDot.getBoundingClientRect();
177
+ const containerRect = container.getBoundingClientRect();
178
+
179
+ // Check if dot is within visible area with margins
180
+ const isOutsideLeft = dotRect.left < containerRect.left + 20;
181
+ const isOutsideRight = dotRect.right > containerRect.right - 20;
182
+
183
+ if (isOutsideLeft || isOutsideRight) {
184
+ const dotPosition = activeDot.offsetLeft;
185
+ const dotWidth = activeDot.clientWidth;
186
+ const containerWidth = container.clientWidth;
187
+ const scrollPosition = dotPosition - (containerWidth / 2) + (dotWidth / 2);
188
+
189
+ // Disable smooth scroll if not supported
190
+ if ('scrollBehavior' in document.documentElement.style) {
191
+ container.scrollTo({
192
+ left: Math.max(0, scrollPosition),
193
+ behavior: 'smooth'
194
+ });
195
+ } else {
196
+ container.scrollLeft = Math.max(0, scrollPosition);
197
+ }
198
+ }
199
+ } catch (error) {
200
+ console.warn('Dot scrolling error:', error);
201
+ }
202
+ }
203
+ }, 50);
204
+
205
+ return () => clearTimeout(scrollTimeout);
206
+ }, [currentSlide, totalPages]);
207
+
208
+ // 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
+ }
230
+ },
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]);
248
+
249
+ // Navigation handlers
250
+ const goToNext = useCallback(() => {
251
+ if (!atEnd && sliderRef.current) {
252
+ sliderRef.current.slickNext();
253
+ }
254
+ }, [atEnd]);
255
+
256
+ const goToPrev = useCallback(() => {
257
+ if (!atBeginning && sliderRef.current) {
258
+ sliderRef.current.slickPrev();
259
+ }
260
+ }, [atBeginning]);
261
+
262
+ // Project link renderer
263
+ const renderProjectLink = useCallback((url, Icon, label, ariaLabel) => {
264
+ if (!url) return null;
265
+
266
+ return (
267
+ <a
268
+ href={url}
269
+ target="_blank"
270
+ rel="noopener noreferrer"
271
+ className={styles.projectLink}
272
+ aria-label={ariaLabel}
273
+ >
274
+ <Icon />
275
+ <span>{label}</span>
276
+ </a>
277
+ );
278
+ }, []);
279
+
280
+ // Get state label and class
281
+ const getProjectStateInfo = useCallback((state) => {
282
+ switch(state?.toLowerCase()) {
283
+ // For projects currently in active development
284
+ case 'active':
285
+ return {
286
+ label: 'Active',
287
+ className: styles.stateActive,
288
+ };
289
+ // For finished projects
290
+ case 'completed':
291
+ return {
292
+ label: 'Completed',
293
+ className: styles.stateCompleted,
294
+ };
295
+ // For projects receiving updates/maintenance
296
+ case 'maintenance':
297
+ return {
298
+ label: 'Maintenance',
299
+ className: styles.stateMaintenance,
300
+ };
301
+ // For temporarily paused development
302
+ case 'paused':
303
+ return {
304
+ label: 'Paused',
305
+ className: styles.statePaused,
306
+ };
307
+ // For projects no longer maintained
308
+ case 'archived':
309
+ return {
310
+ label: 'Archived',
311
+ className: styles.stateArchived,
312
+ };
313
+ // For future projects in planning stage
314
+ case 'planned':
315
+ return {
316
+ label: 'Planned',
317
+ className: styles.statePlanned,
318
+ };
319
+ // Default state when not specified
320
+ case 'n/a':
321
+ default:
322
+ return {
323
+ label: 'N/A',
324
+ className: styles.stateNA,
325
+ };
326
+ }
327
+ }, []);
328
+
329
+ // Render navigation dots with proper CSS classes based on count
330
+ const renderNavigationDots = useCallback(() => {
331
+ if (totalPages <= 1) return null;
332
+
333
+ // Determine if we should use scrollable or centered layout
334
+ const fewDots = totalPages <= 5;
335
+
336
+ return (
337
+ <div
338
+ className={`${styles.navDotsContainer} ${fewDots ? styles.centerDots : styles.scrollDots}`}
339
+ role="tablist"
340
+ aria-label="Project carousel navigation"
341
+ >
342
+ {Array.from({ length: totalPages }, (_, i) => (
343
+ <button
344
+ key={i}
345
+ className={`${styles.navDot} ${currentSlide === i ? styles.activeDot : ''}`}
346
+ onClick={() => goToSlide(i)}
347
+ aria-label={`Go to slide ${i + 1} of ${totalPages}`}
348
+ aria-selected={currentSlide === i}
349
+ role="tab"
350
+ type="button"
351
+ ref={currentSlide === i ? activeDotRef : null}
352
+ />
353
+ ))}
354
+ </div>
355
+ );
356
+ }, [currentSlide, totalPages, goToSlide]);
357
+
358
+ return (
359
+ <div id={id} className={`${styles.projectsSection} ${className || ''}`} role="region" aria-label="Projects section">
360
+ <div className={styles.projectsContainer}>
361
+ <div className={styles.projectsHeader}>
362
+ <h2 className={styles.projectsTitle}>
363
+ {title || "My Projects"}
364
+ </h2>
365
+ <p className={styles.projectsSubtitle}>
366
+ {subtitle || "A collection of all my works, with featured projects highlighted"}
367
+ </p>
368
+ </div>
369
+
370
+ {projects.length === 0 ? (
371
+ <div className={styles.noProjects}>
372
+ <p>No projects to display.</p>
373
+ </div>
374
+ ) : (
375
+ <div className={styles.carouselContainer}>
376
+ {/* Desktop navigation buttons (sides) */}
377
+ <button
378
+ className={`${styles.carouselControl} ${styles.prevButton} ${styles.desktopOnly} ${atBeginning ? styles.disabledButton : ''}`}
379
+ onClick={goToPrev}
380
+ aria-label="View previous projects"
381
+ aria-disabled={atBeginning}
382
+ type="button"
383
+ disabled={atBeginning}
384
+ >
385
+ <FaChevronLeft aria-hidden="true" />
386
+ </button>
387
+
388
+ <div className={styles.carouselWrapper} aria-roledescription="carousel" aria-label="Projects carousel">
389
+ <Slider ref={sliderRef} {...settings}>
390
+ {projects.map((project, index) => (
391
+ <div
392
+ key={project.id || project.title + index}
393
+ className={styles.carouselSlide}
394
+ data-project-id={project.id}
395
+ aria-roledescription="slide"
396
+ aria-label={`Project ${index + 1} of ${projects.length}: ${project.title}`}
397
+ >
398
+ <div className={`${styles.carouselCard} ${project.featured ? styles.featuredCard : ''}`}>
399
+ {/* Project state badge */}
400
+ {project.state && (
401
+ <div
402
+ className={styles.projectStateBadge}
403
+ title={`Project status: ${getProjectStateInfo(project.state).label}`}
404
+ >
405
+ <span className={`${styles.projectStateLabel} ${getProjectStateInfo(project.state).className}`}>
406
+ {getProjectStateInfo(project.state).label}
407
+ </span>
408
+ </div>
409
+ )}
410
+
411
+ <div className={styles.projectImageContainer}>
412
+ {project.image && (
413
+ <img
414
+ src={project.image}
415
+ alt={project.title}
416
+ className={styles.projectImage}
417
+ loading="lazy"
418
+ />
419
+ )}
420
+
421
+ {/* Featured badge */}
422
+ {project.featured && (
423
+ <div className={styles.featuredBadge} title="Featured Project" aria-label="Featured project">
424
+ <FaStar aria-hidden="true" />
425
+ </div>
426
+ )}
427
+ </div>
428
+
429
+ <div className={styles.projectContent}>
430
+ <h3 className={styles.projectTitle}>{project.title}</h3>
431
+
432
+ {project.tags?.length > 0 && (
433
+ <div className={styles.projectTags}>
434
+ {project.tags.map(tag => (
435
+ <span key={tag} className={styles.projectTag}>{tag}</span>
436
+ ))}
437
+ </div>
438
+ )}
439
+
440
+ <p className={styles.projectDescription}>{project.description}</p>
441
+ </div>
442
+
443
+ <div className={styles.projectLinks}>
444
+ {renderProjectLink(
445
+ project.website,
446
+ FaGlobe,
447
+ "Website",
448
+ `Visit ${project.title} website`
449
+ )}
450
+
451
+ {renderProjectLink(
452
+ project.github,
453
+ FaGithub,
454
+ "Source",
455
+ `Repository with source code`
456
+ )}
457
+
458
+ {renderProjectLink(
459
+ project.Demo,
460
+ FaPlay,
461
+ "Demo",
462
+ `Live demo for ${project.title}`
463
+ )}
464
+ </div>
465
+ </div>
466
+ </div>
467
+ ))}
468
+ </Slider>
469
+
470
+ {/* Desktop navigation dots */}
471
+ <div className={styles.desktopDotsContainer}>
472
+ {renderNavigationDots()}
473
+ </div>
474
+
475
+ {/* Mobile navigation controls (bottom) */}
476
+ <div className={styles.mobileNavigationControls}>
477
+ {totalPages > 1 && (
478
+ <>
479
+ <button
480
+ className={`${styles.carouselControl} ${styles.prevButton} ${atBeginning ? styles.disabledButton : ''}`}
481
+ onClick={goToPrev}
482
+ aria-label="View previous projects"
483
+ aria-disabled={atBeginning}
484
+ type="button"
485
+ disabled={atBeginning}
486
+ >
487
+ <FaChevronLeft aria-hidden="true" />
488
+ </button>
489
+
490
+ {/* Mobile navigation dots */}
491
+ <div
492
+ className={styles.dotsScrollContainer}
493
+ ref={dotsContainerRef}
494
+ >
495
+ {renderNavigationDots()}
496
+ </div>
497
+
498
+ <button
499
+ className={`${styles.carouselControl} ${styles.nextButton} ${atEnd ? styles.disabledButton : ''}`}
500
+ onClick={goToNext}
501
+ aria-label="View next projects"
502
+ aria-disabled={atEnd}
503
+ type="button"
504
+ disabled={atEnd}
505
+ >
506
+ <FaChevronRight aria-hidden="true" />
507
+ </button>
508
+ </>
509
+ )}
510
+ </div>
511
+ </div>
512
+
513
+ {/* Desktop navigation button (right side) */}
514
+ <button
515
+ className={`${styles.carouselControl} ${styles.nextButton} ${styles.desktopOnly} ${atEnd ? styles.disabledButton : ''}`}
516
+ onClick={goToNext}
517
+ aria-label="View next projects"
518
+ aria-disabled={atEnd}
519
+ type="button"
520
+ disabled={atEnd}
521
+ >
522
+ <FaChevronRight />
523
+ </button>
524
+ </div>
525
+ )}
526
+ </div>
527
+ </div>
528
+ );
529
+ }