project demo
introduction
This project is basically a simple tool I made to preview any HTML project directly from a ZIP file without extracting it. I wanted something quick and clean where I could just drop a ZIP and instantly see the website running. It helps a lot when testing small front-end projects or checking someone’s code before opening it properly. Everything opens inside an iframe, and all files load automatically with correct paths. It’s just a neat way to save time and avoid the usual hassle of manual unzipping.
Source Code: Scroll down to download zip file 👇
html code
Inside the body tag, we used a main container that splits the layout into a sidebar and a preview panel. The sidebar holds the upload section with a drag-and-drop zone, file input, status messages, and file info display. The preview panel contains a header with a clear button and an iframe where the selected project is shown. Structural elements like divs organize everything neatly into sections. Interactive elements like buttons, icons, and text containers help manage uploading, clearing, and viewing the ZIP preview.
Project Preview - ZIP
🎨 Project Preview
Preview HTML projects from ZIP files
📁
Drop ZIP file here
or click to browse
File found: —
Type: —
Preview
css code
We used a full reset to remove default browser spacing and make everything consistent. The layout uses a grid system for the container and flexbox for centering, aligning, and structuring elements cleanly. There are styled components like the sidebar, preview panel, upload zone, and buttons, each with shadows, gradients, or hover effects. We added responsive rules so the layout adjusts on smaller screens. Extra styling handles states like drag-over, status messages, and iframe presentation to keep everything polished.
/* Reset default styles */
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
/* Main body - centered with gradient background */
body {
display: flex;
justify-content: center;
align-items: center;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, sans-serif;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
padding: 20px;
min-height: 100vh;
}
/* Main container - 2 column layout (sidebar + preview) */
.container {
width: 80%;
margin: 0 auto;
display: grid;
grid-template-columns: 300px 1fr;
gap: 20px;
height: 100%;
transform-origin: top center;
}
/* Left sidebar - file upload and info */
.sidebar {
background: white;
border-radius: 12px;
padding: 24px;
box-shadow: 0 10px 40px rgba(0, 0, 0, 0.1);
overflow-y: auto;
}
/* Title styling */
h1 {
font-size: 24px;
margin-bottom: 8px;
color: #333;
}
/* Subtitle styling */
.subtitle {
color: #666;
font-size: 14px;
margin-bottom: 24px;
line-height: 1.5;
}
/* Upload zone - drop area for ZIP files */
.upload-zone {
border: 2px dashed #ddd;
border-radius: 8px;
padding: 40px 20px;
text-align: center;
cursor: pointer;
transition: all 0.3s;
margin-bottom: 20px;
background: #f9fafb;
}
/* Hover and drag-over states */
.upload-zone:hover,
.upload-zone.dragover {
border-color: #667eea;
background: #f0f4ff;
}
.upload-zone.dragover {
transform: scale(1.02);
}
/* Upload icon styling */
.upload-icon {
font-size: 48px;
margin-bottom: 12px;
}
/* Upload text styling */
.upload-text {
font-weight: 600;
color: #333;
margin-bottom: 4px;
}
/* Upload hint text */
.upload-hint {
font-size: 13px;
color: #666;
}
/* Hidden file input */
input[type="file"] {
display: none;
}
/* Button styling */
button {
width: 100%;
padding: 12px;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
border: none;
border-radius: 6px;
font-size: 14px;
font-weight: 600;
cursor: pointer;
transition: transform 0.2s;
}
button:hover {
transform: translateY(-2px);
}
button:active {
transform: translateY(0);
}
/* Status message container */
.status {
padding: 12px;
border-radius: 6px;
margin-bottom: 16px;
font-size: 13px;
display: none;
}
.status.info {
background: #e0f2fe;
color: #0369a1;
border-left: 4px solid #0369a1;
}
.status.error {
background: #fee;
color: #c00;
border-left: 4px solid #c00;
}
.status.success {
background: #d1fae5;
color: #065f46;
border-left: 4px solid #065f46;
}
.info-box {
background: #f9fafb;
padding: 12px;
border-radius: 6px;
font-size: 13px;
color: #555;
margin-top: 16px;
}
.info-box strong {
color: #333;
}
.preview-panel {
width: 100%;
background: white;
border-radius: 12px;
padding: 16px;
box-shadow: 0 10px 40px rgba(0, 0, 0, 0.1);
display: flex;
flex-direction: column;
}
.preview-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 12px;
padding-bottom: 12px;
border-bottom: 1px solid #eee;
}
.preview-title {
font-weight: 600;
color: #333;
}
.clear-btn {
width: auto;
padding: 8px 16px;
background: #ef4444;
font-size: 13px;
}
.iframe-container {
flex: 1;
border: 1px solid #ddd;
border-radius: 8px;
overflow: hidden;
background: white;
aspect-ratio: 16 / 9;
}
iframe {
width: 100%;
height: 100%;
border: none;
}
@media (max-width: 1024px) {
.container {
grid-template-columns: 1fr;
height: auto;
}
.preview-panel {
min-height: auto;
}
.iframe-container {
aspect-ratio: 16 / 9;
}
}
javascript code
The JavaScript handles selecting and processing the ZIP file using JSZip, then extracting all files inside it. It finds the main HTML file, rewrites resource paths, and creates blob URLs so everything loads properly in the iframe. It updates the UI with status messages, file details, and clears everything when needed. Event listeners manage drag-and-drop, file input changes, and the clear button. It also handles cleanup by revoking blob URLs to avoid memory leaks.
// DOM element references
const uploadZone = document.getElementById("uploadZone");
const fileInput = document.getElementById("fileInput");
const clearBtn = document.getElementById("clearBtn");
const status = document.getElementById("status");
const foundFile = document.getElementById("foundFile");
const projectType = document.getElementById("projectType");
const preview = document.getElementById("preview");
// Store blob URLs for cleanup
let blobUrls = [];
// Display status message
function showStatus(message, type = "info") {
status.textContent = message;
status.className = `status ${type}`;
status.style.display = "block";
console.log(`[${type.toUpperCase()}]`, message);
}
// Hide status message
function hideStatus() {
status.style.display = "none";
}
// Cleanup blob URLs to free memory
function revokeBlobUrls() {
blobUrls.forEach((url) => URL.revokeObjectURL(url));
blobUrls = [];
}
// Upload zone click to open file browser
uploadZone.addEventListener("click", () => fileInput.click());
// Drag over upload zone
uploadZone.addEventListener("dragover", (e) => {
e.preventDefault();
uploadZone.classList.add("dragover");
});
// Drag leave upload zone
uploadZone.addEventListener("dragleave", () => {
uploadZone.classList.remove("dragover");
});
// Drop file on zone
uploadZone.addEventListener("drop", (e) => {
e.preventDefault();
uploadZone.classList.remove("dragover");
const file = e.dataTransfer.files[0];
if (file) handleZipFile(file);
});
// File input change event
fileInput.addEventListener("change", (e) => {
const file = e.target.files[0];
if (file) handleZipFile(file);
});
// Clear button to reset preview
clearBtn.addEventListener("click", () => {
preview.src = "about:blank";
fileInput.value = "";
foundFile.textContent = "—";
projectType.textContent = "—";
hideStatus();
revokeBlobUrls();
});
// Process ZIP file and display in iframe
async function handleZipFile(file) {
if (!file.name.endsWith(".zip")) {
showStatus("Please upload a .zip file", "error");
return;
}
hideStatus();
revokeBlobUrls();
try {
const zip = new JSZip();
const contents = await zip.loadAsync(file);
// Extract all file paths
const files = Object.keys(contents.files).filter(
(path) => !contents.files[path].dir
);
// Find HTML file
let htmlFile = files.find((f) => f.toLowerCase().endsWith("index.html"));
if (!htmlFile) {
htmlFile = files.find((f) => f.toLowerCase().endsWith(".html"));
}
if (!htmlFile) {
showStatus("No HTML file found in ZIP", "error");
return;
}
foundFile.textContent = htmlFile;
projectType.textContent = "ZIP Archive";
// Create blob URLs with MIME types
const urlMap = {};
const mimeTypes = {
html: "text/html",
css: "text/css",
js: "application/javascript",
json: "application/json",
png: "image/png",
jpg: "image/jpeg",
jpeg: "image/jpeg",
gif: "image/gif",
svg: "image/svg+xml",
webp: "image/webp",
woff: "font/woff",
woff2: "font/woff2",
ttf: "font/ttf",
eot: "application/vnd.ms-fontobject",
};
for (const path of files) {
const file = contents.files[path];
const blob = await file.async("blob");
const ext = path.split(".").pop().toLowerCase();
const mimeType = mimeTypes[ext] || "application/octet-stream";
const typedBlob = new Blob([blob], { type: mimeType });
const blobUrl = URL.createObjectURL(typedBlob);
blobUrls.push(blobUrl);
// Store with multiple path variations for flexible resolution
urlMap[path] = blobUrl;
urlMap[path.toLowerCase()] = blobUrl;
const fileName = path.split("/").pop();
if (!urlMap[fileName]) urlMap[fileName] = blobUrl;
if (!urlMap[fileName.toLowerCase()])
urlMap[fileName.toLowerCase()] = blobUrl;
}
// Read HTML and update resource paths
let html = await contents.files[htmlFile].async("text");
const baseDir = htmlFile.includes("/")
? htmlFile.substring(0, htmlFile.lastIndexOf("/") + 1)
: "";
// Replace src/href attributes
html = html.replace(
/(src|href)=["']([^"']+)["']/gi,
(match, attr, path) => {
if (
path.startsWith("http") ||
path.startsWith("//") ||
path.startsWith("data:")
) {
return match;
}
const cleanPath = path.replace(/^\.\//, "").replace(/^\//, "");
const fullPath = baseDir + cleanPath;
const resolved =
urlMap[fullPath] ||
urlMap[fullPath.toLowerCase()] ||
urlMap[cleanPath] ||
urlMap[cleanPath.toLowerCase()] ||
urlMap[path.split("/").pop()] ||
urlMap[path.split("/").pop().toLowerCase()];
return resolved ? `${attr}="${resolved}"` : match;
}
);
// Replace CSS url() references
html = html.replace(/url\(['"]?([^'"()]+)['"]?\)/gi, (match, path) => {
if (
path.startsWith("http") ||
path.startsWith("//") ||
path.startsWith("data:")
) {
return match;
}
const cleanPath = path.replace(/^\.\//, "").replace(/^\//, "");
const fullPath = baseDir + cleanPath;
const resolved =
urlMap[fullPath] ||
urlMap[fullPath.toLowerCase()] ||
urlMap[cleanPath] ||
urlMap[cleanPath.toLowerCase()];
return resolved ? `url('${resolved}')` : match;
});
// Display in iframe
const htmlBlob = new Blob([html], { type: "text/html" });
const htmlUrl = URL.createObjectURL(htmlBlob);
blobUrls.push(htmlUrl);
preview.src = htmlUrl;
showStatus("Preview loaded successfully!", "success");
} catch (error) {
console.error("Error processing ZIP:", error);
showStatus("Error processing ZIP: " + error.message, "error");
}
}