Compare commits

..

No commits in common. "7f6c9c26806486de1e792e46c2160e78e4c52763" and "dc780a3ec4185e3be53986678f46e073671f616d" have entirely different histories.

20 changed files with 183 additions and 385 deletions

View file

@ -1,124 +1,123 @@
steps: steps:
build: build:
image: node:22 image: node:22
commands: commands:
- npm ci - npm ci
- npm run build - npm run build
- echo "VERSION=$(cat version.txt)" > .env - echo "VERSION=$(cat version.txt)" > .env
clear-from-host: clear-from-host:
image: appleboy/drone-ssh image: appleboy/drone-ssh
settings: settings:
host: host:
from_secret: ssh_host from_secret: ssh_host
username: username:
from_secret: ssh_username from_secret: ssh_username
key: key:
from_secret: ssh_key from_secret: ssh_key
port: 2332 port: 2332
script: script:
- cd /home/mnisyif/docker-containers/mnisyif/frontend - cd /home/mnisyif/docker-containers/mnisyif/frontend
- rm -rf data index-*.js index-*.css logos papers pp projects - rm -rf data index-*.js index-*.css logos papers pp projects
- find . -maxdepth 1 -type f -delete - find . -maxdepth 1 -type f -delete
- echo "Target directory cleared, resumes folder preserved" - echo "Target directory cleared, resumes folder preserved"
copy-to-host: copy-to-host:
image: appleboy/drone-scp image: appleboy/drone-scp
settings: settings:
host: host:
from_secret: ssh_host from_secret: ssh_host
username: username:
from_secret: ssh_username from_secret: ssh_username
key: key:
from_secret: ssh_key from_secret: ssh_key
port: 2332 port: 2332
target: /home/mnisyif/docker-containers/mnisyif/frontend target: /home/mnisyif/docker-containers/mnisyif/frontend
source: source:
- dist/ - dist/
- nginx.conf - nginx.conf
- version.txt - version.txt
deploy:
image: appleboy/drone-ssh
settings:
host:
from_secret: ssh_host
username:
from_secret: ssh_username
key:
from_secret: ssh_key
port: 2332
script:
- cd /home/mnisyif/docker-containers/mnisyif/frontend
- VERSION=$(cat version.txt)
- echo "Nginx configuration:"
- cat nginx.conf
- echo "Contents of dist directory:"
- ls -la dist
# Stop and remove the existing container if it exists
- docker stop frontend || true
- docker rm frontend || true
# Run the new container with the current version, mounting the files
- >
docker run -d --name frontend -p 5173:80
-v /home/mnisyif/docker-containers/mnisyif/frontend/dist:/usr/share/nginx/html:ro
-v /home/mnisyif/docker-containers/mnisyif/frontend/nginx.conf:/etc/nginx/nginx.conf:ro
nginx:alpine
# Tag the running container with the version
- docker tag nginx:alpine frontend:$VERSION
- echo "Deployment completed"
deploy: confirm-deployment:
image: appleboy/drone-ssh image: appleboy/drone-ssh
settings: settings:
host: host:
from_secret: ssh_host from_secret: ssh_host
username: username:
from_secret: ssh_username from_secret: ssh_username
key: key:
from_secret: ssh_key from_secret: ssh_key
port: 2332 port: 2332
script: script:
- cd /home/mnisyif/docker-containers/mnisyif/frontend - cd /home/mnisyif/docker-containers/mnisyif/frontend
- VERSION=$(cat version.txt) - VERSION=$(cat version.txt)
- echo "Nginx configuration:" - echo "Confirming deployment for version: $VERSION"
- cat nginx.conf - docker ps -a
- echo "Contents of dist directory:" - if ! docker ps | grep -q frontend-$VERSION; then
- ls -la dist echo "Container failed to start";
# Stop and remove the existing container if it exists docker logs frontend-$VERSION;
- docker stop frontend || true exit 1;
- docker rm frontend || true fi
# Run the new container with the current version, mounting the files - echo "Container is running, checking Nginx configuration..."
- > - docker exec frontend-$VERSION nginx -t || { echo "Nginx configuration test failed"; exit 1; }
docker run -d --name frontend -p 5173:80 - echo "Listing contents of /usr/share/nginx/html"
-v /home/mnisyif/docker-containers/mnisyif/frontend/dist:/usr/share/nginx/html:ro - docker exec frontend-$VERSION ls -la /usr/share/nginx/html
-v /home/mnisyif/docker-containers/mnisyif/frontend/nginx.conf:/etc/nginx/nginx.conf:ro - echo "Listing contents of /usr/share/nginx/html/resumes"
nginx:alpine - docker exec frontend-$VERSION ls -la /usr/share/nginx/html/resumes || echo "Resumes directory not found"
# Tag the running container with the version - echo "Checking HTTP response..."
- docker tag nginx:alpine frontend:$VERSION - curl -I http://localhost:5173 || { echo "HTTP request failed"; exit 1; }
- echo "Deployment completed" - echo "Deployment confirmed successfully"
confirm-deployment: cleanup:
image: appleboy/drone-ssh image: appleboy/drone-ssh
settings: settings:
host: host:
from_secret: ssh_host from_secret: ssh_host
username: username:
from_secret: ssh_username from_secret: ssh_username
key: key:
from_secret: ssh_key from_secret: ssh_key
port: 2332 port: 2332
script: script:
- echo "Verifying deployment..." - echo "Performing cleanup..."
# Verify the container is running - docker system prune -f --volumes
- docker ps | grep frontend || { echo "Container failed to start"; exit 1; } - >
# Display container logs for img in $(docker images frontend --format "{{.Tag}}" | grep -v $(cat /home/mnisyif/docker-containers/mnisyif/frontend/version.txt)); do
- docker logs frontend docker rmi frontend:$img || true;
# Test Nginx configuration done
- docker exec frontend nginx -t - echo "Cleanup completed"
# Check Nginx process
- docker exec frontend ps aux | grep nginx
# Check contents of /usr/share/nginx/html in the container
- docker exec frontend ls -la /usr/share/nginx/html
# Perform a simple HTTP request to check if the server is responding
- curl -I http://localhost:5173 || { echo "HTTP request failed"; exit 1; }
- echo "Deployment confirmed successfully"
cleanup: trigger:
image: appleboy/drone-ssh branch:
settings: - master
host: event:
from_secret: ssh_host - push
username:
from_secret: ssh_username
key:
from_secret: ssh_key
port: 2332
script:
- echo "Performing cleanup..."
- docker system prune -f --volumes
- >
for img in $(docker images frontend --format "{{.Tag}}" | grep -v $(cat /home/mnisyif/docker-containers/mnisyif/frontend/version.txt)); do
docker rmi frontend:$img || true
done
- echo "Cleanup completed"
# trigger:
# branch:
# - master
# event:
# - push
when:
- branch: master
event: push

