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

2
.gitignore vendored
View file

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

13
package-lock.json generated
View file

@ -4513,9 +4513,9 @@
}
},
"dotenv": {
"version": "10.0.0",
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-10.0.0.tgz",
"integrity": "sha512-rlBi9d8jpv9Sf1klPjNfFAuWDjKLwTIJJ/VxtoTwIR6hnZxcEOQCZg2oIL3MWBYw5GpUDKOEnND7LXTbIpQ03Q=="
"version": "16.4.7",
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.7.tgz",
"integrity": "sha512-47qPchRCykZC03FhkYAhrvwU4xDBFIj1QPqaarj6mdM/hgUzfPHcpkHJOn3mJAufFeeAxAzeGsr5X0M4k6fLZQ=="
},
"dotenv-expand": {
"version": "5.1.0",
@ -10368,6 +10368,13 @@
"webpack-dev-server": "^4.6.0",
"webpack-manifest-plugin": "^4.0.2",
"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": {

View file

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

View file

@ -6,57 +6,55 @@ import ProjectDetails from './components/ProjectDetails';
import projects from './data/projects';
function App() {
const [selectedProjectId, setSelectedProjectId] = useState(null);
const [selectedProjectId, setSelectedProjectId] = useState(null);
const handleProjectClick = (projectId) => {
setSelectedProjectId(projectId);
window.scrollTo(0, 0);
};
const handleProjectClick = (projectId) => {
setSelectedProjectId(projectId);
window.scrollTo(0, 0);
};
const handleBackClick = () => {
setSelectedProjectId(null);
};
const handleBackClick = () => {
setSelectedProjectId(null);
};
const selectedProject = selectedProjectId
? projects.find(p => p.id === selectedProjectId)
: null;
const selectedProject = selectedProjectId ? projects.find(p => p.id === selectedProjectId) : null;
return (
<div className="app">
<Header
onBackClick={handleBackClick}
showBackButton={selectedProjectId !== null}
/>
return (
<div className="app">
<Header
onBackClick={handleBackClick}
showBackButton={selectedProjectId !== null}
/>
<main className="app-content">
{selectedProject ? (
<ProjectDetails project={selectedProject} />
) : (
<div className="projects-list">
<div className="intro">
<h2>Freelance Experience</h2>
<p>
Below is a portfolio of my freelance projects.
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>
<main className="app-content">
{selectedProject ? (
<ProjectDetails project={selectedProject} />
) : (
<div className="projects-list">
<div className="intro">
<h2>Freelance Experience</h2>
<p>
Below is a portfolio of my freelance projects.
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 => (
<ProjectCard
key={project.id}
project={project}
onClick={handleProjectClick}
/>
))}
</div>
)}
</main>
{projects.map(project => (
<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>
);
<footer className="app-footer">
<p>&copy; {new Date().getFullYear()} Comet Technologies | All Rights Reserved</p>
</footer>
</div>
);
}
export default App;

View file

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

View file

@ -1,106 +1,166 @@
import React, { useState, useEffect } from 'react';
const ImageCarousel = ({ projectId }) => {
const [images, setImages] = useState([]);
const [currentImageIndex, setCurrentImageIndex] = useState(0);
const [loading, setLoading] = useState(true);
const [images, setImages] = useState([]);
const [highResImages, setHighResImages] = useState([]);
const [fileIds, setFileIds] = useState([]);
const [currentImageIndex, setCurrentImageIndex] = useState(0);
const [loading, setLoading] = useState(true);
const [viewMode, setViewMode] = useState('thumbnail');
useEffect(() => {
const loadImages = async () => {
try {
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);
}
const getDirectDownloadUrl = (fileId) => {
return `https://lh3.google.com/u/0/d/${fileId}`
};
loadImages();
const getThumbnailUrl = (fileId) => {
return `https://drive.google.com/thumbnail?id=${fileId}&sz=w300`;
};
setCurrentImageIndex(0);
}, [projectId]);
useEffect(() => {
const loadImages = async () => {
try {
const apiKey = process.env.REACT_APP_GOOGLE_API_KEY;
const nextImage = () => {
setCurrentImageIndex((prevIndex) =>
prevIndex === images.length - 1 ? 0 : prevIndex + 1
);
};
if (!apiKey) {
throw new Error("Google API key not found in environment variables");
}
const prevImage = () => {
setCurrentImageIndex((prevIndex) =>
prevIndex === 0 ? images.length - 1 : prevIndex - 1
);
};
let folderId = "1kLdnnm47c7GkmKgiyIbv2QXh7sqFZ6p4";
if (loading) {
return <div className="carousel-loading">Loading images...</div>;
}
let query = encodeURIComponent(`'${folderId}' in parents and trashed=false`);
if (images.length === 0) {
return <div className="carousel-empty">No images available</div>;
}
let driveResponse = await fetch(
`https://www.googleapis.com/drive/v3/files?q=${query}&fields=files(id,name,mimeType)&key=${apiKey}`
);
return (
<div className="carousel">
<button
className="carousel-button prev"
onClick={prevImage}
aria-label="Previous image"
>
&lt;
</button>
if (!driveResponse.ok) {
throw new Error(`Google Drive API error: ${driveResponse.status}`);
}
<div className="carousel-image-container">
<img
src={images[currentImageIndex]}
alt={`Project ${projectId} - ${currentImageIndex + 1}`}
className="carousel-image"
/>
<div className="carousel-counter">
{currentImageIndex + 1} / {images.length}
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]);
const toggleViewMode = () => {
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);
}
};
const nextImage = () => {
if(viewMode === 'highres')
toggleViewMode();
setCurrentImageIndex((prevIndex) =>
prevIndex === images.length - 1 ? 0 : prevIndex + 1
);
};
const prevImage = () => {
if(viewMode === 'highres')
toggleViewMode();
setCurrentImageIndex((prevIndex) =>
prevIndex === 0 ? images.length - 1 : prevIndex - 1
);
};
if (loading) {
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>
<button
className="carousel-button next"
onClick={nextImage}
aria-label="Next image"
>
&gt;
</button>
</div>
);
);
};
export default ImageCarousel;

View file

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

View file

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