Features Implemented:
Responsive 4-Column Layout:
- Uses CSS Grid for desktop view (4 columns)
- Switches to 2 columns on medium screens (tablets)
- Stacks to 1 column on mobile devices
Profile Column:
- Circular profile image with border
- Centered with caption
- Soft shadow and hover effect
Basic Details Column:
- Student name as heading
- Field of study as subheading
- Short bio paragraph
Portfolio Column:
- List of 5 projects with clickable links
- Hover effects for better interactivity
Contact Column:
- Contact form with name, email, and message fields
- JavaScript alert on form submission
- Download CV button with distinct styling
Design Elements:
- Clean, minimal aesthetic
- Soft shadows and rounded corners
- Hover animations for interactivity
- Professional color scheme
- Legible typography
Technical Implementation:
- Pure HTML, CSS, and JavaScript (no external libraries)
- CSS Grid for layout
- Media queries for responsiveness
- Simple form validation and feedback
The placeholder image can be replaced with an actual student photo by changing the src attribute in the profile image tag. Similarly, the download CV link can be updated to point to an actual PDF file.
:root {
--primary-color: #4a6fa5;
--secondary-color: #166088;
--background-color: #f8f9fa;
--text-color: #333;
--border-color: #ddd;
--hover-color: #e9ecef;
--dark-bg: #2d3748;
--dark-text: #f7fafc;
--dark-border: #4a5568;
}
[data-theme="dark"] {
--primary-color: #63b3ed;
--secondary-color: #4299e1;
--background-color: #2d3748;
--text-color: #f7fafc;
--border-color: #4a5568;
--hover-color: #4a5568;
}
* {
box-sizing: border-box;
margin: 0;
padding: 0;
}
body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
line-height: 1.6;
background-color: var(--background-color);
color: var(--text-color);
padding: 20px;
transition: background-color 0.3s, color 0.3s;
}
.container {
max-width: 1000px;
margin: 0 auto;
padding: 20px;
}
h1 {
text-align: center;
margin-bottom: 30px;
color: var(--primary-color);
}
.toolbar {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
flex-wrap: wrap;
gap: 10px;
}
.settings {
display: flex;
gap: 15px;
align-items: center;
flex-wrap: wrap;
}
.drop-area {
border: 2px dashed var(--border-color);
border-radius: 8px;
padding: 40px 20px;
text-align: center;
cursor: pointer;
margin-bottom: 20px;
transition: all 0.3s;
background-color: rgba(255, 255, 255, 0.05);
}
.drop-area:hover {
border-color: var(--primary-color);
background-color: var(--hover-color);
}
.drop-area.highlight {
border-color: var(--primary-color);
background-color: rgba(74, 111, 165, 0.1);
}
.drop-area p {
margin-bottom: 15px;
font-size: 18px;
}
.btn {
background-color: var(--primary-color);
color: white;
border: none;
padding: 10px 20px;
border-radius: 5px;
cursor: pointer;
font-size: 16px;
transition: background-color 0.3s;
}
.btn:hover {
background-color: var(--secondary-color);
}
.btn:disabled {
background-color: #cccccc;
cursor: not-allowed;
}
.file-input {
display: none;
}
.preview-container {
margin-top: 30px;
}
.preview-title {
margin-bottom: 15px;
display: flex;
justify-content: space-between;
align-items: center;
}
.preview-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
gap: 15px;
margin-bottom: 20px;
}
.preview-item {
position: relative;
border: 1px solid var(--border-color);
border-radius: 5px;
overflow: hidden;
transition: transform 0.2s;
}
.preview-item:hover {
transform: scale(1.02);
}
.preview-item img {
width: 100%;
height: 150px;
object-fit: cover;
display: block;
}
.preview-item .remove-btn {
position: absolute;
top: 5px;
right: 5px;
background-color: rgba(255, 0, 0, 0.7);
color: white;
border: none;
width: 25px;
height: 25px;
border-radius: 50%;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
font-size: 12px;
}
.preview-item .remove-btn:hover {
background-color: rgba(255, 0, 0, 0.9);
}
.loading {
display: none;
text-align: center;
margin: 20px 0;
}
.loading-spinner {
border: 4px solid rgba(0, 0, 0, 0.1);
border-radius: 50%;
border-top: 4px solid var(--primary-color);
width: 40px;
height: 40px;
animation: spin 1s linear infinite;
margin: 0 auto 10px;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
.theme-toggle {
background: none;
border: none;
color: var(--text-color);
cursor: pointer;
font-size: 24px;
padding: 5px;
}
select {
padding: 8px 12px;
border-radius: 5px;
border: 1px solid var(--border-color);
background-color: var(--background-color);
color: var(--text-color);
}
.filename-input {
padding: 8px 12px;
border-radius: 5px;
border: 1px solid var(--border-color);
background-color: var(--background-color);
color: var(--text-color);
min-width: 200px;
}
@media (max-width: 768px) {
.toolbar {
flex-direction: column;
align-items: flex-start;
}
.settings {
width: 100%;
margin-top: 10px;
}
.preview-grid {
grid-template-columns: repeat(auto-fill, minmax(120px, 1fr));
}
}
// Initialize jsPDF
const { jsPDF } = window.jspdf;
// DOM elements
const dropArea = document.getElementById('dropArea');
const fileInput = document.getElementById('fileInput');
const uploadBtn = document.getElementById('uploadBtn');
const convertBtn = document.getElementById('convertBtn');
const previewContainer = document.getElementById('previewContainer');
const previewGrid = document.getElementById('previewGrid');
const imageCount = document.getElementById('imageCount');
const loading = document.getElementById('loading');
const orientationSelect = document.getElementById('orientation');
const filenameInput = document.getElementById('filename');
const themeToggle = document.getElementById('themeToggle');
// Store uploaded files
let files = [];
// Theme toggle
themeToggle.addEventListener('click', () => {
document.body.dataset.theme = document.body.dataset.theme === 'dark' ? 'light' : 'dark';
localStorage.setItem('theme', document.body.dataset.theme);
});
// Check for saved theme preference
if (localStorage.getItem('theme') === 'dark' ||
(window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches && !localStorage.getItem('theme'))) {
document.body.dataset.theme = 'dark';
}
// Upload button click event
uploadBtn.addEventListener('click', () => {
fileInput.click();
});
// File input change event
fileInput.addEventListener('change', (e) => {
if (e.target.files.length > 0) {
handleFiles(e.target.files);
}
});
// Drag and drop events
['dragenter', 'dragover', 'dragleave', 'drop'].forEach(eventName => {
dropArea.addEventListener(eventName, preventDefaults, false);
});
function preventDefaults(e) {
e.preventDefault();
e.stopPropagation();
}
['dragenter', 'dragover'].forEach(eventName => {
dropArea.addEventListener(eventName, highlight, false);
});
['dragleave', 'drop'].forEach(eventName => {
dropArea.addEventListener(eventName, unhighlight, false);
});
function highlight() {
dropArea.classList.add('highlight');
}
function unhighlight() {
dropArea.classList.remove('highlight');
}
dropArea.addEventListener('drop', (e) => {
const dt = e.dataTransfer;
const droppedFiles = dt.files;
if (droppedFiles.length > 0) {
handleFiles(droppedFiles);
}
});
// Handle uploaded files
function handleFiles(newFiles) {
const validFiles = Array.from(newFiles).filter(file => file.type.startsWith('image/'));
if (validFiles.length === 0) {
alert('Please upload valid image files.');
return;
}
files = [...files, ...validFiles];
updatePreview();
}
// Update preview grid
function updatePreview() {
if (files.length === 0) {
previewContainer.style.display = 'none';
convertBtn.disabled = true;
return;
}
previewContainer.style.display = 'block';
convertBtn.disabled = false;
imageCount.textContent = `${files.length} ${files.length === 1 ? 'image' : 'images'}`;
previewGrid.innerHTML = '';
files.forEach((file, index) => {
const reader = new FileReader();
reader.onload = (e) => {
const previewItem = document.createElement('div');
previewItem.className = 'preview-item';
previewItem.dataset.index = index;
const img = document.createElement('img');
img.src = e.target.result;
img.alt = file.name;
const removeBtn = document.createElement('button');
removeBtn.className = 'remove-btn';
removeBtn.innerHTML = '×';
removeBtn.addEventListener('click', (e) => {
e.stopPropagation();
removeImage(index);
});
previewItem.appendChild(img);
previewItem.appendChild(removeBtn);
previewGrid.appendChild(previewItem);
};
reader.readAsDataURL(file);
});
// Initialize Sortable for reordering
new Sortable(previewGrid, {
animation: 150,
onEnd: (evt) => {
// Update files array based on new order
const newFiles = [...files];
const movedItem = newFiles.splice(evt.oldIndex, 1)[0];
newFiles.splice(evt.newIndex, 0, movedItem);
files = newFiles;
}
});
}
// Remove image from preview
function removeImage(index) {
files.splice(index, 1);
updatePreview();
}
// Convert to PDF
convertBtn.addEventListener('click', async () => {
if (files.length === 0) return;
loading.style.display = 'block';
convertBtn.disabled = true;
try {
const orientation = orientationSelect.value;
const pdf = new jsPDF({
orientation: orientation,
unit: 'mm'
});
// Process each image
for (let i = 0; i < files.length; i++) {
const file = files[i];
const img = await loadImage(file);
// Calculate dimensions to fit page
const pageWidth = pdf.internal.pageSize.getWidth();
const pageHeight = pdf.internal.pageSize.getHeight();
let imgWidth = pageWidth - 20; // 10mm margin on each side
let imgHeight = (img.height * imgWidth) / img.width;
// If image is too tall, scale down
if (imgHeight > pageHeight - 20) {
imgHeight = pageHeight - 20;
imgWidth = (img.width * imgHeight) / img.height;
}
// Center the image on the page
const x = (pageWidth - imgWidth) / 2;
const y = (pageHeight - imgHeight) / 2;
// Add image to PDF
pdf.addImage(img, 'JPEG', x, y, imgWidth, imgHeight);
// Add new page if not the last image
if (i < files.length - 1) {
pdf.addPage();
}
}
// Download PDF
const filename = filenameInput.value || 'images-to-pdf';
pdf.save(`${filename}.pdf`);
} catch (error) {
console.error('Error generating PDF:', error);
alert('An error occurred while generating the PDF. Please try again.');
} finally {
loading.style.display = 'none';
convertBtn.disabled = false;
}
});
// Helper function to load image and get dimensions
function loadImage(file) {
return new Promise((resolve, reject) => {
const img = new Image();
const reader = new FileReader();
reader.onload = (e) => {
img.src = e.target.result;
};
img.onload = () => {
resolve(img);
};
img.onerror = () => {
reject(new Error('Failed to load image'));
};
reader.readAsDataURL(file);
});
}
Post a Comment
Thank you
Learning robo team