added image support
This commit is contained in:
parent
65ed1548a7
commit
15fc3944b4
8 changed files with 255 additions and 187 deletions
2
.gitignore
vendored
2
.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": {
|
"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": {
|
||||||
|
|
|
||||||
|
|
@ -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"
|
||||||
|
|
|
||||||
86
src/App.js
86
src/App.js
|
|
@ -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) => {
|
const handleProjectClick = (projectId) => {
|
||||||
setSelectedProjectId(projectId);
|
setSelectedProjectId(projectId);
|
||||||
window.scrollTo(0, 0);
|
window.scrollTo(0, 0);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleBackClick = () => {
|
const handleBackClick = () => {
|
||||||
setSelectedProjectId(null);
|
setSelectedProjectId(null);
|
||||||
};
|
};
|
||||||
|
|
||||||
const selectedProject = selectedProjectId
|
const selectedProject = selectedProjectId ? projects.find(p => p.id === selectedProjectId) : null;
|
||||||
? projects.find(p => p.id === selectedProjectId)
|
|
||||||
: null;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="app">
|
<div className="app">
|
||||||
<Header
|
<Header
|
||||||
onBackClick={handleBackClick}
|
onBackClick={handleBackClick}
|
||||||
showBackButton={selectedProjectId !== null}
|
showBackButton={selectedProjectId !== null}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<main className="app-content">
|
<main className="app-content">
|
||||||
{selectedProject ? (
|
{selectedProject ? (
|
||||||
<ProjectDetails project={selectedProject} />
|
<ProjectDetails project={selectedProject} />
|
||||||
) : (
|
) : (
|
||||||
<div className="projects-list">
|
<div className="projects-list">
|
||||||
<div className="intro">
|
<div className="intro">
|
||||||
<h2>Freelance Experience</h2>
|
<h2>Freelance Experience</h2>
|
||||||
<p>
|
<p>
|
||||||
Below is a portfolio of my freelance projects.
|
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.
|
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>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{projects.map(project => (
|
{projects.map(project => (
|
||||||
<ProjectCard
|
<ProjectCard
|
||||||
key={project.id}
|
key={project.id}
|
||||||
project={project}
|
project={project}
|
||||||
onClick={handleProjectClick}
|
onClick={handleProjectClick}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
<footer className="app-footer">
|
<footer className="app-footer">
|
||||||
<p>© {new Date().getFullYear()} Comet Technologies | All Rights Reserved</p>
|
<p>© {new Date().getFullYear()} Comet Technologies | All Rights Reserved</p>
|
||||||
</footer>
|
</footer>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default App;
|
export default App;
|
||||||
|
|
@ -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}>
|
||||||
← Back to Projects
|
← 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;
|
||||||
|
|
@ -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);
|
||||||
|
const [loading, setLoading] = useState(true);
|
||||||
|
const [viewMode, setViewMode] = useState('thumbnail');
|
||||||
|
|
||||||
useEffect(() => {
|
const getDirectDownloadUrl = (fileId) => {
|
||||||
|
return `https://lh3.google.com/u/0/d/${fileId}`
|
||||||
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);
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
loadImages();
|
const getThumbnailUrl = (fileId) => {
|
||||||
|
return `https://drive.google.com/thumbnail?id=${fileId}&sz=w300`;
|
||||||
|
};
|
||||||
|
|
||||||
setCurrentImageIndex(0);
|
useEffect(() => {
|
||||||
}, [projectId]);
|
const loadImages = async () => {
|
||||||
|
try {
|
||||||
|
const apiKey = process.env.REACT_APP_GOOGLE_API_KEY;
|
||||||
|
|
||||||
const nextImage = () => {
|
if (!apiKey) {
|
||||||
setCurrentImageIndex((prevIndex) =>
|
throw new Error("Google API key not found in environment variables");
|
||||||
prevIndex === images.length - 1 ? 0 : prevIndex + 1
|
}
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const prevImage = () => {
|
let folderId = "1kLdnnm47c7GkmKgiyIbv2QXh7sqFZ6p4";
|
||||||
setCurrentImageIndex((prevIndex) =>
|
|
||||||
prevIndex === 0 ? images.length - 1 : prevIndex - 1
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
if (loading) {
|
let query = encodeURIComponent(`'${folderId}' in parents and trashed=false`);
|
||||||
return <div className="carousel-loading">Loading images...</div>;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (images.length === 0) {
|
let driveResponse = await fetch(
|
||||||
return <div className="carousel-empty">No images available</div>;
|
`https://www.googleapis.com/drive/v3/files?q=${query}&fields=files(id,name,mimeType)&key=${apiKey}`
|
||||||
}
|
);
|
||||||
|
|
||||||
return (
|
if (!driveResponse.ok) {
|
||||||
<div className="carousel">
|
throw new Error(`Google Drive API error: ${driveResponse.status}`);
|
||||||
<button
|
}
|
||||||
className="carousel-button prev"
|
|
||||||
onClick={prevImage}
|
|
||||||
aria-label="Previous image"
|
|
||||||
>
|
|
||||||
<
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<div className="carousel-image-container">
|
const folderData = await driveResponse.json();
|
||||||
<img
|
let foundFolder = folderData.files.filter(file => file.mimeType.startsWith('application/')).find(item => item.name === projectId);
|
||||||
src={images[currentImageIndex]}
|
|
||||||
alt={`Project ${projectId} - ${currentImageIndex + 1}`}
|
if (foundFolder) {
|
||||||
className="carousel-image"
|
folderId = foundFolder.id;
|
||||||
/>
|
query = encodeURIComponent(`'${folderId}' in parents and trashed=false`);
|
||||||
<div className="carousel-counter">
|
driveResponse = await fetch(
|
||||||
{currentImageIndex + 1} / {images.length}
|
`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"
|
||||||
|
>
|
||||||
|
<
|
||||||
|
</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>
|
||||||
</div>
|
);
|
||||||
|
|
||||||
<button
|
|
||||||
className="carousel-button next"
|
|
||||||
onClick={nextImage}
|
|
||||||
aria-label="Next image"
|
|
||||||
>
|
|
||||||
>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
export default ImageCarousel;
|
export default ImageCarousel;
|
||||||
|
|
@ -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;
|
||||||
|
|
@ -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;
|
||||||
Loading…
Reference in a new issue