View file

@ -2,7 +2,7 @@
<html lang="en"> <html lang="en">
<head> <head>
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/logos/favicon.svg" /> <link rel="icon" type="image/svg+xml" href="/logos/logo.png" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Murtadha Nisyif | Portfolio</title> <title>Murtadha Nisyif | Portfolio</title>
</head> </head>

View file

@ -2,147 +2,52 @@
{ {
"id": 1, "id": 1,
"title": "PaperKeypad", "title": "PaperKeypad",
"category": ["Misc"], "category": "Misc",
"images": ["/assets/projects/keypad0.jpg"], "images": ["/assets/projects/keypad0.jpg"],
"description": "Ever need to use a keyboard, but you got only your phone and a printer, PaperKeypad is a keypad that is made of paper.", "description": "Ever need to use a keyboard, but you got only your phone and a printer, PaperKeypad is a keypad that is made of paper.",
"technologies": ["Java", "JavaFX", "Android Studio"], "technologies": ["Java", "JavaFX", "Android Studio"],
"features": ["Mobile sensor manipulation", "Responsive design"], "features": ["Mobile sensor manipulation", "Responsive design"],
"githubLink": "https://github.com/betato/PaperKeypad", "githubLink": "https://github.com/betato/PaperKeypad"
"date": 2019
}, },
{ {
"id": 2, "id": 2,
"title": "StonkBot", "title": "StonkBot",
"category": ["Misc"], "category": "Misc",
"images": ["/assets/projects/stonkbot0.jpg"], "images": ["/assets/projects/stonkbot0.jpg"],
"description": "The fear of losing money is common among first-time and seasoned investors alike. This inspired the creation of Stonk Bot, a fantasy trading platform that can be implemented in Discord.", "description": "The fear of losing money is common among first-time and seasoned investors alike. This inspired the creation of Stonk Bot, a fantasy trading platform that can be implemented in Discord.",
"technologies": ["Python", "VS Code", "Matplotlib", "Financial Modeling Prep API", "Discord API"], "technologies": ["Python", "VS Code", "Matplotlib", "Financial Modeling Prep API", "Discord API"],
"features": ["Buy shares", "Sell shares", "View stock information", "View personal portfolio", "View leaderboard"], "features": ["Buy shares", "Sell shares", "View stock information", "View personal portfolio", "View leaderboard"],
"githubLink": "https://github.com/aidanbruneel/stonkbot", "githubLink": "https://github.com/aidanbruneel/stonkbot",
"liveLink": "https://discord.com/invite/tQNkk7v7R8", "liveLink": "https://discord.com/invite/tQNkk7v7R8"
"date": 2022
}, },
{ {
"id": 3, "id": 3,
"title": "Car Model Classification", "title": "Car Model Classification",
"category": ["Machine Learning"], "category": "Machine Learning",
"images": ["/assets/projects/carmodelclass0.png"], "images": ["/assets/projects/carmodelclass0.png"],
"description": "Developing a computer vision application to identify a vehicle model from a given image is an interesting and challenging problem to solve. Challenge of this problem is that different vehicle models can appear very similar and the same vehicle can look different and hard to identify depending on lighting conditions, angle and many other factors. In this project, I decided to train a Convolutional Neural Network(CNN) to generate a model that can identify a given vehicle model.", "description": "Developing a computer vision application to identify a vehicle model from a given image is an interesting and challenging problem to solve. Challenge of this problem is that different vehicle models can appear very similar and the same vehicle can look different and hard to identify depending on lighting conditions, angle and many other factors. In this project, I decided to train a Convolutional Neural Network(CNN) to generate a model that can identify a given vehicle model.",
"technologies": ["Python", "Tensorflow", "CNN", "Deep learning", "ResNet", "EfficientNet", "Stanford Cars Dataset"], "technologies": ["Python", "Tensorflow", "CNN", "Deep learning", "ResNet", "EfficientNet", "Stanford Cars Dataset"],
"features": ["Buy shares", "Sell shares", "View stock information", "View personal portfolio", "View leaderboard"], "features": ["Buy shares", "Sell shares", "View stock information", "View personal portfolio", "View leaderboard"],
"githubLink": "https://github.com/mnisyif/carClassificationModel", "githubLink": "https://github.com/mnisyif/carClassificationModel"
"date": 2022
}, },
{ {
"id": 4, "id": 4,
"title": "Memory Allocation Simulations", "title": "Memory Allocation Simulations",
"category": ["Misc"], "category": "Misc",
"images": ["/assets/projects/memallc0.png"], "images": ["/assets/projects/memallc0.png"],
"description": "This implementation uses doubly linked list to simulate memory allocation given 4 different memory management algorithms", "description": "This implementation uses doubly linked list to simulate memory allocation given 4 different memory management algorithms",
"technologies": ["C", "CMake", "Data structures"], "technologies": ["C", "CMake", "Data structures"],
"features": ["First fit", "Best fit", "Next fit", "Worst fit"], "features": ["First fit", "Best fit", "Next fit", "Worst fit"],
"githubLink": "https://github.com/mnisyif/MemoryAllocationAlgorithm/tree/main", "githubLink": "https://github.com/mnisyif/MemoryAllocationAlgorithm/tree/main"
"date": 2022
}, },
{ {
"id": 5, "id": 5,
"title": "Transformer-based Semantic Transcoding", "title": "Portfolio Website",
"category": ["Machine Learning"], "category": "Web Development",
"images": ["/assets/projects/semantic01.png"], "images": ["/assets/projects/memallc0.png"],
"description": "Developed PyTorch models for E2E semantic transcoding, deployed on Xilinx SoC boards using Vitis AI™", "description": "This implementation uses doubly linked list to simulate memory allocation given 4 different memory management algorithms",
"technologies": ["PyTorch", "Vitis AI", "Xilinx SoC", "Machine Learning", "C++"], "technologies": ["C", "CMake", "Data structures"],
"features": ["E2E semantic transcoding", "Hardware acceleration", "SoC deployment", "C++ deployment"], "features": ["First fit", "Best fit", "Next fit", "Worst fit"],
"githubLink": "https://github.com/mnisyif/masters-research", "githubLink": "https://github.com/mnisyif/MemoryAllocationAlgorithm/tree/main"
"date": 2024 }
},
{
"id": 6,
"title": "Clean Architecture C# Backend",
"category": ["Web Development"],
"images": ["/assets/projects/clean_architecture_backend.png"],
"description": "Engineered a scalable portfolio website backend using C#, adhering to Clean Architecture principles and implementing CI/CD pipeline for efficient deployment",
"technologies": ["C#", "Clean Architecture", "CI/CD", "REST Api"],
"features": ["Scalable backend", "Clean Architecture implementation", "Automated deployment", "RESTful"],
"githubLink": "https://github.com/mnisyif/portfolio-backend",
"date": 2024
},
{
"id": 7,
"title": "DevOps Homelab Maestro",
"category": ["DevOps"],
"images": ["/assets/projects/homelab_maestro.png"],
"description": "Orchestrating a robust homelab environment with Docker containers, Kubernetes clusters, Ceph distributed storage, and CI/CD pipelines for seamless application deployment",
"technologies": ["Docker", "Kubernetes", "Ceph", "CI/CD"],
"features": ["Containerized applications", "Orchestration", "Distributed storage", "Automated deployment"],
"date": 2023
},
{
"id": 8,
"title": "RL Dynamic Noise Cancelling",
"category": ["Machine Learning"],
"images": ["/assets/projects/noise_cancelling.png"],
"description": "Implemented real-time Automatic Noise Filtering using Reinforcement Learning and Dynamic Sparse Training in PyTorch",
"technologies": ["PyTorch", "Reinforcement Learning", "Dynamic Sparse Training", "Jupyter Notebooks"],
"features": ["Real-time filtering", "Automatic noise cancellation", "Sparse training", "Interactive development"],
"githubLink": "https://github.com/mnisyif/rl-noise-cancelling",
"date": 2023
},
{
"id": 9,
"title": "Real-Time Text-to-Braille",
"category": ["Embedded Systems"],
"images": ["/assets/projects/braille01.jpg","/assets/projects/braille02.jpg"],
"description": "Built a Raspberry Pi device for real-time image-to-Braille conversion, enhancing accessibility for the deaf-blind community",
"technologies": ["Raspberry Pi", "Image Processing", "OCR", "Python"],
"features": ["Real-time conversion", "Low-cost OCR algorithm", "Lookup table for Braille conversion", "Accessibility enhancement"],
"date": 2023
},
{
"id": 10,
"title": "ZAMAZ UTI Diagnosis",
"category": ["Embedded Systems"],
"images": ["/assets/projects/zamaz01.jpg","/assets/projects/zamaz02.jpg"],
"description": "Developed a Raspberry Pi-based system for automated urine test analysis, achieving 16x faster results than standard methods",
"technologies": ["Raspberry Pi", "Python", "Image Processing", "Healthcare Technology"],
"features": ["Automated analysis", "Pixel-based concentration calculation", "E. Coli and Staph bacteria detection", "Rapid results"],
"date": 2022
},
{
"id": 11,
"title": "HAM10K Image Classification with Deep Networks",
"category": ["Machine Learning"],
"images": ["/assets/projects/ham10k_classification.png"],
"description": "Developed and compared three deep learning models (MLP+PCA, DCNN, RegNetY-320) for skin cancer classification using the HAM10000 dataset, achieving 96.89% accuracy with RegNetY-320",
"technologies": ["PyTorch", "Deep Learning", "CNN", "RegNet", "PCA", "Python"],
"features": ["Multi-model comparison", "Data balancing and augmentation", "High accuracy classification", "Medical image analysis"],
"githubLink": "https://github.com/nithinprasad94/ENGG6600_DL_Final_Project",
"liveLink":"https://youtu.be/zHbRmIn7gPo",
"date": 2023
},
{
"id": 12,
"title": "Heart Disease Prediction Web App",
"category": ["Machine Learning", "Web Development"],
"images": ["/assets/projects/heartdis01.png"],
"description": "Developed a Flask-based web application that predicts the likelihood of heart disease using machine learning models. The app processes user input, applies feature encoding and scaling, and provides instant predictions.",
"technologies": [
"Python",
"Flask",
"NumPy",
"Scikit-learn",
"Pickle",
"HTML",
"Machine Learning"
],
"features": [
"User-friendly web interface for input",
"Real-time prediction using pre-trained model",
"Feature encoding and scaling",
"Integration of multiple ML preprocessing steps",
"Handling of both categorical and numerical inputs"
],
"githubLink": "https://github.com/zeyadghulam/engg6600-assignment3",
"date":2023
}
] ]

