Skip to content
Projects
Groups
Snippets
Help
This project
Loading...
Sign in / Register
Toggle navigation
N
nextjs-starter-template
Overview
Overview
Details
Activity
Cycle Analytics
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Issues
0
Issues
0
List
Board
Labels
Milestones
Merge Requests
0
Merge Requests
0
CI / CD
CI / CD
Pipelines
Jobs
Schedules
Charts
Registry
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Open sidebar
Moorthy G
nextjs-starter-template
Commits
1c77c59e
Commit
1c77c59e
authored
Dec 08, 2025
by
Moorthy G
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
refactor(shared/responsive-image): update to process the given sizes & srcSet values
parent
7179500a
Hide whitespace changes
Inline
Side-by-side
Showing
8 changed files
with
290 additions
and
86 deletions
+290
-86
AnotherButton.tsx
app/AnotherButton.tsx
+13
-0
HomeButton.tsx
app/another/HomeButton.tsx
+13
-0
ResponsiveImage.stories.tsx
...onents/shared/ResponsiveImage/ResponsiveImage.stories.tsx
+7
-7
ResponsiveImage.tsx
components/shared/ResponsiveImage/ResponsiveImage.tsx
+69
-30
Source.tsx
components/shared/ResponsiveImage/Source.tsx
+78
-43
breakpoints.ts
components/shared/ResponsiveImage/breakpoints.ts
+4
-6
processSizes.ts
components/shared/ResponsiveImage/processSizes.ts
+58
-0
processSrcSet.ts
components/shared/ResponsiveImage/processSrcSet.ts
+48
-0
No files found.
app/AnotherButton.tsx
0 → 100644
View file @
1c77c59e
'use client'
;
import
Link
from
'next/link'
;
import
{
Button
}
from
'@heroui/button'
;
export
const
AnotherButton
=
()
=>
{
return
(
<
Button
as=
{
Link
}
href=
"/another"
color=
"primary"
size=
"lg"
>
Navigate to another page
</
Button
>
);
};
app/another/HomeButton.tsx
0 → 100644
View file @
1c77c59e
'use client'
;
import
Link
from
'next/link'
;
import
{
Button
}
from
'@heroui/button'
;
export
const
HomeButton
=
()
=>
{
return
(
<
Button
as=
{
Link
}
href=
"/"
color=
"primary"
size=
"lg"
>
Navigate home page
</
Button
>
);
};
components/shared/ResponsiveImage/ResponsiveImage.stories.tsx
View file @
1c77c59e
...
...
@@ -24,7 +24,7 @@ export const ArtDirection = {
src
:
'https://picsum.photos/576/570?jpg'
,
width
:
576
,
height
:
540
,
sizes
:
'100vw'
sizes
:
'
lg 50vw, md 640px, sm
100vw'
},
render
:
(
args
:
ResponsiveImageProps
)
=>
(
<
ResponsiveImage
{
...
args
}
>
...
...
@@ -33,42 +33,42 @@ export const ArtDirection = {
src=
"https://picsum.photos/1920/540?jpg"
width=
{
1920
}
height=
{
540
}
sizes=
"100vw"
sizes=
"
xxl
100vw"
/>
<
Source
media=
"xl"
src=
"https://picsum.photos/1536/540?jpg"
width=
{
1536
}
height=
{
540
}
sizes=
"100vw"
sizes=
"
xl
100vw"
/>
<
Source
media=
"lg"
src=
"https://picsum.photos/1280/540?jpg"
width=
{
1280
}
height=
{
540
}
sizes=
"100vw"
sizes=
"
lg
100vw"
/>
<
Source
media=
"md"
src=
"https://picsum.photos/1024/540?jpg"
width=
{
1024
}
height=
{
540
}
sizes=
"100vw"
sizes=
"
md
100vw"
/>
<
Source
media=
"sm"
src=
"https://picsum.photos/768/540?jpg"
width=
{
768
}
height=
{
540
}
sizes=
"100vw"
sizes=
"
sm
100vw"
/>
<
Source
media=
"(min-width: 576px)"
src=
"https://picsum.photos/640/540?jpg"
width=
{
640
}
height=
{
540
}
sizes=
"100vw"
sizes=
"
sm
100vw"
/>
</
ResponsiveImage
>
)
...
...
components/shared/ResponsiveImage/ResponsiveImage.tsx
View file @
1c77c59e
import
React
,
{
forwardRef
}
from
'react'
;
import
{
forwardRef
}
from
'react'
;
import
{
getImageProps
}
from
'next/image'
;
import
{
processSizes
}
from
'./processSizes'
;
import
{
processSrcSet
}
from
'./processSrcSet'
;
export
type
Image
=
{
url
:
string
;
width
:
number
;
height
:
number
;
alternativeText
?:
string
;
};
/**
* ResponsiveImage component for displaying optimized images with responsive srcSet
*
* This component wraps Next.js Image optimization with custom srcSet filtering
* to ensure only image sizes <= actual image width are included in the srcSet.
*
* @example
* ```tsx
* <ResponsiveImage
* src="/image.jpg"
* width={1920}
* height={1080}
* sizes="100vw"
* alt="Description"
* />
* ```
*
* @example With art direction using Source children
* ```tsx
* <ResponsiveImage
* src="/image.jpg"
* width={576}
* height={540}
* sizes="lg 50vw, md 640px, sm 100vw"
* >
* <Source media="lg" src="/large.jpg" width={1280} height={540} sizes="100vw" />
* <Source media="md" src="/medium.jpg" width={1024} height={540} sizes="100vw" />
* </ResponsiveImage>
* ```
*/
export
interface
ResponsiveImageProps
extends
React
.
HTMLAttributes
<
HTMLImageElement
>
{
src
:
string
;
...
...
@@ -20,31 +45,45 @@ export interface ResponsiveImageProps
children
?:
React
.
ReactNode
;
}
export
const
ResponsiveImage
=
forwardRef
<
HTMLImageElement
,
ResponsiveImageProps
>
(
export
const
ResponsiveImage
=
forwardRef
<
HTMLImageElement
,
ResponsiveImageProps
>
(
(
{
src
,
width
,
height
,
alt
,
sizes
,
quality
,
priority
,
children
,
...
rest
},
{
src
,
width
,
height
,
alt
=
''
,
sizes
,
quality
,
priority
,
children
,
...
rest
},
ref
)
=>
{
if
(
src
)
{
const
{
props
}
=
getImageProps
({
src
,
width
,
height
,
sizes
,
quality
,
priority
,
alt
:
alt
||
''
});
return
(
<
picture
>
{
children
}
<
img
ref=
{
ref
}
{
...
props
}
{
...
rest
}
/>
</
picture
>
);
if
(
!
src
)
{
return
null
;
}
const
processedSizes
=
processSizes
(
sizes
);
const
{
props
}
=
getImageProps
({
src
,
width
,
height
,
sizes
:
processedSizes
,
quality
,
priority
,
alt
});
const
filteredSrcSet
=
processSrcSet
(
props
.
srcSet
,
width
);
return
(
<
picture
>
{
children
}
<
img
{
...
props
}
{
...
rest
}
ref=
{
ref
}
srcSet=
{
filteredSrcSet
}
alt=
{
alt
}
/>
</
picture
>
);
}
);
ResponsiveImage
.
displayName
=
'ResponsiveImage'
;
components/shared/ResponsiveImage/Source.tsx
View file @
1c77c59e
import
React
,
{
memo
}
from
'react'
;
import
{
memo
}
from
'react'
;
import
{
getImageProps
}
from
'next/image'
;
import
breakpoints
from
'./breakpoints'
;
import
breakpoints
,
{
type
BreakpointKey
}
from
'./breakpoints'
;
import
{
processSizes
}
from
'./processSizes'
;
import
{
processSrcSet
}
from
'./processSrcSet'
;
export
type
IResponsiveImageSourceProps
=
React
.
HTMLAttributes
<
HTMLSourceElement
>
&
{
src
:
string
;
width
:
number
;
height
:
number
;
media
:
string
;
sizes
:
string
;
quality
?:
number
;
};
export
interface
SourceProps
extends
React
.
HTMLAttributes
<
HTMLSourceElement
>
{
src
:
string
;
width
:
number
;
height
:
number
;
media
:
string
;
sizes
:
string
;
quality
?:
number
;
}
/**
* Source component for art direction with ResponsiveImage
*
* Used as a child of ResponsiveImage to provide different image sources
* for different media queries. The srcSet is automatically filtered to exclude
* widths greater than the actual image width to exclude redundant image sizes.
*
* @example
* ```tsx
* <ResponsiveImage src="/default.jpg" width={576} height={540} sizes="100vw">
* <Source
* media="lg"
* src="/large.jpg"
* width={1280}
* height={540}
* sizes="100vw"
* />
* <Source
* media="md"
* src="/medium.jpg"
* width={1024}
* height={540}
* sizes="100vw"
* />
* </ResponsiveImage>
* ```
*
* @example With custom media query
* ```tsx
* <Source
* media="(min-width: 900px)"
* src="/custom.jpg"
* width={1200}
* height={600}
* sizes="50vw"
* />
* ```
*/
export
const
Source
=
memo
(
({
src
,
width
,
height
,
sizes
,
media
,
quality
,
...
rest
}:
IResponsiveImageSourceProps
)
=>
{
if
(
src
)
{
const
query
=
breakpoints
[
media
]
||
media
;
const
{
props
}
=
getImageProps
({
width
,
height
,
src
,
sizes
,
quality
,
alt
:
''
});
return
(
<
source
media=
{
query
}
src=
{
props
.
src
}
srcSet=
{
props
.
srcSet
}
width=
{
props
.
width
}
height=
{
props
.
height
}
sizes=
{
props
.
sizes
}
{
...
rest
}
/>
);
({
src
,
width
,
height
,
sizes
,
media
,
quality
,
...
rest
}:
SourceProps
)
=>
{
if
(
!
src
)
{
return
null
;
}
return
null
;
const
query
=
(
media
in
breakpoints
?
breakpoints
[
media
as
BreakpointKey
]
:
null
)
||
media
;
const
processedSizes
=
processSizes
(
sizes
);
const
{
props
}
=
getImageProps
({
width
,
height
,
src
,
sizes
:
processedSizes
,
quality
,
alt
:
''
});
const
filteredSrcSet
=
processSrcSet
(
props
.
srcSet
,
width
);
return
(
<
source
media=
{
query
}
srcSet=
{
filteredSrcSet
||
props
.
src
}
width=
{
props
.
width
}
height=
{
props
.
height
}
sizes=
{
props
.
sizes
}
{
...
rest
}
/>
);
}
);
Source
.
displayName
=
'
ResponsiveImage
Source'
;
Source
.
displayName
=
'Source'
;
components/shared/ResponsiveImage/breakpoints.ts
View file @
1c77c59e
type
Breakpoints
=
{
[
key
:
string
]:
string
;
};
const
breakpoints
:
Breakpoints
=
{
const
breakpoints
=
{
sm
:
'(min-width: 640px)'
,
md
:
'(min-width: 768px)'
,
lg
:
'(min-width: 1024px)'
,
xl
:
'(min-width: 1280px)'
,
xxl
:
'(min-width: 1536px)'
};
}
as
const
;
export
type
BreakpointKey
=
keyof
typeof
breakpoints
;
export
default
breakpoints
;
components/shared/ResponsiveImage/processSizes.ts
0 → 100644
View file @
1c77c59e
import
breakpoints
,
{
type
BreakpointKey
}
from
'./breakpoints'
;
// Breakpoint order from largest to smallest for proper sizes attribute ordering
const
breakpointOrderMap
:
Record
<
BreakpointKey
,
number
>
=
{
xxl
:
0
,
xl
:
1
,
lg
:
2
,
md
:
3
,
sm
:
4
};
/**
* Processes the sizes attribute by replacing breakpoint names with media queries
* and ordering them from largest to smallest breakpoint (required for correct evaluation)
* Example: "sm 640px, lg 50vw, 85vw" -> "(min-width: 1024px) 50vw, (min-width: 640px) 640px, 85vw"
*/
export
function
processSizes
(
sizes
:
string
):
string
{
if
(
!
sizes
)
return
sizes
;
const
parts
=
sizes
.
split
(
','
);
const
breakpointEntries
:
Array
<
{
order
:
number
;
value
:
string
}
>
=
[];
const
defaultEntries
:
string
[]
=
[];
// Parse each part
for
(
const
part
of
parts
)
{
const
trimmed
=
part
.
trim
();
if
(
!
trimmed
)
continue
;
// Check if the part starts with a breakpoint name
const
breakpointMatch
=
trimmed
.
match
(
/^
(
sm|md|lg|xl|xxl
)\s
+/
);
if
(
breakpointMatch
)
{
const
breakpointKey
=
breakpointMatch
[
1
]
as
BreakpointKey
;
// Runtime validation: ensure breakpoint exists
if
(
breakpointKey
in
breakpoints
)
{
const
mediaQuery
=
breakpoints
[
breakpointKey
];
const
sizeValue
=
trimmed
.
substring
(
breakpointMatch
[
0
].
length
).
trim
();
const
processedValue
=
`
${
mediaQuery
}
${
sizeValue
}
`
;
breakpointEntries
.
push
({
order
:
breakpointOrderMap
[
breakpointKey
],
value
:
processedValue
});
}
}
else
{
// Default entry (no breakpoint) - keep at the end
defaultEntries
.
push
(
trimmed
);
}
}
// Sort breakpoint entries by order (smallest order number = largest breakpoint first)
breakpointEntries
.
sort
((
a
,
b
)
=>
a
.
order
-
b
.
order
);
// Combine: breakpoints (largest to smallest) + defaults
return
[...
breakpointEntries
.
map
((
e
)
=>
e
.
value
),
...
defaultEntries
].
join
(
', '
);
}
components/shared/ResponsiveImage/processSrcSet.ts
0 → 100644
View file @
1c77c59e
/**
* Processes the srcSet string to exclude width descriptors greater than the actual image width
* @param srcSet - The srcSet string from Next.js getImageProps (e.g., "/image.jpg?w=640 640w, /image.jpg?w=1024 1024w")
* @param maxWidth - The actual width of the image
* @returns Filtered srcSet string with only widths <= maxWidth
*/
export
function
processSrcSet
(
srcSet
:
string
|
undefined
,
maxWidth
:
number
):
string
|
undefined
{
if
(
!
srcSet
||
typeof
srcSet
!==
'string'
)
{
return
srcSet
;
}
// Split srcSet by comma to get individual entries
const
entries
=
srcSet
.
split
(
','
).
map
(
entry
=>
entry
.
trim
()).
filter
(
Boolean
);
// Filter and process each entry
const
filteredEntries
=
entries
.
map
(
entry
=>
{
// Extract width descriptor (e.g., "640w" from "/image.jpg?w=640 640w")
// Also handle density descriptors (e.g., "2x") - these should be included as-is
const
widthMatch
=
entry
.
match
(
/
\s
+
(\d
+
)
w$/
);
const
densityMatch
=
entry
.
match
(
/
\s
+
(\d
+
(?:\.\d
+
)?)
x$/
);
// If it's a density descriptor (e.g., "2x"), include it as-is
if
(
densityMatch
)
{
return
entry
;
}
// If it's a width descriptor, check against maxWidth
if
(
widthMatch
)
{
const
width
=
parseInt
(
widthMatch
[
1
],
10
);
// Include entry only if width is less than or equal to maxWidth
if
(
width
<=
maxWidth
)
{
return
entry
;
}
return
null
;
}
// If no descriptor found, include the entry as-is (fallback)
return
entry
;
})
.
filter
((
entry
):
entry
is
string
=>
entry
!==
null
);
// Return the filtered srcSet or undefined if empty
return
filteredEntries
.
length
>
0
?
filteredEntries
.
join
(
', '
)
:
undefined
;
}
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment