fix: preserve imported dark image variants
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
export interface PortfolioImage {
|
||||
src: string;
|
||||
darkSrc?: string;
|
||||
width: number;
|
||||
height: number;
|
||||
blurDataURL?: string;
|
||||
|
||||
Reference in New Issue
Block a user