Binary file not shown.

Before

Width:  |  Height:  |  Size: 248 B

View file

@ -1,30 +0,0 @@
<?xml version="1.0" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 20010904//EN"
"http://www.w3.org/TR/2001/REC-SVG-20010904/DTD/svg10.dtd">
<svg version="1.0" xmlns="http://www.w3.org/2000/svg"
width="257.000000pt" height="257.000000pt" viewBox="0 0 257.000000 257.000000"
preserveAspectRatio="xMidYMid meet">
<g transform="translate(0.000000,257.000000) scale(0.100000,-0.100000)"
fill="#000000" stroke="none">
<path d="M1 2043 l1 -528 23 90 c60 238 179 440 362 616 173 166 351 266 578
324 l90 23 -527 1 -528 1 1 -527z"/>
<path d="M1595 2545 c238 -60 440 -179 616 -362 103 -108 163 -191 222 -308
67 -133 105 -254 130 -405 2 -14 5 228 6 538 l1 562 -532 -1 -533 -1 90 -23z"/>
<path d="M1056 2544 c-243 -44 -481 -165 -665 -338 -131 -124 -277 -364 -329
-541 -70 -234 -70 -516 0 -750 52 -177 198 -417 329 -541 138 -130 348 -253
514 -302 177 -53 415 -67 591 -37 250 44 486 163 673 339 131 124 277 364 329
541 70 234 70 516 0 750 -49 166 -172 376 -302 514 -124 131 -364 277 -541
329 -175 52 -427 67 -599 36z m812 -1128 c171 -101 313 -181 316 -178 4 3 6
86 6 184 l0 178 115 0 115 0 0 -305 0 -305 -117 0 -118 0 -310 180 -310 179
-3 -177 c-2 -125 -6 -178 -15 -184 -6 -4 -65 -8 -129 -8 l-118 0 0 155 0 156
-151 -156 -151 -155 -142 0 -141 0 -152 152 -153 153 0 -153 0 -152 -120 0
-120 0 0 305 0 305 118 0 118 0 84 -87 c47 -49 147 -153 224 -231 l140 -144
231 236 230 237 121 -1 120 0 312 -184z"/>
<path d="M2560 1105 c0 -85 -75 -309 -149 -442 -82 -151 -244 -330 -391 -435
-153 -108 -354 -187 -560 -221 -14 -2 230 -5 543 -6 l567 -1 0 565 c0 311 -2
565 -5 565 -3 0 -5 -11 -5 -25z"/>
<path d="M1 533 l-1 -533 563 1 c309 1 551 4 537 6 -151 25 -272 63 -405 130
-117 59 -200 119 -308 222 -183 176 -302 378 -362 616 l-23 90 -1 -532z"/>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 249 KiB

