Initial commit
This commit is contained in:
commit
65ed1548a7
13 changed files with 13292 additions and 0 deletions
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
|
|
@ -0,0 +1 @@
|
|||
node_modules
|
||||
12685
package-lock.json
generated
Normal file
12685
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load diff
33
package.json
Normal file
33
package.json
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
{
|
||||
"name": "comet-technologies",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-scripts": "5.0.1"
|
||||
},
|
||||
"scripts": {
|
||||
"start": "react-scripts start",
|
||||
"build": "react-scripts build",
|
||||
"test": "react-scripts test",
|
||||
"eject": "react-scripts eject"
|
||||
},
|
||||
"eslintConfig": {
|
||||
"extends": [
|
||||
"react-app"
|
||||
]
|
||||
},
|
||||
"browserslist": {
|
||||
"production": [
|
||||
">0.2%",
|
||||
"not dead",
|
||||
"not op_mini all"
|
||||
],
|
||||
"development": [
|
||||
"last 1 chrome version",
|
||||
"last 1 firefox version",
|
||||
"last 1 safari version"
|
||||
]
|
||||
}
|
||||
}
|
||||
BIN
public/favicon.ico
Normal file
BIN
public/favicon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 15 KiB |
18
public/index.html
Normal file
18
public/index.html
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<meta name="theme-color" content="#1a1a2e" />
|
||||
<meta
|
||||
name="description"
|
||||
content="Comet Technologies - Innovative Solutions for Complex Problems"
|
||||
/>
|
||||
<title>Comet Technologies | Freelance Portfolio</title>
|
||||
</head>
|
||||
<body>
|
||||
<noscript>You need to enable JavaScript to run this app.</noscript>
|
||||
<div id="root"></div>
|
||||
</body>
|
||||
</html>
|
||||
248
src/App.css
Normal file
248
src/App.css
Normal file
|
|
@ -0,0 +1,248 @@
|
|||
/* Base Styles */
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
||||
line-height: 1.6;
|
||||
color: #333;
|
||||
background-color: #f8f9fa;
|
||||
}
|
||||
|
||||
.app {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
/* Header Styles */
|
||||
.app-header {
|
||||
background-color: #1a1a2e;
|
||||
color: white;
|
||||
padding: 2rem 1rem;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.header-content {
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.back-button {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
background: none;
|
||||
border: none;
|
||||
color: white;
|
||||
font-size: 1rem;
|
||||
cursor: pointer;
|
||||
padding: 0.5rem;
|
||||
}
|
||||
|
||||
.back-button:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.app-header h1 {
|
||||
font-size: 2.5rem;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.tagline {
|
||||
font-size: 1.1rem;
|
||||
font-weight: 300;
|
||||
}
|
||||
|
||||
/* Main Content */
|
||||
.app-content {
|
||||
flex: 1;
|
||||
padding: 2rem 1rem;
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
/* Project List */
|
||||
.projects-list {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
|
||||
gap: 2rem;
|
||||
}
|
||||
|
||||
.intro {
|
||||
grid-column: 1 / -1;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.intro h2 {
|
||||
font-size: 2rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.intro p {
|
||||
font-size: 1.1rem;
|
||||
color: #555;
|
||||
}
|
||||
|
||||
/* Project Card */
|
||||
.project-card {
|
||||
background-color: white;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
|
||||
padding: 1.5rem;
|
||||
transition: transform 0.3s ease, box-shadow 0.3s ease;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.project-card:hover {
|
||||
transform: translateY(-5px);
|
||||
box-shadow: 0 8px 16px rgba(0, 0, 0, 0.15);
|
||||
}
|
||||
|
||||
.project-card h3 {
|
||||
margin-bottom: 1rem;
|
||||
color: #1a1a2e;
|
||||
}
|
||||
|
||||
.project-summary {
|
||||
margin-bottom: 1.5rem;
|
||||
color: #555;
|
||||
}
|
||||
|
||||
.view-details {
|
||||
text-align: right;
|
||||
color: #16213e;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
/* Project Details */
|
||||
.project-details {
|
||||
background-color: white;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
|
||||
padding: 2rem;
|
||||
}
|
||||
|
||||
.project-details h2 {
|
||||
color: #1a1a2e;
|
||||
margin-bottom: 1.5rem;
|
||||
font-size: 1.8rem;
|
||||
}
|
||||
|
||||
.project-carousel {
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.project-description p {
|
||||
margin-bottom: 1rem;
|
||||
font-size: 1.05rem;
|
||||
}
|
||||
|
||||
/* Carousel Styles */
|
||||
.carousel {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
position: relative;
|
||||
background-color: #f1f1f1;
|
||||
border-radius: 4px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.carousel-image-container {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
position: relative;
|
||||
height: 400px;
|
||||
}
|
||||
|
||||
.carousel-image {
|
||||
max-width: 100%;
|
||||
max-height: 100%;
|
||||
object-fit: contain;
|
||||
}
|
||||
|
||||
.carousel-button {
|
||||
background-color: rgba(0, 0, 0, 0.5);
|
||||
border: none;
|
||||
color: white;
|
||||
font-size: 1.5rem;
|
||||
padding: 0.5rem 1rem;
|
||||
cursor: pointer;
|
||||
height: 100%;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
.carousel-button:hover {
|
||||
background-color: rgba(0, 0, 0, 0.7);
|
||||
}
|
||||
|
||||
.carousel-counter {
|
||||
position: absolute;
|
||||
bottom: 10px;
|
||||
right: 10px;
|
||||
background-color: rgba(0, 0, 0, 0.6);
|
||||
color: white;
|
||||
padding: 0.25rem 0.5rem;
|
||||
border-radius: 4px;
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
|
||||
.carousel-loading, .carousel-empty {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
height: 300px;
|
||||
background-color: #f1f1f1;
|
||||
color: #555;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
/* Footer */
|
||||
.app-footer {
|
||||
background-color: #1a1a2e;
|
||||
color: white;
|
||||
text-align: center;
|
||||
padding: 1.5rem;
|
||||
margin-top: 2rem;
|
||||
}
|
||||
|
||||
/* Responsive styles */
|
||||
@media (max-width: 768px) {
|
||||
.header-content {
|
||||
padding: 0 1rem;
|
||||
}
|
||||
|
||||
.back-button {
|
||||
position: static;
|
||||
transform: none;
|
||||
margin-bottom: 1rem;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.project-details {
|
||||
padding: 1.5rem;
|
||||
}
|
||||
|
||||
.carousel-image-container {
|
||||
height: 300px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 480px) {
|
||||
.app-header h1 {
|
||||
font-size: 2rem;
|
||||
}
|
||||
|
||||
.carousel-image-container {
|
||||
height: 200px;
|
||||
}
|
||||
}
|
||||
62
src/App.js
Normal file
62
src/App.js
Normal file
|
|
@ -0,0 +1,62 @@
|
|||
import React, { useState } from 'react';
|
||||
import './App.css';
|
||||
import Header from './components/Header';
|
||||
import ProjectCard from './components/ProjectCard';
|
||||
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;
|
||||
|
||||
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>
|
||||
|
||||
{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;
|
||||
19
src/components/Header.js
Normal file
19
src/components/Header.js
Normal file
|
|
@ -0,0 +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>
|
||||
);
|
||||
};
|
||||
|
||||
export default Header;
|
||||
106
src/components/ImageCarousel.js
Normal file
106
src/components/ImageCarousel.js
Normal file
|
|
@ -0,0 +1,106 @@
|
|||
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 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();
|
||||
|
||||
setCurrentImageIndex(0);
|
||||
}, [projectId]);
|
||||
|
||||
const nextImage = () => {
|
||||
setCurrentImageIndex((prevIndex) =>
|
||||
prevIndex === images.length - 1 ? 0 : prevIndex + 1
|
||||
);
|
||||
};
|
||||
|
||||
const prevImage = () => {
|
||||
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>;
|
||||
}
|
||||
|
||||
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}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button
|
||||
className="carousel-button next"
|
||||
onClick={nextImage}
|
||||
aria-label="Next image"
|
||||
>
|
||||
>
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ImageCarousel;
|
||||
17
src/components/ProjectCard.js
Normal file
17
src/components/ProjectCard.js
Normal file
|
|
@ -0,0 +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>
|
||||
);
|
||||
};
|
||||
|
||||
export default ProjectCard;
|
||||
28
src/components/ProjectDetails.js
Normal file
28
src/components/ProjectDetails.js
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
import React from 'react';
|
||||
import ImageCarousel from './ImageCarousel';
|
||||
|
||||
const ProjectDetails = ({ project }) => {
|
||||
if (!project) {
|
||||
return <div className="project-not-found">Project not found</div>;
|
||||
}
|
||||
|
||||
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>
|
||||
);
|
||||
};
|
||||
|
||||
export default ProjectDetails;
|
||||
64
src/data/projects.js
Normal file
64
src/data/projects.js
Normal file
|
|
@ -0,0 +1,64 @@
|
|||
const projects = [
|
||||
{
|
||||
id: "loanterra",
|
||||
title: "DevOps for Loanterra (2023-2025)",
|
||||
imagesFolder: "/assets/loanterra",
|
||||
description: `
|
||||
- Setup Loanterra Cloud environment with Okta for authentication and resource provisioning.\n\n
|
||||
- Ensured SOC2 Compliance through implementation of security controls and documentation.\n\n
|
||||
- DNS setup for mail server and custom vanity urls.\n\n
|
||||
- Integrated with Turnkey Lender for Loan management.\n\n
|
||||
- Built an automated backup SQL ingester and tester on AWS to ingest SQL backups from TurnKey's Azure environment.\n\n
|
||||
- Multiple Email tenancy migrations.\n\n
|
||||
- Assessed old site for cost optimizations (identified payments being charged for free wordpress addons).
|
||||
`
|
||||
},
|
||||
{
|
||||
id: "fecal-vision-model",
|
||||
title: "Fecal Vision Model for North of 55 Veterinary Services (2022)",
|
||||
imagesFolder: "/assets/fecal-vision-model",
|
||||
description: `
|
||||
- Over 1 year I developed a vision model using TensorFlow that was ~90% accurate (sufficient for medical diagnoses) in fecal egg counts using standard McMaster method fecal slides. This reduced slide analysis time from 20 minutes down to 5 minutes per sample.\n\n
|
||||
|
||||
- Commercial alternatives like WSI systems start around $90,000USD, making this a cost-effective solution.\n\n
|
||||
|
||||
- I modified a microscope to automate slide scanning and created data pipeline for high-quality slide scans for training purposes.\n\n
|
||||
|
||||
- Using QuPath's scripting capabilities and sample annotated slides, I created a pipeline where the veterinarian only needed to take slide scans using a custom modified microscope, place the scans in a network folder, and receive analysis results in Excel format.\n\n
|
||||
`
|
||||
},
|
||||
{
|
||||
id: "orchard-market",
|
||||
title: "Order Picking Software for Orchard Market (2021)",
|
||||
imagesFolder: "/assets/orchard-market",
|
||||
description: `
|
||||
I built several pieces of software for Orchard Market, a collaboration of 5 farms sharing warehousing and storefront space.\n\n
|
||||
|
||||
The order picking software aggregated inventory across all 5 disparate inventory systems, allowing orders to be efficiently processed based on availability and delivery time. This reduced order fulfillment time by 15%.\n\n
|
||||
|
||||
Depending on the inventory system, integrations were written in either JavaScript (for Excel-based systems) or C# (Produce Pro) to send data to a central server running Croptracker.\n\n
|
||||
|
||||
Implemented phone image text recognition for handwritten order forms to quickly and accurately update the inventory system, reducing data entry errors by 50%. Python scripts extract handwriting from order forms, using QR codes to determine form types and apply appropriate analysis.\n\n
|
||||
`
|
||||
},
|
||||
{
|
||||
id: "mycology-lab",
|
||||
title: "Mycology Lab Technical Lead (2019-2022)",
|
||||
imagesFolder: "/assets/mycology-lab",
|
||||
description: `
|
||||
Fungal Stimuli Tester:\n\n
|
||||
Built a 3-axis gantry system that moves a central platform along an x,y plane with a winch that raises/lowers the end piece.\n\n
|
||||
The system features modular end effectors with multiple stimuli options including temperature probes, liquid dispensers, laser pointers, and more.\n\n
|
||||
Used 8 32-bit ADCs to record signal propagation within mycelial networks, with Fast Fourier Transform for spike detection and wavelet processing methods. \n\n
|
||||
Data logged to PostgreSQL for analysis. System built in C++.\n\n\n\n\n\n
|
||||
|
||||
Mycelium Bioreactor:\n\n
|
||||
Automated bioreactor for growing liquid mycelium culture with automated temperature, oxygen, stirring, and harvesting control. Implemented sterilization systems and contamination detection using OpenCV computer vision. System built in C++.\n\n\n\n\n\n
|
||||
|
||||
Growth Chamber:\n\n
|
||||
9-cell growth chamber with individual temperature control for each level, optimized for mycelial remediation experiments on contaminated cardboard. The system logged environmental variables and growth progress, allowing for optimization of growing conditions. System built in C++.
|
||||
`
|
||||
}
|
||||
];
|
||||
|
||||
export default projects;
|
||||
11
src/index.js
Normal file
11
src/index.js
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
import React from 'react';
|
||||
import ReactDOM from 'react-dom/client';
|
||||
import './App.css';
|
||||
import App from './App';
|
||||
|
||||
const root = ReactDOM.createRoot(document.getElementById('root'));
|
||||
root.render(
|
||||
<React.StrictMode>
|
||||
<App />
|
||||
</React.StrictMode>
|
||||
);
|
||||
Loading…
Reference in a new issue