added image support
This commit is contained in:
parent
65ed1548a7
commit
15fc3944b4
8 changed files with 255 additions and 187 deletions
4
.gitignore
vendored
4
.gitignore
vendored
|
|
@ -1 +1,3 @@
|
|||
node_modules
|
||||
node_modules
|
||||
env
|
||||
.env
|
||||
13
package-lock.json
generated
13
package-lock.json
generated
|
|
@ -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": {
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
96
src/App.js
96
src/App.js
|
|
@ -6,57 +6,55 @@ import ProjectDetails from './components/ProjectDetails';
|
|||
import projects from './data/projects';
|
||||
|
||||
function App() {
|
||||
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;
|
||||
const [selectedProjectId, setSelectedProjectId] = useState(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>
|
||||
const handleProjectClick = (projectId) => {
|
||||
setSelectedProjectId(projectId);
|
||||
window.scrollTo(0, 0);
|
||||
};
|
||||
|
||||
const handleBackClick = () => {
|
||||
setSelectedProjectId(null);
|
||||
};
|
||||
|
||||
const selectedProject = selectedProjectId ? projects.find(p => p.id === selectedProjectId) : null;
|
||||
|
||||
return (
|
||||
<div className="app">
|
||||
<Header
|
||||
onBackClick={handleBackClick}
|
||||
showBackButton={selectedProjectId !== null}
|
||||
/>
|
||||
|
||||
{projects.map(project => (
|
||||
<ProjectCard
|
||||
key={project.id}
|
||||
project={project}
|
||||
onClick={handleProjectClick}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</main>
|
||||
|
||||
<footer className="app-footer">
|
||||
<p>© {new Date().getFullYear()} Comet Technologies | All Rights Reserved</p>
|
||||
</footer>
|
||||
</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>
|
||||
|
||||
<footer className="app-footer">
|
||||
<p>© {new Date().getFullYear()} Comet Technologies | All Rights Reserved</p>
|
||||
</footer>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default App;
|
||||
|
|
@ -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}>
|
||||
← 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}>
|
||||
← Back to Projects
|
||||
</button>
|
||||
)}
|
||||
<h1>Comet Technologies</h1>
|
||||
<p className="tagline">You need I'll build it</p>
|
||||
</div>
|
||||
</header>
|
||||
);
|
||||
};
|
||||
|
||||
export default Header;
|
||||
|
|
@ -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);
|
||||
|
||||
useEffect(() => {
|
||||
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');
|
||||
|
||||
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();
|
||||
|
||||
setCurrentImageIndex(0);
|
||||
}, [projectId]);
|
||||
const getThumbnailUrl = (fileId) => {
|
||||
return `https://drive.google.com/thumbnail?id=${fileId}&sz=w300`;
|
||||
};
|
||||
|
||||
const nextImage = () => {
|
||||
setCurrentImageIndex((prevIndex) =>
|
||||
prevIndex === images.length - 1 ? 0 : prevIndex + 1
|
||||
);
|
||||
};
|
||||
useEffect(() => {
|
||||
const loadImages = async () => {
|
||||
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 = () => {
|
||||
setCurrentImageIndex((prevIndex) =>
|
||||
prevIndex === 0 ? images.length - 1 : prevIndex - 1
|
||||
);
|
||||
};
|
||||
let folderId = "1kLdnnm47c7GkmKgiyIbv2QXh7sqFZ6p4";
|
||||
|
||||
let query = encodeURIComponent(`'${folderId}' in parents and trashed=false`);
|
||||
|
||||
if (loading) {
|
||||
return <div className="carousel-loading">Loading images...</div>;
|
||||
}
|
||||
let driveResponse = await fetch(
|
||||
`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) {
|
||||
return <div className="carousel-empty">No images available</div>;
|
||||
}
|
||||
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);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="carousel">
|
||||
<button
|
||||
className="carousel-button prev"
|
||||
onClick={prevImage}
|
||||
aria-label="Previous image"
|
||||
>
|
||||
<
|
||||
</button>
|
||||
|
||||
<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 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"
|
||||
>
|
||||
<
|
||||
</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"
|
||||
>
|
||||
>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button
|
||||
className="carousel-button next"
|
||||
onClick={nextImage}
|
||||
aria-label="Next image"
|
||||
>
|
||||
>
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
|
||||
export default ImageCarousel;
|
||||
|
|
@ -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;
|
||||
|
|
@ -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>
|
||||
|
||||
<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>
|
||||
);
|
||||
return (
|
||||
<div className="project-details">
|
||||
<h2>{project.title}</h2>
|
||||
|
||||
<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>
|
||||
);
|
||||
};
|
||||
|
||||
export default ProjectDetails;
|
||||
Loading…
Reference in a new issue