After

Width:  |  Height:  |  Size: 4.9 MiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 57 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 60 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 45 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

View file

@ -13,8 +13,6 @@ import InfoSection from "./shared/components/info/InfoSection";
import styles from "./App.module.css"; import styles from "./App.module.css";
import { fetchEducationData, fetchExperienceData, fetchPersonalData, fetchProjectsData } from "./utils/dataFetcher";
function App() { function App() {
const [educationData, setEducationData] = useState([]); const [educationData, setEducationData] = useState([]);
const [experienceData, setExperienceData] = useState([]); const [experienceData, setExperienceData] = useState([]);
@ -22,21 +20,35 @@ function App() {
const [personalData, setPersonalData] = useState([]); const [personalData, setPersonalData] = useState([]);
useEffect(() => { useEffect(() => {
const fetchData = async () => { const fetchEducationData = async () => {
const education = await fetchEducationData(); const response = await fetch("/assets/data/educationData.json");
setEducationData(education); const data = await response.json();
setEducationData(data);
const experience = await fetchExperienceData();
setExperienceData(experience);
const projects = await fetchProjectsData();
setProjectsData(projects);
const personal = await fetchPersonalData();
setPersonalData(personal);
}; };
fetchData(); const fetchExperienceData = async () => {
const response = await fetch("/assets/data/experienceData.json");
const data = await response.json();
setExperienceData(data);
};
const fetchProjectsData = async () => {
const response = await fetch("/assets/data/projectsData.json");
const data = await response.json();
setProjectsData(data);
};
const fetchPersonalData = async () => {
const response = await fetch("/assets/data/personalData.json");
const data = await response.json();
setPersonalData(data);
// console.log(data)
};
fetchEducationData();
fetchExperienceData();
fetchProjectsData();
fetchPersonalData();
}, []); }, []);
return ( return (

View file

@ -17,16 +17,13 @@ function Projects({ title, data }) {
}, []); }, []);
const categories = useMemo(() => { const categories = useMemo(() => {
const cats = new Set(data.flatMap((project) => project.category)); const cats = new Set(data.map((project) => project.category));
return ["All", ...Array.from(cats)]; return ["All", ...Array.from(cats)];
}, [data]); }, [data]);
const sortedAndFilteredProjects = useMemo(() => { const filteredProjects = useMemo(() => {
let filteredProjects = activeFilter === "All" if (activeFilter === "All") return data;
? data return data.filter((project) => project.category === activeFilter);
: data.filter((project) => project.category.includes(activeFilter));
return filteredProjects.sort((a, b) => b.date - a.date);
}, [data, activeFilter]); }, [data, activeFilter]);
const handleFilterClick = (category) => { const handleFilterClick = (category) => {
@ -44,23 +41,14 @@ function Projects({ title, data }) {
<h2 className={styles.sectionTitle}>{title}</h2> <h2 className={styles.sectionTitle}>{title}</h2>
<div className={styles.filterContainer}> <div className={styles.filterContainer}>
{categories.map((category) => ( {categories.map((category) => (
<button <button key={category} className={`${styles.filterButton} ${activeFilter === category ? styles.active : ""}`} onClick={() => handleFilterClick(category)}>
key={category}
className={`${styles.filterButton} ${activeFilter === category ? styles.active : ""}`}
onClick={() => handleFilterClick(category)}
>
{category} {category}
</button> </button>
))} ))}
</div> </div>
<div className={styles.projectGrid}> <div className={styles.projectGrid}>
{sortedAndFilteredProjects.map((project) => ( {filteredProjects.map((project) => (
<ProjectCard <ProjectCard key={project.id} project={project} onClick={openModal} className={animatingOut ? styles.fadeOut : styles.fadeIn} />
key={project.id}
project={project}
onClick={openModal}
className={animatingOut ? styles.fadeOut : styles.fadeIn}
/>
))} ))}
</div> </div>
{selectedProject && <ProjectModal project={selectedProject} onClose={closeModal} />} {selectedProject && <ProjectModal project={selectedProject} onClose={closeModal} />}
@ -68,4 +56,4 @@ function Projects({ title, data }) {
); );
} }
export default Projects; export default Projects;

View file

@ -1,44 +1,7 @@
import React, { useRef, useEffect, useState } from "react"; import React from "react";
import styles from "./ProjectCard.module.css"; import styles from "./ProjectCard.module.css";
function ProjectCard({ project, onClick, className }) { function ProjectCard({ project, onClick, className }) {
const [truncatedDescription, setTruncatedDescription] = useState(project.description);
const descriptionRef = useRef(null);
const formatList = (items) => {
return items.map((item, index, arr) => (
<React.Fragment key={index}>
{item}
{index < arr.length - 1 && <span className={styles.separator}>, </span>}
</React.Fragment>
));
};
useEffect(() => {
const truncateDescription = () => {
const element = descriptionRef.current;
if (!element) return;
const maxHeight = parseInt(window.getComputedStyle(element).lineHeight) * 4; // 4 lines
let text = project.description;
element.textContent = text;
while (element.scrollHeight > maxHeight && text.length > 0) {
text = text.slice(0, -1);
element.textContent = text + '...';
}
setTruncatedDescription(element.textContent);
};
truncateDescription();
window.addEventListener('resize', truncateDescription);
return () => {
window.removeEventListener('resize', truncateDescription);
};
}, [project.description]);
return ( return (
<div className={`${styles.card} ${className}`} onClick={() => onClick(project)}> <div className={`${styles.card} ${className}`} onClick={() => onClick(project)}>
<div className={styles.imageSlider}> <div className={styles.imageSlider}>
@ -46,17 +9,11 @@ function ProjectCard({ project, onClick, className }) {
</div> </div>
<div className={styles.content}> <div className={styles.content}>
<h3 className={styles.title}>{project.title}</h3> <h3 className={styles.title}>{project.title}</h3>
<div className={styles.categories}> <p className={styles.category}>{project.category}</p>
{formatList(project.category)} <p className={styles.description}>{project.description}</p>
</div>
<p ref={descriptionRef} className={styles.description}>{truncatedDescription}</p>
<div className={styles.technologies}>
{formatList(project.technologies)}
</div>
{/* <p className={styles.date}>{project.date}</p> */}
</div> </div>
</div> </div>
); );
} }
export default ProjectCard; export default ProjectCard;

View file

@ -5,10 +5,9 @@
overflow: hidden; overflow: hidden;
transition: transform 0.3s ease; transition: transform 0.3s ease;
cursor: pointer; cursor: pointer;
height: 450px; /* Fixed height for the card */
display: flex; display: flex;
flex-direction: column; flex-direction: column;
height: auto;
min-height: 450px;
} }
.card:hover { .card:hover {
@ -33,6 +32,7 @@
display: flex; display: flex;
flex-direction: column; flex-direction: column;
padding: 1rem; padding: 1rem;
overflow: hidden;
} }
.title { .title {
@ -42,7 +42,7 @@
color: #333; color: #333;
} }
.categories { .category {
font-size: 0.8rem; font-size: 0.8rem;
color: #666; color: #666;
margin-bottom: 0.5rem; margin-bottom: 0.5rem;
@ -51,24 +51,10 @@
.description { .description {
font-size: 0.9rem; font-size: 0.9rem;
color: #666; color: #666;
margin-bottom: 0.5rem; flex-grow: 1;
overflow: hidden; overflow: hidden;
line-height: 1.4; display: -webkit-box;
max-height: calc(1.4em * 4); /* 4 lines of text */ -webkit-line-clamp: 4; /* Adjust this number to show more or fewer lines */
-webkit-box-orient: vertical;
text-overflow: ellipsis;
} }
.technologies {
font-size: 0.8rem;
color: #0066cc;
margin-bottom: 0.5rem;
}
.date {
font-size: 0.8rem;
color: #999;
margin-top: auto;
}
.separator {
margin: 0 2px;
}

View file

@ -8,7 +8,7 @@ const ResumeDownloader = ({ resumeLink }) => {
const link = document.createElement("a"); const link = document.createElement("a");
link.href = resumeLink; link.href = resumeLink;
console.log(link.href); console.log(link.href);
link.download = "Murtadha_Nisyif_Resume.pdf"; link.download = "Murtadha.pdf";
link.target = "_blank"; link.target = "_blank";
link.rel = "noopener noreferrer"; link.rel = "noopener noreferrer";

View file

@ -1,19 +0,0 @@
export const fetchEducationData = async () => {
const response = await fetch("/assets/data/educationData.json");
return await response.json();
};
export const fetchExperienceData = async () => {
const response = await fetch("/assets/data/experienceData.json");
return await response.json();
};
export const fetchProjectsData = async () => {
const response = await fetch("/assets/data/projectsData.json");
return await response.json();
};
export const fetchPersonalData = async () => {
const response = await fetch("/assets/data/personalData.json");
return await response.json();
};

View file

@ -1 +1 @@
0.12.4 0.12.1