small banner photos for navbar

This commit is contained in:
Michael Freno
2025-12-30 13:56:01 -05:00
parent f031666ffc
commit 17ea918081
3 changed files with 148 additions and 2 deletions

View File

@@ -25,6 +25,23 @@ function formatDomainName(url: string): string {
return withoutWww.charAt(0).toUpperCase() + withoutWww.slice(1); return withoutWww.charAt(0).toUpperCase() + withoutWww.slice(1);
} }
/**
* Converts a banner photo URL to its thumbnail version
* Replaces the filename with -small variant (e.g., image.jpg -> image-small.jpg)
*/
function getThumbnailUrl(bannerPhoto: string | null): string {
if (!bannerPhoto) return "/blueprint.jpg";
// Check if URL contains a file extension
const match = bannerPhoto.match(/^(.+)(\.[^.]+)$/);
if (match) {
return `${match[1]}-small${match[2]}`;
}
// Fallback to original if no extension found
return bannerPhoto;
}
interface GitCommit { interface GitCommit {
sha: string; sha: string;
message: string; message: string;
@@ -393,9 +410,16 @@ export function LeftBar() {
<Typewriter class="flex flex-col" keepAlive={false}> <Typewriter class="flex flex-col" keepAlive={false}>
<div class="relative overflow-hidden"> <div class="relative overflow-hidden">
<img <img
src={post.banner_photo || "/blueprint.jpg"} src={getThumbnailUrl(post.banner_photo)}
alt="post-cover" alt="post-cover"
class="float-right mb-1 ml-2 h-12 w-16 rounded object-cover" class="float-right mb-1 ml-2 h-12 w-16 rounded object-cover"
onError={(e) => {
// Fallback to full banner if thumbnail doesn't exist
const img = e.currentTarget;
if (img.src !== (post.banner_photo || "/blueprint.jpg")) {
img.src = post.banner_photo || "/blueprint.jpg";
}
}}
/> />
<span class="inline wrap-break-word hyphens-auto"> <span class="inline wrap-break-word hyphens-auto">
{insertSoftHyphens(post.title.replace(/_/g, " "))} {insertSoftHyphens(post.title.replace(/_/g, " "))}

View File

@@ -58,3 +58,83 @@ export function createIsMobile(
): Accessor<boolean> { ): Accessor<boolean> {
return () => isMobile(windowWidth()); return () => isMobile(windowWidth());
} }
/**
* Resizes an image file to a maximum width/height while maintaining aspect ratio
* @param file Original image file
* @param maxWidth Maximum width in pixels
* @param maxHeight Maximum height in pixels
* @param quality JPEG quality (0-1), default 0.85
* @returns Resized image as Blob
*/
export async function resizeImage(
file: File | Blob,
maxWidth: number,
maxHeight: number,
quality: number = 0.85
): Promise<Blob> {
return new Promise((resolve, reject) => {
const img = new Image();
const canvas = document.createElement("canvas");
const ctx = canvas.getContext("2d");
if (!ctx) {
reject(new Error("Failed to get canvas context"));
return;
}
img.onload = () => {
let { width, height } = img;
// Calculate new dimensions maintaining aspect ratio
if (width > maxWidth || height > maxHeight) {
const aspectRatio = width / height;
if (width > height) {
width = Math.min(width, maxWidth);
height = width / aspectRatio;
} else {
height = Math.min(height, maxHeight);
width = height * aspectRatio;
}
}
canvas.width = width;
canvas.height = height;
// Draw image on canvas with new dimensions
ctx.drawImage(img, 0, 0, width, height);
// Convert canvas to blob
canvas.toBlob(
(blob) => {
if (blob) {
resolve(blob);
} else {
reject(new Error("Failed to create blob from canvas"));
}
},
"image/jpeg",
quality
);
};
img.onerror = () => {
reject(new Error("Failed to load image"));
};
// Load image from file
const reader = new FileReader();
reader.onload = (e) => {
if (e.target?.result) {
img.src = e.target.result as string;
} else {
reject(new Error("Failed to read file"));
}
};
reader.onerror = () => {
reject(new Error("FileReader error"));
};
reader.readAsDataURL(file);
});
}

View File

@@ -4,6 +4,7 @@
*/ */
import { api } from "~/lib/api"; import { api } from "~/lib/api";
import { resizeImage } from "~/lib/resize-utils";
export default async function AddImageToS3( export default async function AddImageToS3(
file: Blob | File, file: Blob | File,
@@ -26,7 +27,7 @@ export default async function AddImageToS3(
const ext = /^.+\.([^.]+)$/.exec(filename); const ext = /^.+\.([^.]+)$/.exec(filename);
const contentType = ext ? `image/${ext[1]}` : "application/octet-stream"; const contentType = ext ? `image/${ext[1]}` : "application/octet-stream";
// Upload file to S3 using pre-signed URL // Upload original file to S3 using pre-signed URL
const uploadResponse = await fetch(uploadURL, { const uploadResponse = await fetch(uploadURL, {
method: "PUT", method: "PUT",
headers: { headers: {
@@ -39,6 +40,47 @@ export default async function AddImageToS3(
throw new Error("Failed to upload file to S3"); throw new Error("Failed to upload file to S3");
} }
// For blog cover images, also create and upload a thumbnail
if (type === "blog") {
try {
// Create thumbnail (max 200x200px for sidebar display)
const thumbnail = await resizeImage(file, 200, 200, 0.8);
// Generate thumbnail filename: insert "-small" before extension
const thumbnailFilename = filename.replace(
/(\.[^.]+)$/,
"-small$1"
);
// Get pre-signed URL for thumbnail
const { uploadURL: thumbnailUploadURL } =
await api.misc.getPreSignedURL.mutate({
type,
title,
filename: thumbnailFilename
});
// Upload thumbnail to S3
const thumbnailUploadResponse = await fetch(thumbnailUploadURL, {
method: "PUT",
headers: {
"Content-Type": "image/jpeg" // Thumbnails are always JPEG
},
body: thumbnail
});
if (!thumbnailUploadResponse.ok) {
console.error("Failed to upload thumbnail to S3");
// Don't fail the entire upload if thumbnail fails
} else {
console.log("Thumbnail uploaded successfully");
}
} catch (thumbnailError) {
console.error("Thumbnail creation/upload failed:", thumbnailError);
// Don't fail the entire upload if thumbnail fails
}
}
return key; return key;
} catch (e) { } catch (e) {
console.error("S3 upload error:", e); console.error("S3 upload error:", e);