small banner photos for navbar
This commit is contained in:
@@ -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, " "))}
|
||||||
|
|||||||
@@ -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);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
Reference in New Issue
Block a user