added image support

This commit is contained in:
2ManyProjects 2025-03-26 12:18:40 -05:00
parent 65ed1548a7
commit 15fc3944b4
8 changed files with 255 additions and 187 deletions

4
.gitignore vendored
View file

@ -1 +1,3 @@
node_modules node_modules
env
.env

13
package-lock.json generated
View file

@ -4513,9 +4513,9 @@
} }
}, },
"dotenv": { "dotenv": {
"version": "10.0.0", "version": "16.4.7",
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-10.0.0.tgz", "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.7.tgz",
"integrity": "sha512-rlBi9d8jpv9Sf1klPjNfFAuWDjKLwTIJJ/VxtoTwIR6hnZxcEOQCZg2oIL3MWBYw5GpUDKOEnND7LXTbIpQ03Q==" "integrity": "sha512-47qPchRCykZC03FhkYAhrvwU4xDBFIj1QPqaarj6mdM/hgUzfPHcpkHJOn3mJAufFeeAxAzeGsr5X0M4k6fLZQ=="
}, },
"dotenv-expand": { "dotenv-expand": {
"version": "5.1.0", "version": "5.1.0",
@ -10368,6 +10368,13 @@
"webpack-dev-server": "^4.6.0", "webpack-dev-server": "^4.6.0",
"webpack-manifest-plugin": "^4.0.2", "webpack-manifest-plugin": "^4.0.2",
"workbox-webpack-plugin": "^6.4.1" "workbox-webpack-plugin": "^6.4.1"
},
"dependencies": {
"dotenv": {
"version": "10.0.0",
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-10.0.0.tgz",
"integrity": "sha512-rlBi9d8jpv9Sf1klPjNfFAuWDjKLwTIJJ/VxtoTwIR6hnZxcEOQCZg2oIL3MWBYw5GpUDKOEnND7LXTbIpQ03Q=="
}
} }
}, },
"read-cache": { "read-cache": {

View file

@ -3,6 +3,7 @@
"version": "0.1.0", "version": "0.1.0",
"private": true, "private": true,
"dependencies": { "dependencies": {
"dotenv": "^16.4.7",
"react": "^18.2.0", "react": "^18.2.0",
"react-dom": "^18.2.0", "react-dom": "^18.2.0",
"react-scripts": "5.0.1" "react-scripts": "5.0.1"

View file

@ -6,57 +6,55 @@ import ProjectDetails from './components/ProjectDetails';
import projects from './data/projects'; import projects from './data/projects';
function App() { function App() {
const [selectedProjectId, setSelectedProjectId] = useState(null); const [selectedProjectId, setSelectedProjectId] = useState(null);
const handleProjectClick = (projectId) => {
setSelectedProjectId(projectId);
window.scrollTo(0, 0);
};
const handleBackClick = () => {
setSelectedProjectId(null);
};
const selectedProject = selectedProjectId
? projects.find(p => p.id === selectedProjectId)
: null;
return ( const handleProjectClick = (projectId) => {
<div className="app"> setSelectedProjectId(projectId);
<Header window.scrollTo(0, 0);
onBackClick={handleBackClick} };
showBackButton={selectedProjectId !== null}
/> const handleBackClick = () => {
setSelectedProjectId(null);
<main className="app-content"> };
{selectedProject ? (
<ProjectDetails project={selectedProject} /> const selectedProject = selectedProjectId ? projects.find(p => p.id === selectedProjectId) : null;
) : (
<div className="projects-list"> return (
<div className="intro"> <div className="app">
<h2>Freelance Experience</h2> <Header
<p> onBackClick={handleBackClick}
Below is a portfolio of my freelance projects. showBackButton={selectedProjectId !== null}
Each project represents solutions to complex problems across various domains. If you've been given this site please feel free to contact me for more details that can't be made publicly available. />
</p>
</div>
{projects.map(project => ( <main className="app-content">
<ProjectCard {selectedProject ? (
key={project.id} <ProjectDetails project={selectedProject} />
project={project} ) : (
onClick={handleProjectClick} <div className="projects-list">
/> <div className="intro">
))} <h2>Freelance Experience</h2>
</div> <p>
)} Below is a portfolio of my freelance projects.
</main> Each project represents solutions to complex problems across various domains. If you've been given this site please feel free to contact me for more details that can't be made publicly available.
</p>
<footer className="app-footer"> </div>
<p>&copy; {new Date().getFullYear()} Comet Technologies | All Rights Reserved</p>
</footer> {projects.map(project => (
</div> <ProjectCard
); key={project.id}
project={project}
onClick={handleProjectClick}
/>
))}
</div>
)}
</main>
<footer className="app-footer">
<p>&copy; {new Date().getFullYear()} Comet Technologies | All Rights Reserved</p>
</footer>
</div>
);
} }
export default App; export default App;

View file

@ -1,19 +1,19 @@
import React from 'react'; import React from 'react';
const Header = ({ onBackClick, showBackButton }) => { const Header = ({ onBackClick, showBackButton }) => {
return ( return (
<header className="app-header"> <header className="app-header">
<div className="header-content"> <div className="header-content">
{showBackButton && ( {showBackButton && (
<button className="back-button" onClick={onBackClick}> <button className="back-button" onClick={onBackClick}>
&larr; Back to Projects &larr; Back to Projects
</button> </button>
)} )}
<h1>Comet Technologies</h1> <h1>Comet Technologies</h1>
<p className="tagline">You need I'll build it</p> <p className="tagline">You need I'll build it</p>
</div> </div>
</header> </header>
); );
}; };
export default Header; export default Header;

View file

@ -1,106 +1,166 @@
import React, { useState, useEffect } from 'react'; import React, { useState, useEffect } from 'react';
const ImageCarousel = ({ projectId }) => { const ImageCarousel = ({ projectId }) => {
const [images, setImages] = useState([]); const [images, setImages] = useState([]);
const [currentImageIndex, setCurrentImageIndex] = useState(0); const [highResImages, setHighResImages] = useState([]);
const [loading, setLoading] = useState(true); const [fileIds, setFileIds] = useState([]);
const [currentImageIndex, setCurrentImageIndex] = useState(0);
useEffect(() => { const [loading, setLoading] = useState(true);
const [viewMode, setViewMode] = useState('thumbnail');
const loadImages = async () => { const getDirectDownloadUrl = (fileId) => {
try { return `https://lh3.google.com/u/0/d/${fileId}`
const importAll = (r) => r.keys().map(r);
let imageContext = null;
try {
switch(projectId){
case "mycology-lab":
imageContext = require.context(`../assets/mycology-lab`, false, /\.(png|jpe?g|svg)$/);
break
case "loanterra":
imageContext = require.context(`../assets/loanterra`, false, /\.(png|jpe?g|svg)$/);
break
case "orchard-market":
imageContext = require.context(`../assets/orchard-market`, false, /\.(png|jpe?g|svg)$/);
break
case "fecal-vision-model":
imageContext = require.context(`../assets/fecal-vision-model`, false, /\.(png|jpe?g|svg)$/);
break
default:
imageContext = require.context(`../assets/mycology-lab`, false, /\.(png|jpe?g|svg)$/);
}
} catch (e) {
console.log("Error", e);
}
if(imageContext){
const imageFiles = importAll(imageContext);
setImages(imageFiles);
}
} catch (error) {
console.error("Error loading images:", error);
setImages([]);
} finally {
setLoading(false);
}
}; };
loadImages(); const getThumbnailUrl = (fileId) => {
return `https://drive.google.com/thumbnail?id=${fileId}&sz=w300`;
setCurrentImageIndex(0); };
}, [projectId]);
const nextImage = () => { useEffect(() => {
setCurrentImageIndex((prevIndex) => const loadImages = async () => {
prevIndex === images.length - 1 ? 0 : prevIndex + 1 try {
); const apiKey = process.env.REACT_APP_GOOGLE_API_KEY;
};
if (!apiKey) {
throw new Error("Google API key not found in environment variables");
}
const prevImage = () => { let folderId = "1kLdnnm47c7GkmKgiyIbv2QXh7sqFZ6p4";
setCurrentImageIndex((prevIndex) =>
prevIndex === 0 ? images.length - 1 : prevIndex - 1 let query = encodeURIComponent(`'${folderId}' in parents and trashed=false`);
);
};
if (loading) { let driveResponse = await fetch(
return <div className="carousel-loading">Loading images...</div>; `https://www.googleapis.com/drive/v3/files?q=${query}&fields=files(id,name,mimeType)&key=${apiKey}`
} );
if (!driveResponse.ok) {
throw new Error(`Google Drive API error: ${driveResponse.status}`);
}
const folderData = await driveResponse.json();
let foundFolder = folderData.files.filter(file => file.mimeType.startsWith('application/')).find(item => item.name === projectId);
if (foundFolder) {
folderId = foundFolder.id;
query = encodeURIComponent(`'${folderId}' in parents and trashed=false`);
driveResponse = await fetch(
`https://www.googleapis.com/drive/v3/files?q=${query}&fields=files(id,name,mimeType)&key=${apiKey}`
);
const fileData = await driveResponse.json();
const ids = fileData.files
.filter(file => file.mimeType.startsWith('image/'))
.map(file => file.id);
setFileIds(ids);
const thumbnailUrls = ids.map(id => getThumbnailUrl(id));
setImages(thumbnailUrls);
}
} catch (error) {
console.error("Error loading images:", error);
setImages([]);
} finally {
setLoading(false);
}
};
loadImages();
setCurrentImageIndex(0);
setViewMode('thumbnail');
}, [projectId]);
if (images.length === 0) { const toggleViewMode = () => {
return <div className="carousel-empty">No images available</div>; if (viewMode === 'thumbnail') {
} setViewMode('highres');
setHighResImages([]);
setLoading(true);
const highResUrls = fileIds.map(id => getDirectDownloadUrl(id));
setHighResImages(highResUrls);
setLoading(false);
} else {
setViewMode('thumbnail');
setImages([]);
setLoading(true);
const thumbnailUrls = fileIds.map(id => getThumbnailUrl(id));
setImages(thumbnailUrls);
setLoading(false);
}
};
return ( const nextImage = () => {
<div className="carousel"> if(viewMode === 'highres')
<button toggleViewMode();
className="carousel-button prev" setCurrentImageIndex((prevIndex) =>
onClick={prevImage} prevIndex === images.length - 1 ? 0 : prevIndex + 1
aria-label="Previous image" );
> };
&lt;
</button> const prevImage = () => {
if(viewMode === 'highres')
<div className="carousel-image-container"> toggleViewMode();
<img setCurrentImageIndex((prevIndex) =>
src={images[currentImageIndex]} prevIndex === 0 ? images.length - 1 : prevIndex - 1
alt={`Project ${projectId} - ${currentImageIndex + 1}`} );
className="carousel-image" };
/>
<div className="carousel-counter"> if (loading) {
{currentImageIndex + 1} / {images.length} return <div className="carousel-loading">Loading images...</div>;
}
if (images.length === 0) {
return <div className="carousel-empty">No images available</div>;
}
const addDefaultImg = ev => {
// ev.target.src = images[currentImageIndex]
ev.target.src = 'https://upload.wikimedia.org/wikipedia/commons/1/14/No_Image_Available.jpg'
}
return (
<div className="carousel">
<button
className="carousel-button prev"
onClick={prevImage}
aria-label="Previous image"
>
&lt;
</button>
<div className="carousel-image-container">
<img
src={viewMode === "thumbnail" ? images[currentImageIndex] : highResImages[currentImageIndex]}
alt={`Project ${projectId} - ${currentImageIndex + 1}`}
className="carousel-image"
onClick={toggleViewMode}
onError={(e) => addDefaultImg}
style={{
width: '100%',
height: 'auto'
}}
/>
<div className="carousel-counter">
{currentImageIndex + 1} / {images.length}
{viewMode === 'thumbnail' && (
<span className="image-quality-indicator"> [SD](Click Image for HD)</span>
)}
{viewMode === 'highres' && (
<span className="image-quality-indicator"> [HD](Click Image for SD)</span>
)}
</div>
</div>
<button
className="carousel-button next"
onClick={nextImage}
aria-label="Next image"
>
&gt;
</button>
</div> </div>
</div> );
<button
className="carousel-button next"
onClick={nextImage}
aria-label="Next image"
>
&gt;
</button>
</div>
);
}; };
export default ImageCarousel; export default ImageCarousel;

View file

@ -1,17 +1,17 @@
import React from 'react'; import React from 'react';
const ProjectCard = ({ project, onClick }) => { const ProjectCard = ({ project, onClick }) => {
const summary = project.description.trim().split('\n\n')[0].trim(); const summary = project.description.trim().split('\n\n')[0].trim();
const summaryLength = 120; const summaryLength = 120;
return ( return (
<div className="project-card" onClick={() => onClick(project.id)}> <div className="project-card" onClick={() => onClick(project.id)}>
<h3>{project.title}</h3> <h3>{project.title}</h3>
<p className="project-summary"> <p className="project-summary">
{summary.length > summaryLength ? `${summary.substring(0, summaryLength)}...` : summary} {summary.length > summaryLength ? `${summary.substring(0, summaryLength)}...` : summary}
</p> </p>
<div className="view-details">View Details</div> <div className="view-details">View Details</div>
</div> </div>
); );
}; };
export default ProjectCard; export default ProjectCard;

View file

@ -2,27 +2,27 @@ import React from 'react';
import ImageCarousel from './ImageCarousel'; import ImageCarousel from './ImageCarousel';
const ProjectDetails = ({ project }) => { const ProjectDetails = ({ project }) => {
if (!project) { if (!project) {
return <div className="project-not-found">Project not found</div>; return <div className="project-not-found">Project not found</div>;
} }
const paragraphs = project.description.trim().split('\n\n'); const paragraphs = project.description.trim().split('\n\n');
return ( return (
<div className="project-details"> <div className="project-details">
<h2>{project.title}</h2> <h2>{project.title}</h2>
<div className="project-carousel"> <div className="project-carousel">
<ImageCarousel projectId={project.id} /> <ImageCarousel projectId={project.id} />
</div> </div>
<div className="project-description"> <div className="project-description">
{paragraphs.map((paragraph, index) => ( {paragraphs.map((paragraph, index) => (
<p key={index}>{paragraph.trim()}</p> <p key={index}>{paragraph.trim()}</p>
))} ))}
</div> </div>
</div> </div>
); );
}; };
export default ProjectDetails; export default ProjectDetails;