Skip to content

Commit 34220c1

Browse files
committed
important commit, fix hero image flicker, add fixed blur delay 200ms, update zod types somewhat
1 parent e7273c9 commit 34220c1

File tree

6 files changed

+77
-37
lines changed

6 files changed

+77
-37
lines changed

docs/working-notes/todo4.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -184,6 +184,9 @@ clean comments for astro jsx
184184
<!-- Mobile Web App -->
185185
https://github.com/benavlabs/landstro
186186

187+
------------
188+
update Zod code
189+
astro zod v3, main zod v4
187190
```
188191

189192

src/components/react/ImageBlurPreloader.tsx

Lines changed: 17 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,15 @@
11
import { useEffect, useState } from 'react';
22

3+
import useDelayed from '@/components/react/hooks/useDelayed';
34
import usePrevious from '@/components/react/hooks/usePrevious';
5+
import { CONFIG_CLIENT } from '@/config/client';
46
import { cn } from '@/utils/styles';
57

68
import type { ImgTagAttributes } from '@/types/image';
79
import type { FC } from 'react';
810

11+
const { BLUR_IMAGE_DELAY } = CONFIG_CLIENT;
12+
913
interface Props {
1014
blurAttributes: ImgTagAttributes;
1115
mainAttributes: ImgTagAttributes;
@@ -28,18 +32,21 @@ const ImageBlurPreloader: FC<Props> = ({
2832

2933
const prevMainAttributes = usePrevious(mainAttributes);
3034

31-
const isNewImage = !(
35+
// track transition to new image
36+
const isMainImageChanged = !(
3237
prevMainAttributes?.src === mainAttributes.src &&
3338
prevMainAttributes.srcSet === mainAttributes.srcSet
3439
);
3540

41+
const isDelayedBlur = useDelayed(isMainImageChanged, BLUR_IMAGE_DELAY);
42+
3643
// reset isLoading on main image change
3744
useEffect(() => {
38-
if (isNewImage) {
45+
if (isMainImageChanged) {
3946
setIsLoadingBlur(true);
4047
setIsLoadingMain(true);
4148
}
42-
}, [isNewImage, setIsLoadingMain, setIsLoadingBlur]);
49+
}, [isMainImageChanged, setIsLoadingMain, setIsLoadingBlur]);
4350

4451
// important: main image must be in DOM for onLoad to work
4552
// unmount and display: none will fail
@@ -63,10 +70,6 @@ const ImageBlurPreloader: FC<Props> = ({
6370
: blurAttributes.src || blurAttributes.srcSet
6471
);
6572

66-
// Todo: cant use opacity for control, must use 2 <img /> tags
67-
// incomingMain, currentMain states, and assign key for unmount
68-
// https://chatgpt.com/s/t_689754f4fbf8819181ad44b5f3a0bbd7
69-
7073
return (
7174
<div className={cn('relative size-full', divClassName)}>
7275
{hasImage && (
@@ -88,8 +91,13 @@ const ImageBlurPreloader: FC<Props> = ({
8891
onLoad={handleLoadMain}
8992
className={cn(
9093
'object-cover absolute top-0 left-0 size-full',
91-
// important: dont hide main image until next blur image is loaded
92-
isLoadingMain && !isLoadingBlur ? 'opacity-0' : 'opacity-100',
94+
// important:
95+
// _!isLoadingBlur - don't hide main image until next blur image is loaded
96+
// isMainImageChanged - don't show main image while it's transitioning - solves flickering
97+
// isDelayedBlur - fixed blur delay
98+
!isLoadingBlur && (isLoadingMain || isMainImageChanged || isDelayedBlur)
99+
? 'opacity-0'
100+
: 'opacity-100',
93101
className
94102
)}
95103
/>
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
import { useEffect, useState } from 'react';
2+
3+
const useDelayed = (tracked: boolean, delayMs: number) => {
4+
const [delayed, setDelayed] = useState(false);
5+
6+
useEffect(() => {
7+
if (!tracked) {
8+
// No delay, set immediately
9+
if (delayMs === 0) {
10+
setDelayed(false);
11+
return;
12+
}
13+
14+
setDelayed(true);
15+
16+
const timeout = setTimeout(() => {
17+
setDelayed(false);
18+
}, delayMs);
19+
20+
return () => clearTimeout(timeout);
21+
}
22+
}, [tracked, delayMs]);
23+
24+
return delayed;
25+
};
26+
27+
export default useDelayed;

src/config/client.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ const configClientData: ConfigClientType = {
1818
PAGE_SIZE_POST_CARD_SMALL: 6,
1919
PAGE_SIZE_PROJECT_CARD: 6,
2020
MORE_POSTS_COUNT: 3,
21+
BLUR_IMAGE_DELAY: 200,
2122
DEFAULT_MODE: 'light',
2223
DEFAULT_THEME: 'default-light',
2324
AUTHOR_NAME: 'Nemanja Mitic',

src/schemas/config.ts

Lines changed: 23 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -15,10 +15,10 @@ export const processEnvSchema = z.object({
1515
PREVIEW_MODE: z
1616
.enum(booleanValues)
1717
.transform((value) => value === 'true')
18-
.default('false'),
18+
.default(false),
1919
// ensure no trailing slash
20-
SITE_URL: z.string().url().regex(/[^/]$/, 'SITE_URL should not end with a slash "/"'),
21-
PLAUSIBLE_SCRIPT_URL: z.string().url().or(z.literal('')).optional(),
20+
SITE_URL: z.url().regex(/[^/]$/, 'SITE_URL should not end with a slash "/"'),
21+
PLAUSIBLE_SCRIPT_URL: z.url().or(z.literal('')).optional(),
2222
PLAUSIBLE_DOMAIN: z
2323
.string()
2424
.or(z.enum(['', 'localhost'])) // for types
@@ -30,7 +30,7 @@ export const processEnvSchema = z.object({
3030
value === '' ||
3131
value === 'localhost' || // astro:env default
3232
domainSubdomainRegex.test(value),
33-
(value) => ({ message: `Invalid hostname for PLAUSIBLE_DOMAIN 1: ${value}` })
33+
{ message: 'Invalid hostname for PLAUSIBLE_DOMAIN 1' }
3434
),
3535
});
3636

@@ -40,23 +40,22 @@ export const configServerSchema = processEnvSchema
4040

4141
export const configClientSchema = processEnvSchema
4242
.pick({ SITE_URL: true, PLAUSIBLE_SCRIPT_URL: true, PLAUSIBLE_DOMAIN: true })
43-
.merge(
44-
z.object({
45-
SITE_URL_CANONICAL: z.string().min(1),
46-
SITE_TITLE: z.string().min(1),
47-
SITE_DESCRIPTION: z.string().min(1),
48-
PAGE_SIZE_POST_CARD: z.number(),
49-
PAGE_SIZE_POST_CARD_SMALL: z.number(),
50-
PAGE_SIZE_PROJECT_CARD: z.number(),
51-
MORE_POSTS_COUNT: z.number(),
52-
DEFAULT_MODE: z.enum(modeValues), // check that theme and mode match
53-
DEFAULT_THEME: z.enum(themeValues),
54-
AUTHOR_NAME: z.string().min(1),
55-
AUTHOR_EMAIL: z.string().email(),
56-
AUTHOR_GITHUB: z.string().url(),
57-
AUTHOR_LINKEDIN: z.string().url(),
58-
AUTHOR_TWITTER: z.string().url(),
59-
AUTHOR_YOUTUBE: z.string().url(),
60-
REPO_URL: z.string().url(),
61-
})
62-
);
43+
.extend({
44+
SITE_URL_CANONICAL: z.string().min(1),
45+
SITE_TITLE: z.string().min(1),
46+
SITE_DESCRIPTION: z.string().min(1),
47+
PAGE_SIZE_POST_CARD: z.number(),
48+
PAGE_SIZE_POST_CARD_SMALL: z.number(),
49+
PAGE_SIZE_PROJECT_CARD: z.number(),
50+
MORE_POSTS_COUNT: z.number(),
51+
BLUR_IMAGE_DELAY: z.number(),
52+
DEFAULT_MODE: z.enum(modeValues), // check that theme and mode match
53+
DEFAULT_THEME: z.enum(themeValues),
54+
AUTHOR_NAME: z.string().min(1),
55+
AUTHOR_EMAIL: z.email(),
56+
AUTHOR_GITHUB: z.url(),
57+
AUTHOR_LINKEDIN: z.url(),
58+
AUTHOR_TWITTER: z.url(),
59+
AUTHOR_YOUTUBE: z.url(),
60+
REPO_URL: z.url(),
61+
});

src/utils/validation.ts

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,14 @@
11
import { z } from 'zod';
22

3-
import type { ZodSchema } from 'zod';
3+
import type { ZodError, ZodType } from 'zod';
44

5-
export const zodErrorToString = (error: z.ZodError): string => {
6-
return error.errors.map((err: z.ZodIssue) => `${err.path.join('.')}: ${err.message}`).join(', ');
5+
export const zodErrorToString = (error: ZodError): string => {
6+
return error.issues
7+
.map((err: z.core.$ZodIssue) => `${err.path.join('.')}: ${err.message}`)
8+
.join(', ');
79
};
810

9-
export const validateData = <T extends ZodSchema>(config: z.infer<T>, schema: T): z.infer<T> => {
11+
export const validateData = <T extends ZodType>(config: z.infer<T>, schema: T): z.infer<T> => {
1012
const parsedConfig = schema.safeParse(config);
1113

1214
if (!parsedConfig.success) {

0 commit comments

Comments
 (0)