cleanup | integrate site2

This commit is contained in:
babayaga 2025-12-26 17:02:20 +01:00
parent 2123ae4361
commit 427e57820b
7 changed files with 134 additions and 529 deletions

View File

@ -33,6 +33,7 @@ export interface GalleryImage {
height?: number
width?: number
gps?: { lon: number, lat: number }
hash?: string
}
export interface MetaJSON {

View File

@ -1,11 +1,10 @@
import { createHash } from 'node:crypto'
import { readFileSync } from 'node:fs'
import * as path from 'node:path'
import pMap from 'p-map'
import { GlobOptions } from 'glob'
import { sanitizeUri } from 'micromark-util-sanitize-uri'
import ExifReader from 'exifreader'
import fs from 'node:fs'
import { loadImage } from "imagetools/api"
@ -15,162 +14,16 @@ import { sync as exists } from '@polymech/fs/exists'
import { sync as read } from '@polymech/fs/read'
import { logger } from '@/base/index.js'
import { GalleryImage, MetaJSON } from '@/base/images.js'
import { removeArrayValues, removeArrays, removeBufferValues, removeEmptyObjects } from '@/base/objects.js'
import { ITEM_ASSET_URL, PRODUCT_CONFIG, PRODUCT_ROOT, DEFAULT_IMAGE_URL } from '../app/config.js'
import { ITEM_ASSET_URL, PRODUCT_CONFIG, PRODUCT_ROOT, DEFAULT_IMAGE_URL, FILE_SERVER_DEV } from '../app/config.js'
import { GalleryImage, MetaJSON } from './images.js'
import { env } from './index.js'
const IMAGES_GLOB = '*.+(JPG|jpg|png|PNG|gif)'
const IMAGES_GLOB = '*.+(JPG|jpg|png|jpeg|PNG|gif)'
// Extract date from EXIF data or file stats
export async function extractImageDate(filePath: string): Promise<{ year: number, dateTaken: Date }> {
let dateTaken: Date | null = null;
try {
// Try to get EXIF date
const exifData = await ExifReader.load(filePath);
const dateTimeOriginal = exifData?.['DateTimeOriginal']?.description;
const dateTime = exifData?.['DateTime']?.description;
const createDate = exifData?.['CreateDate']?.description;
// Parse EXIF date (format: "YYYY:MM:DD HH:MM:SS")
const exifDateString = dateTimeOriginal || dateTime || createDate;
if (exifDateString) {
const [datePart, timePart] = exifDateString.split(' ');
const [year, month, day] = datePart.split(':').map(Number);
const [hour, minute, second] = (timePart || '00:00:00').split(':').map(Number);
dateTaken = new Date(year, month - 1, day, hour, minute, second);
}
} catch (e) {
console.warn(`[extractImageDate] Could not read EXIF data for ${filePath}:`, e);
}
// Fallback to file modification date
if (!dateTaken) {
try {
const stats = fs.statSync(filePath);
dateTaken = stats.mtime;
} catch (e) {
console.warn(`[extractImageDate] Could not read file stats for ${filePath}:`, e);
dateTaken = new Date(); // Ultimate fallback
}
}
return {
year: dateTaken.getFullYear(),
dateTaken
};
}
// Predefined grouping functions
export const groupByYear = (image: { year?: number, dateTaken?: Date }) => {
const year = image.year || (image.dateTaken ? image.dateTaken.getFullYear() : new Date().getFullYear());
return {
key: year.toString(),
label: year.toString(),
sortOrder: year
};
};
export const groupByMonth = (image: { year?: number, dateTaken?: Date }) => {
const date = image.dateTaken || new Date();
const year = date.getFullYear();
const month = date.getMonth();
const monthNames = [
'January', 'February', 'March', 'April', 'May', 'June',
'July', 'August', 'September', 'October', 'November', 'December'
];
return {
key: `${year}-${month.toString().padStart(2, '0')}`,
label: `${monthNames[month]} ${year}`,
sortOrder: year * 100 + month
};
};
// Type for grouping function result
export interface GroupInfo {
key: string;
label: string;
sortOrder: number;
}
// Extract comprehensive metadata from image file
export async function extractImageMetadata(filePath: string): Promise<{
year: number;
dateTaken: Date;
title: string;
description: string;
keywords: string[];
width?: number;
height?: number;
exifRaw?: any;
}> {
const fileName = path.basename(filePath, path.extname(filePath));
const baseDir = path.dirname(filePath);
// Get date information
const { year, dateTaken } = await extractImageDate(filePath);
// Load companion metadata files
const metaJsonPath = path.join(baseDir, `${fileName}.json`);
const metaMarkdownPath = path.join(baseDir, `${fileName}.md`);
let metaJson: any = { alt: "", keywords: "", title: "", description: "" };
let metaMarkdown = "";
try {
if (fs.existsSync(metaJsonPath)) {
metaJson = JSON.parse(fs.readFileSync(metaJsonPath, 'utf8'));
}
if (fs.existsSync(metaMarkdownPath)) {
metaMarkdown = fs.readFileSync(metaMarkdownPath, 'utf8');
}
} catch (e) {
console.warn(`Error loading metadata for ${fileName}:`, e);
}
// Load EXIF data
let exifRaw: any = null;
try {
exifRaw = await ExifReader.load(filePath);
} catch (e) {
console.warn(`Error loading EXIF data for ${filePath}:`, e);
exifRaw = {};
}
// Extract EXIF metadata following the same priority as media.ts gallery function
const keywords = exifRaw?.['LastKeywordXMP']?.description || exifRaw?.iptc?.Keywords?.description || '';
const exifDescription = exifRaw?.['ImageDescription']?.description || '';
const width = exifRaw?.['Image Width']?.value;
const height = exifRaw?.['Image Height']?.value;
// Priority order: EXIF title -> JSON title -> generated from filename
const title = exifRaw?.title?.description || metaJson.title ||
fileName.replace(/[-_]/g, ' ').replace(/\b\w/g, l => l.toUpperCase());
// Priority order: JSON description -> Markdown -> EXIF ImageDescription -> EXIF IPTC Caption -> empty
const description = metaJson.description || metaMarkdown || exifDescription ||
exifRaw?.iptc?.['Caption/Abstract']?.description || '';
return {
year,
dateTaken,
title,
description,
keywords: keywords ? keywords.split(',').map((k: string) => k.trim()) : [],
width,
height,
exifRaw
};
}
export const default_sort = (files: string[]): string[] =>
{
export const default_sort = (files: string[]): string[] => {
const getSortableParts = (filename: string) => {
const baseName = path.parse(filename).name;
const match = baseName.match(/^(\d+)_?(.*)$/); // Match leading numbers
@ -273,7 +126,6 @@ export const gallery = async (
}
galleryFiles = default_sort(galleryFiles)
return await pMap(galleryFiles, async (file: string) => {
const parts = path.parse(file)
const filePath = path.join(mediaPath, file)
const meta_path_json = `${mediaPath}/${parts.name}.json`
@ -314,6 +166,7 @@ export const gallery = async (
const assetUrl = (filePath) => {
return sanitizeUri(ITEM_ASSET_URL(
{
FILE_SERVER_DEV,
assetPath,
filePath,
ITEM_REL: product,
@ -321,6 +174,9 @@ export const gallery = async (
}
))
}
const fileBuffer = readFileSync(filePath);
const hash = createHash('md5').update(fileBuffer).digest('hex').substring(0, 8);
const ret: GalleryImage =
{
name: path.parse(file).name,
@ -352,9 +208,9 @@ export const gallery = async (
width,
height,
title,
gps: { lon, lat }
gps: { lon, lat },
hash
}
return ret
})
}

View File

@ -27,7 +27,7 @@ export const removeArrayValues = (obj: any): any => {
try {
delete obj[key];
} catch (e) {
debugger
//debugger
}
} else if (typeof obj[key] === 'object') {

View File

@ -1,277 +0,0 @@
---
import { Img } from "imagetools/components";
import Translate from "@/components/polymech/i18n.astro"
import { translate } from "@/base/i18n"
import { item_keywords } from '@/commons/seo.js'
import pMap from 'p-map'
import { toJsonLd } from '@/base/media.js'
import { IComponentConfig } from "@polymech/commons"
import { I18N_SOURCE_LANGUAGE, IMAGE_SETTINGS } from "config/config.js"
interface Image {
alt: string
src: string
title?: string
description?: string
item?:IComponentConfig
}
export interface Props {
images: Image[];
id?: string;
siteKeywords?: string[];
item?: IComponentConfig;
gallerySettings?: {
SIZES_REGULAR?: string;
SIZES_THUMB?: string;
SIZES_LARGE?: string;
SHOW_TITLE?: boolean;
SHOW_DESCRIPTION?: boolean;
};
lightboxSettings?: {
SIZES_REGULAR?: string;
SIZES_THUMB?: string;
SIZES_LARGE?: string;
SHOW_TITLE?: boolean;
SHOW_DESCRIPTION?: boolean;
};
}
const { images, gallerySettings = {}, lightboxSettings = {}, item } = Astro.props;
const mergedGallerySettings = {
SIZES_REGULAR: gallerySettings.SIZES_REGULAR || IMAGE_SETTINGS.GALLERY.SIZES_REGULAR,
SIZES_THUMB: gallerySettings.SIZES_THUMB || IMAGE_SETTINGS.GALLERY.SIZES_THUMB,
SHOW_TITLE: gallerySettings.SHOW_TITLE ?? IMAGE_SETTINGS.GALLERY.SHOW_TITLE,
SHOW_DESCRIPTION: gallerySettings.SHOW_DESCRIPTION ?? IMAGE_SETTINGS.GALLERY.SHOW_DESCRIPTION,
}
const mergedLightboxSettings = {
SIZES_LARGE: lightboxSettings.SIZES_LARGE || IMAGE_SETTINGS.LIGHTBOX.SIZES_LARGE,
SHOW_TITLE: lightboxSettings.SHOW_TITLE ?? IMAGE_SETTINGS.LIGHTBOX.SHOW_TITLE,
SHOW_DESCRIPTION: lightboxSettings.SHOW_DESCRIPTION ?? IMAGE_SETTINGS.LIGHTBOX.SHOW_DESCRIPTION,
}
const locale = Astro.currentLocale || I18N_SOURCE_LANGUAGE
const canonicalURL = new URL(Astro.url.pathname, Astro.site)
const alt = async (altText) => `${altText} - ${await item_keywords(item, locale)}`
const alt_translated = await pMap(images, async (image) => {
const text = await alt(image.alt)
return await translate(text, I18N_SOURCE_LANGUAGE, locale)
}, { concurrency: 1 })
//const json_ld = await pMap(LANGUAGES, async (lang) => {
const ld = await toJsonLd(images, Astro.currentLocale || I18N_SOURCE_LANGUAGE, canonicalURL.toString())
//}, { concurrency: 1 })
---
<div
x-data={`
{
open: false,
currentIndex: 0,
total: ${images.length},
lightboxLoaded: false,
touchStartX: 0,
touchEndX: 0,
mouseStartX: 0,
mouseEndX: 0,
mouseIsDown: false,
minSwipeDistance: 50,
isSwiping: false,
images: ${JSON.stringify(images)},
handleSwipe(isMouseEvent = false) {
if (!this.isSwiping && !this.mouseIsDown) return;
const startX = isMouseEvent ? this.mouseStartX : this.touchStartX;
const endX = isMouseEvent ? this.mouseEndX : this.touchEndX;
const swipeDistance = endX - startX;
if (Math.abs(swipeDistance) >= this.minSwipeDistance) {
if (swipeDistance > 0 && this.currentIndex > 0) {
// Swiped right, show previous image
this.currentIndex--;
} else if (swipeDistance < 0 && this.currentIndex < this.total - 1) {
// Swiped left, show next image
this.currentIndex++;
}
}
this.isSwiping = false;
this.mouseIsDown = false;
},
preloadAndOpen(index = null) {
// If we're in the middle of a swipe gesture, don't open the lightbox
if (this.isSwiping || this.mouseIsDown) return;
// If an index is provided, update the current index
if (index !== null) {
this.currentIndex = index;
}
this.lightboxLoaded = false;
let img = new Image();
img.src = this.images[this.currentIndex].src;
this.lightboxLoaded = true;
this.open = true;
img.onload = () => { };
}
}
`}
@keydown.escape.window="open = false"
@keydown.window="if(open){
if($event.key === 'ArrowRight' && currentIndex < total - 1){
currentIndex++;
lightboxLoaded = false;
preloadAndOpen();
} else if($event.key === 'ArrowLeft' && currentIndex > 0){
currentIndex--;
lightboxLoaded = false;
preloadAndOpen();
}
}"
class="product-gallery"><div class="flex flex-col h-full">
<!-- Main Image (with swipe functionality) -->
<div
class="flex-1 p-1 flex items-center justify-center cursor-pointer rounded-lg"
@click="preloadAndOpen()"
@touchstart="touchStartX = $event.touches[0].clientX; isSwiping = true;"
@touchend="touchEndX = $event.changedTouches[0].clientX; handleSwipe();"
@touchcancel="isSwiping = false;"
@mousedown="mouseStartX = $event.clientX; mouseIsDown = true; $event.preventDefault();"
@mousemove="if(mouseIsDown) { mouseEndX = $event.clientX; }"
@mouseup="if(mouseIsDown) { mouseEndX = $event.clientX; handleSwipe(true); }"
@mouseleave="mouseIsDown = false;"
>
{images.map((image, index) => (
<div x-show={`currentIndex === ${index}`} key={index} class="w-full h-full flex items-center justify-center">
<Img
src={image.src}
alt={alt_translated[index]}
objectFit="contain"
format="avif"
sizes={mergedGallerySettings.SIZES_REGULAR}
loading="lazy"
attributes={{
img: { class: "main-image p-1 rounded-lg max-h-[60vh] aspect-square" }
}}
/>
</div>
))}
</div>
<!-- Image Info -->
{ (mergedGallerySettings.SHOW_TITLE || mergedGallerySettings.SHOW_DESCRIPTION) && (
<div class="text-center py-4">
{images.map((image, index) => (
<div x-show={`currentIndex === ${index}`} key={index}>
{ mergedGallerySettings.SHOW_TITLE && ( <h2 id="imageTitle" class="text-xl font-bold">{image.title}</h2>)}
{ mergedGallerySettings.SHOW_DESCRIPTION && (<p id="imageDescription" class="text-gray-600"><Translate>{ image.description}</Translate></p>)}
</div>
))}
</div>
)}
<!-- Thumbnails - only shown when multiple images exist -->
{images.length > 1 && (
<div class="p-1 overflow-x-auto scrollbar-thin scrollbar-thumb-gray-400 scrollbar-track-gray-200">
<div class="flex p-2 mt-2 ml-2 mr-2 gap-2 items-center justify-center">
{images.map((image, index) => (
<button
key={index}
x-on:click={`currentIndex = ${index};`}
:class={`currentIndex === ${index} ? 'ring-2 ring-orange-500' : ''`}
class="thumbnail thumbnail-btn flex-shrink rounded-lg"
>
<Img
src={image.src}
objectFit="cover"
format="avif"
placeholder="blurred"
sizes={mergedGallerySettings.SIZES_THUMB}
alt={alt_translated[index]}
breakpoints={[100]}
attributes={{
img: {
class: "w-32 h-32 rounded-lg hover:ring-2 hover:ring-blue-500 thumbnail-img aspect-square"
}
}}
loading="lazy"
/>
</button>
))}
</div>
</div>
)}
</div>
<!-- Lightbox Modal (with swipe functionality) -->
<div
x-show="open"
x-transition
:class="{ 'lightbox': !lightboxLoaded }"
class="fixed inset-0 bg-black bg-opacity-75 flex items-center justify-center lightbox z-10"
>
<div
class="relative max-w-full max-h-full"
@touchstart="touchStartX = $event.touches[0].clientX; isSwiping = true;"
@touchend="touchEndX = $event.changedTouches[0].clientX; handleSwipe();"
@touchcancel="isSwiping = false;"
@mousedown="mouseStartX = $event.clientX; mouseIsDown = true; $event.preventDefault();"
@mousemove="if(mouseIsDown) { mouseEndX = $event.clientX; }"
@mouseup="if(mouseIsDown) { mouseEndX = $event.clientX; handleSwipe(true); }"
@mouseleave="mouseIsDown = false;"
>
{images.map((image, index) => (
<div x-show={`currentIndex === ${index}`} key={index}>
<Img
src={image.src}
alt={alt_translated[index]}
placeholder="blurred"
format="avif"
objectFit="contain"
sizes={IMAGE_SETTINGS.LIGHTBOX.SIZES_LARGE}
attributes={{
img: { class: "max-w-[90vw] max-h-[90vh] object-contain rounded-lg lightbox-main" }
}}
/>
{ (mergedLightboxSettings.SHOW_TITLE || mergedLightboxSettings.SHOW_DESCRIPTION) &&
(image.title || image.description) && (
<div class="absolute bottom-0 left-1/2 transform -translate-x-1/2 m-[8px] max-h-[32vh] p-2 text-white bg-black/50 rounded-lg" style="width: 90%;">
{ mergedLightboxSettings.SHOW_TITLE && ( <h3 class="text-xl"><Translate>{image.title}</Translate></h3>)}
{ mergedLightboxSettings.SHOW_DESCRIPTION && (<p><Translate>{image.description}</Translate></p>)}
</div>
)}
</div>
))}
<!-- Close Button -->
<button
x-on:click="open = false"
class="absolute top-0 right-0 text-white text-2xl p-4 m-[8px] bg-gray-800/75 bg-opacity-75 rounded-lg lightbox-nav"
aria-label="Close"
>
&times;
</button>
<!-- Navigation Buttons -->
<button
x-show="currentIndex > 0"
x-on:click="currentIndex--; lightboxLoaded = false; preloadAndOpen();"
:disabled="!lightboxLoaded"
class="absolute left-0 top-1/2 transform -translate-y-1/2 p-4 m-[8px] text-white text-3xl bg-gray-800/75 bg-opacity-75 rounded-lg lightbox-nav"
:class="{'opacity-50 cursor-not-allowed': !lightboxLoaded, 'hover:bg-gray-700/75': lightboxLoaded}"
aria-label="Previous"
>
&#10094;
</button>
<button
x-show="currentIndex < total - 1"
x-on:click="currentIndex++; lightboxLoaded = false; preloadAndOpen();"
:disabled="!lightboxLoaded"
class="absolute right-0 top-1/2 transform -translate-y-1/2 p-4 m-[8px] text-white text-3xl bg-gray-800/75 bg-opacity-75 rounded-lg lightbox-nav"
:class="{'opacity-50 cursor-not-allowed': !lightboxLoaded, 'hover:bg-gray-700/50': lightboxLoaded}"
aria-label="Next"
>
&#10095;
</button>
</div>
<script type="application/ld+json" set:html={ JSON.stringify(ld) }></script>
</div>

View File

@ -2,11 +2,10 @@
import LGallery from "./GalleryK.astro";
import path from "node:path";
import fs from "node:fs";
import { glob } from 'glob';
import { glob } from "glob";
import { globBase, pathInfo } from "@polymech/commons";
import { GalleryImage } from "@/base/images.js";
import { gallery } from "@/base/media.js";
import { resolveImagePath } from '@/utils/path-resolution';
import { resolveImagePath } from "@/utils/path-resolution";
export interface Props {
images?: GalleryImage[];
@ -20,24 +19,28 @@ const { images, glob: globPattern, entryPath, ...props } = Astro.props;
// Get current content directory dynamically from URL
const currentUrl = Astro.url.pathname;
const pathSegments = currentUrl.split('/').filter(Boolean);
const pathSegments = currentUrl.split("/").filter(Boolean);
// Handle locale-aware URLs (e.g., /en/resources/test/ vs /resources/test/)
// Check if first segment is a locale (2-character language code)
const isLocaleFirst = pathSegments.length > 0 && pathSegments[0].length === 2 && /^[a-z]{2}$/.test(pathSegments[0]);
const isLocaleFirst =
pathSegments.length > 0 &&
pathSegments[0].length === 2 &&
/^[a-z]{2}$/.test(pathSegments[0]);
// Determine content subdirectory (e.g., 'resources', 'blog', etc.)
let contentSubdir = 'resources'; // fallback
let contentSubdir = "resources"; // fallback
if (pathSegments.length >= 1) {
contentSubdir = isLocaleFirst && pathSegments.length > 1
? pathSegments[1] // Skip locale: /en/resources/test/ -> "resources"
: pathSegments[0]; // No locale: /resources/test/ -> "resources"
contentSubdir =
isLocaleFirst && pathSegments.length > 1
? pathSegments[1] // Skip locale: /en/resources/test/ -> "resources"
: pathSegments[0]; // No locale: /resources/test/ -> "resources"
}
// Get the nested content directory (e.g., 'cassandra' from /resources/cassandra/home/)
// We need to distinguish between:
// 1. /resources/test -> root-level file test.mdx -> contentPath = "resources"
// 2. /es/resources/test -> root-level file test.mdx with locale -> contentPath = "resources"
// 2. /es/resources/test -> root-level file test.mdx with locale -> contentPath = "resources"
// 3. /resources/cassandra/home -> nested file cassandra/home.mdx -> contentPath = "resources/cassandra"
// 4. /es/resources/cassandra/home -> nested file with locale -> contentPath = "resources/cassandra"
let contentPath = contentSubdir;
@ -56,7 +59,7 @@ if (pathSegments.length >= minNestedSegments) {
}
}
const contentDir = path.join(process.cwd(), 'src', 'content', contentPath);
const contentDir = path.join(process.cwd(), "src", "content", contentPath);
let allImages: GalleryImage[] = [];
@ -66,13 +69,20 @@ if (globPattern) {
// For content collections, we need to create a mock product structure
// that the gallery function can work with
const mockProductPath = `content/${contentSubdir}`;
// Since gallery expects a specific directory structure, let's use our custom approach
// but leverage the media processing logic where possible
let matchedFiles = glob.sync(globPattern, { cwd: contentDir, absolute: true });
let matchedFiles = glob.sync(globPattern, {
cwd: contentDir,
absolute: true,
});
if (matchedFiles.length === 0) {
const pathInfo2 = pathInfo(globPattern, false, path.join(contentDir, globBase(globPattern).base));
const pathInfo2 = pathInfo(
globPattern,
false,
path.join(contentDir, globBase(globPattern).base),
);
matchedFiles = pathInfo2.FILES;
}
@ -81,8 +91,8 @@ if (globPattern) {
matchedFiles.map(async (filePath) => {
const relativePath = path.relative(process.cwd(), filePath);
const fileName = path.basename(filePath, path.extname(filePath));
const webPath = `/${relativePath.replace(/\\/g, '/')}`;
const webPath = `/${relativePath.replace(/\\/g, "/")}`;
// Create basic image structure
const image: GalleryImage = {
name: fileName,
@ -91,9 +101,11 @@ if (globPattern) {
thumb: webPath,
responsive: webPath,
alt: `Image: ${fileName}`,
title: fileName.replace(/[-_]/g, ' ').replace(/\b\w/g, l => l.toUpperCase()),
title: fileName
.replace(/[-_]/g, " ")
.replace(/\b\w/g, (l) => l.toUpperCase()),
description: `Auto-loaded from ${globPattern}`,
keywords: '',
keywords: "",
width: 0,
height: 0,
gps: { lon: 0, lat: 0 },
@ -101,13 +113,13 @@ if (globPattern) {
format: path.extname(filePath).toLowerCase().slice(1),
width: 0,
height: 0,
space: '',
space: "",
channels: 0,
depth: '',
depth: "",
density: 0,
chromaSubsampling: '',
chromaSubsampling: "",
isProgressive: false,
resolutionUnit: '',
resolutionUnit: "",
hasProfile: false,
hasAlpha: false,
orientation: 0,
@ -116,42 +128,42 @@ if (globPattern) {
alt: `Image: ${fileName}`,
keywords: "",
title: "",
description: ""
description: "",
},
markdown: ''
}
markdown: "",
},
};
// Try to load companion markdown and JSON files (like media.ts does)
const baseDir = path.dirname(filePath);
const metaJsonPath = path.join(baseDir, `${fileName}.json`);
const metaMarkdownPath = path.join(baseDir, `${fileName}.md`);
try {
if (fs.existsSync(metaJsonPath)) {
const metaJson = JSON.parse(fs.readFileSync(metaJsonPath, 'utf8'));
const metaJson = JSON.parse(fs.readFileSync(metaJsonPath, "utf8"));
image.meta!.json = metaJson;
image.alt = metaJson.alt || image.alt;
image.title = metaJson.title || image.title;
image.description = metaJson.description || image.description;
}
if (fs.existsSync(metaMarkdownPath)) {
const markdown = fs.readFileSync(metaMarkdownPath, 'utf8');
const markdown = fs.readFileSync(metaMarkdownPath, "utf8");
image.meta!.markdown = markdown;
image.description = markdown || image.description;
}
} catch (e) {
console.warn(`Error loading metadata for ${fileName}:`, e);
}
return image;
})
}),
);
allImages = [...globImages];
} catch (error) {
console.warn('Glob pattern failed:', error);
console.warn("Glob pattern failed:", error);
allImages = [];
}
}
@ -162,15 +174,15 @@ if (images) {
}
// Resolve relative paths in all image sources and convert to LGallery format
const resolvedImages = allImages.map(image => {
const resolvedImages = allImages.map((image) => {
const resolvedSrc = resolveImagePath(image.src, entryPath, Astro.url);
// Convert GalleryImage to LGallery's expected Image interface
return {
src: resolvedSrc,
alt: image.alt || '',
alt: image.alt || "",
title: image.title,
description: image.description
description: image.description,
};
});
---

View File

@ -1,13 +1,13 @@
---
import { DEFAULT_IMAGE_URL } from '@/app/config.js'
import { Picture } from "imagetools/components"
import { image_url } from "@/base/media.js"
import { DEFAULT_IMAGE_URL } from "@/app/config.js";
import { Picture } from "imagetools/components";
import { image_url } from "../../base/media.js";
interface SafeImageProps {
src: string
alt: string
fallback?: string
[propName: string]: any
src: string;
alt: string;
fallback?: string;
[propName: string]: any;
}
const {
@ -15,8 +15,9 @@ const {
alt,
fallback = DEFAULT_IMAGE_URL,
...rest
} = Astro.props as SafeImageProps
} = Astro.props as SafeImageProps;
const srcSafe = await image_url(src, fallback)
const srcSafe = await image_url(src, fallback);
---
<Picture class="" src={srcSafe} alt={alt} {...rest} />

View File

@ -1,18 +1,15 @@
import * as path from 'path'
import { findUp } from 'find-up'
import { sync as read } from '@polymech/fs/read'
import { sync as exists } from '@polymech/fs/exists'
import { filesEx, forward_slash, resolveConfig } from '@polymech/commons'
import { ICADNodeSchema, IComponentConfig } from '@polymech/commons/component'
import { ContentEntryRenderFunction, ContentEntryType } from 'astro'
import { RenderedContent, DataEntry } from "astro:content"
import { DataEntry } from "astro:content"
import type { Loader, LoaderContext } from 'astro/loaders'
import {
CAD_MAIN_MATCH, PRODUCT_BRANCHES,
CAD_EXTENSIONS, CAD_MODEL_EXT, PRODUCT_DIR, PRODUCT_GLOB,
@ -22,16 +19,16 @@ import {
parseBoolean
} from 'config/config.js'
import { env } from '../base/index.js'
import { gallery } from '@/base/media.js';
import { env } from '@/base/index.js'
import { gallery } from '@polymech/astro-base/base/media.js';
import { get } from '@polymech/commons/component'
import { PFilterValid } from '@polymech/commons/filter'
import { IAssemblyData } from '@polymech/cad'
import { logger as log } from '@/base/index.js'
interface ILoaderContextEx extends LoaderContext {
entryTypes: Map<string, ContentEntryType>
}
interface IComponentConfigEx extends IComponentConfig {
content: string
@ -42,12 +39,9 @@ interface IComponentConfigEx extends IComponentConfig {
}
export interface IStoreItem extends DataEntry {
data: IComponentConfigEx
rendered?: RenderedContent
}
let loaderCtx: ILoaderContextEx
const renderFunctionByContentType = new WeakMap<ContentEntryType, ContentEntryRenderFunction>();
const filterBranch = (items: { rel: string, config, path }[],
branch: string = RETAIL_PRODUCT_BRANCH) => {
@ -88,41 +82,56 @@ const onComponent = async (item: IStoreItem, ctx: ILoaderContextEx) => {
*/
}
export const getRenderFunction = async (fileName: string) => {
let entryType = loaderCtx.entryTypes.get(path.parse(fileName).ext) as ContentEntryType
let _render = renderFunctionByContentType.get(entryType) as ContentEntryRenderFunction
const cad = async (item: IStoreItem, ctx: ILoaderContextEx): Promise<ICADNodeSchema[]> => {
const root = PRODUCT_ROOT()
const data: IComponentConfigEx = item.data
const itemRel = data.rel
const default_profile = env(itemRel)
const mainAssemblies = filesEx(root, CAD_MAIN_MATCH(itemRel), { absolute: false })
//log.debug(`Loading CAD/CAM for ${itemRel} in ${root} with ${CAD_MAIN_MATCH(itemRel)}`)
const cadMeta = mainAssemblies.map((file: string): ICADNodeSchema => {
const result: ICADNodeSchema = {
file,
name: path.basename(file)
}
CAD_EXTENSIONS.forEach(ext => result[ext] = decodeURIComponent(forward_slash(CAD_URL(file.replace('.SLDASM', ext), data as Record<string, string>))))
result.model = path.join(root, file.replace('.SLDASM', CAD_MODEL_EXT))
return result
})
if (!cadMeta.length) {
return []
}
item.data.cad = cadMeta
item.data.preview3d = cadMeta[0].html
if (!_render) {
_render = await (entryType as any).getRenderFunction({})
renderFunctionByContentType.set(entryType, _render)
if (CAD_EXPORT_CONFIGURATIONS) {
const assemblies = cadMeta.filter((assembly) => assembly.model && exists(assembly.model))
const components = cadMeta.map((assembly) => {
const modelPath = assembly.model as string
const model: IAssemblyData = read(modelPath, 'json') as IAssemblyData
if (!model) {
return
}
const configurations = Object.keys(model.Configurations).filter((c) => {
return c !== CAD_DEFAULT_CONFIGURATION &&
parseBoolean(model.Configurations[c].Hide || '')
})
if (!configurations.length ||
parseBoolean(model.Configurations?.Global?.Configurations || '')) {
return
}
log.debug(`Loading CAD/CAM for ${itemRel}`, configurations)
})
}
return _render
}
export const renderMarkup = async (content, data: any, fileName: string = 'template.md') => {
if (!loaderCtx) {
debugger
log.error('Loader context not set')
return
}
const _render = await getRenderFunction(fileName)
if (!_render) {
log.error('No render function')
return
}
return _render({
body: content,
data,
filePath: fileName,
} as any)
return cadMeta
}
// Removed obsolete loader types and render functions
const onItem = async (item: IStoreItem, ctx: ILoaderContextEx) => {
if (!item || !item.data) {
ctx.logger.error(`Error completing ${''}: no data`);
return
}
if(!loaderCtx){
loaderCtx = ctx
}
const { logger } = ctx
let data: IComponentConfigEx = item.data
@ -134,17 +143,18 @@ const onItem = async (item: IStoreItem, ctx: ILoaderContextEx) => {
data.product_rel = itemRelMin
data.assets = {
renderings: [],
gallery: []
gallery: [],
screenshots: []
}
//////////////////////////////////////////
//
// Body
//
let contentPath = path.join(itemDir, 'templates/shared', 'body.md')
await getRenderFunction(contentPath)
if (exists(contentPath)) {
data.content = read(contentPath) as string
(item as any).filePath = contentPath
item.filePath = contentPath
}
//////////////////////////////////////////
//
@ -159,7 +169,7 @@ const onItem = async (item: IStoreItem, ctx: ILoaderContextEx) => {
let resourcesDefaultPath = await findUp('resources.md', {
stopAt: PRODUCT_ROOT(),
cwd: itemDir
}) || ""
}) || ""
exists(resourcesDefaultPath) && (data.shared_resources = read(resourcesDefaultPath) as string || "")
//////////////////////////////////////////
//
@ -199,7 +209,8 @@ const onItem = async (item: IStoreItem, ctx: ILoaderContextEx) => {
//
// Extensions, CAD, Media, etc.
//
data.cad = await cad(item, ctx)
data.assets.renderings = await gallery('renderings', data.rel) as []
data.assets.renderings.length && (data.thumbnail =
{
@ -208,10 +219,12 @@ const onItem = async (item: IStoreItem, ctx: ILoaderContextEx) => {
src: data.assets.renderings[0].thumb
})
data.assets.gallery = await gallery('media/gallery', data.rel) as []
data.assets.main_image = (data.assets.gallery as any[]).find((item: any) => item.src.endsWith('latest.jpg'))
data.image = data.assets.renderings[0] || {}
data.assets.showcase = await gallery('media/showcase', data.rel) as []
data.assets.samples = await gallery('media/samples', data.rel) as []
data.assets.screenshots = await gallery('media/screenshots', data.rel) as []
}
export function loader(): Loader {
@ -221,8 +234,8 @@ export function loader(): Loader {
watcher,
parseData,
store,
generateDigest,
entryTypes }: ILoaderContextEx) => {
generateDigest
}: ILoaderContextEx) => {
store.clear();
let products = items({})
@ -251,8 +264,7 @@ export function loader(): Loader {
watcher,
parseData,
store,
generateDigest,
entryTypes
generateDigest
} as any)
storeItem.data['config'] = JSON.stringify({
...storeItem.data
@ -264,4 +276,4 @@ export function loader(): Loader {
name: "store-loader",
load
};
}
}