fix: preserve imported dark image variants

This commit is contained in:
2026-09-16 15:14:09 +03:30
parent 083a4e0b7b
commit 385703e5b5
5 changed files with 20 additions and 9 deletions

View File

@@ -11,13 +11,11 @@ interface ProjectThumbnailProps {
className?: string;
}
const getDarkImagePath = (src: string) => src.replace(/(\.[^./]+)$/, '-dark$1');
export const ProjectThumbnail: FC<ProjectThumbnailProps> = ({ image, title, className }) => {
const { theme } = useTheme();
const darkImage = getDarkImagePath(image.src);
const darkImage = image.darkSrc ?? image.src;
const [unavailableDarkImage, setUnavailableDarkImage] = useState<string | null>(null);
const useDarkImage = theme === 'dark' && unavailableDarkImage !== darkImage;
const useDarkImage = theme === 'dark' && darkImage !== image.src && unavailableDarkImage !== darkImage;
return (
<Image

View File

@@ -10,8 +10,10 @@ export const linkSchema = z.string().max(2048).refine(value => {
if (/^\/media\/[a-zA-Z0-9.-]+$/.test(value)) return true;
try { return ['https:', 'http:', 'mailto:', 'tel:'].includes(new URL(value).protocol); } catch { return false; }
}, 'Use an https, http, mailto, tel, or uploaded-media URL');
const imagePathSchema = z.string().regex(/^\/(?:images|media)\/[a-zA-Z0-9/_().-]+$/).refine(value => !value.includes('..'), 'Invalid image path');
export const imageSchema = z.object({
src: z.string().regex(/^\/(?:images|media)\/[a-zA-Z0-9/_().-]+$/).refine(value => !value.includes('..'), 'Invalid image path'),
src: imagePathSchema,
darkSrc: imagePathSchema.optional(),
width: z.number().int().min(1).max(20000), height: z.number().int().min(1).max(20000),
}).strict();
const strings = z.array(text).max(200);

View File

@@ -55,11 +55,20 @@ function readBundle(directory?: string) {
return { content, files };
}
function rewriteMedia<T>(value: T, urls: Map<string, string>): T {
function rewriteMedia<T>(value: T, urls: Map<string, string>, strict = true): T {
const darkVariants = new Map<string, string>();
for (const [source, target] of urls) {
const dark = urls.get(source.replace(/(\.[^./]+)$/, '-dark$1'));
if (dark) darkVariants.set(target, dark);
}
return JSON.parse(JSON.stringify(value), (_key, item) => {
if (item && typeof item === 'object' && typeof item.src === 'string') {
const darkSrc = darkVariants.get(item.src);
if (darkSrc && !item.darkSrc) return { ...item, darkSrc };
}
if (typeof item !== 'string') return item;
if (urls.has(item)) return urls.get(item);
if ((item.startsWith('/images/') && item !== '/images/portfolio-placeholder.svg') || item.startsWith('/media/')) throw new ContentError(`Missing bundle media: ${item}`);
if (strict && ((item.startsWith('/images/') && item !== '/images/portfolio-placeholder.svg') || item.startsWith('/media/'))) throw new ContentError(`Missing bundle media: ${item}`);
return item;
}) as T;
}
@@ -80,7 +89,7 @@ export function importLegacy(connection: DatabaseConnection, directory?: string,
const imported = rewriteMedia(bundle.content, urls);
const current = readContent(connection);
// Existing uploads and unrelated edits survive a media-only migration.
const migrated = JSON.parse(JSON.stringify(current), (_key, item) => typeof item === 'string' ? urls.get(item) ?? item : item) as ContentSnapshot;
const migrated = rewriteMedia(current, urls, false);
const uploads = path.join(destination, 'uploads');
mkdirSync(uploads, { recursive: true, mode: 0o700 });
for (const file of bundle.files) {

View File

@@ -1,5 +1,6 @@
export interface PortfolioImage {
src: string;
darkSrc?: string;
width: number;
height: number;
blurDataURL?: string;