Skip to content

Commit a9a1db4

Browse files
authored
Merge pull request #31 from amplience/dev
2 parents cb82a8c + a3adcf9 commit a9a1db4

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

43 files changed

+1284
-156
lines changed

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -170,7 +170,7 @@ In your Vercel project browse to Settings --> Environment Variables and edit the
170170

171171

172172
## Additional Topics
173-
- [Features Highlights](docs/FeatureHiLites.md)
173+
- [Features Highlights](docs/FeatureHighlights.md)
174174
- [High-Level Architecture](docs/ArchDiagram.md)
175175
- [Available Components](docs/Components.md)
176176
- [Exploring features](docs/DeepDive.md)

components/admin/AdminPanel/AdminPanel.tsx

Lines changed: 21 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,21 +7,28 @@ import VisibilityIcon from '@mui/icons-material/Visibility';
77
import Accordion from '@mui/material/Accordion';
88
import AccordionSummary from '@mui/material/AccordionSummary';
99
import AccordionDetails from '@mui/material/AccordionDetails';
10+
import ElectricBoltIcon from '@mui/icons-material/ElectricBolt';
1011
import { withStyles, WithStyles } from '@mui/styles'
1112

1213
import WithAdminTheme from '@components/admin/AdminTheme';
1314
import ComponentsPanel from './panels/ComponentsPanel';
1415
import ContentPreviewPanel from './panels/ContentPreviewPanel';
1516
import { getHubName } from '@lib/config/locator/config-locator';
1617
import { useECommerce } from '@components/core/Masthead/ECommerceContext';
18+
import AcceleratedMediaPanel from './panels/AcceleratedMediaPanel';
1719

1820
const styles = (theme: Theme) => ({
1921
root: {
2022
},
2123
logo: {
2224
display: 'flex',
2325
padding: '10px 10px 4px 10px',
24-
justifyContent: 'left'
26+
justifyContent: 'center'
27+
},
28+
environment: {
29+
display: 'flex',
30+
justifyContent: 'left',
31+
padding: '8px'
2532
},
2633
icon: {
2734
marginRight: '0.4rem',
@@ -50,11 +57,12 @@ const AdminPanel: React.FunctionComponent<Props> = (props) => {
5057
<Image src="/images/amplience.png" width={247} height={100} alt='amplience' />
5158
</div>
5259
<Divider />
53-
<div className={classes.logo}>
60+
<div className={classes.environment}>
5461
<div>
5562
<span>hub</span> <span><b>{hubname}</b></span>
5663
</div>
57-
<div style={{ marginLeft: '40px' }}>
64+
<div style={{flexGrow: 1}} />
65+
<div style={{justifyContent: 'right'}}>
5866
<span>vendor</span> <span><b>{vendor}</b></span>
5967
</div>
6068
</div>
@@ -78,6 +86,16 @@ const AdminPanel: React.FunctionComponent<Props> = (props) => {
7886
<ComponentsPanel />
7987
</AccordionDetails>
8088
</Accordion>
89+
90+
<Accordion key={'Accelerated Media'}>
91+
<AccordionSummary expandIcon={<ExpandMoreIcon />} aria-controls="panel1a-content">
92+
<ElectricBoltIcon className={classes.icon} />
93+
<Typography variant="button">{'Accelerated Media'}</Typography>
94+
</AccordionSummary>
95+
<AccordionDetails>
96+
<AcceleratedMediaPanel />
97+
</AccordionDetails>
98+
</Accordion>
8199
</div>
82100
</WithAdminTheme>
83101
);
Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
export interface ImageStatistics {
2+
src: string;
3+
name: string;
4+
types: { [key: string]: string }
5+
sizes: { [key: string]: number }
6+
auto: string;
7+
completed: number,
8+
total: number
9+
}
10+
11+
const formatTests = ['auto', 'jpeg', 'webp', 'avif']; // png deliberately excluded
12+
13+
export const formatColors: { [key: string]: string } = {
14+
jpeg: '#FFA200',
15+
webp: '#00B6FF',
16+
avif: '#65CC02',
17+
auto: '#8F9496',
18+
png: '#E94420'
19+
}
20+
21+
export const typeFromFormat: { [key: string]: string } = {
22+
'image/webp': 'webp',
23+
'image/jpeg': 'jpeg',
24+
'image/avif': 'avif',
25+
'image/png': 'png'
26+
};
27+
28+
29+
export function isValid(stat: ImageStatistics, key: string): boolean {
30+
let type = stat.types[key];
31+
let realKey = typeFromFormat[type] ?? key;
32+
33+
return key === 'auto' || key === realKey;
34+
}
35+
36+
export function hasInvalid(stat: ImageStatistics): boolean {
37+
for (const key of Object.keys(stat.sizes)) {
38+
if (!isValid(stat, key)) {
39+
return true;
40+
}
41+
}
42+
43+
return false;
44+
}
45+
46+
function getAcceptHeader(): string {
47+
// TODO: guess accept header based on browser version?
48+
return 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8';
49+
}
50+
51+
export async function DetermineImageSizes(onChange: (stats: ImageStatistics[]) => void) {
52+
const images = Array.from(document.images);
53+
54+
const uniqueSrc = new Set<string>();
55+
const result: ImageStatistics[] = [];
56+
57+
const promises: Promise<any>[] = [];
58+
59+
for (const image of images) {
60+
const src = image.currentSrc;
61+
62+
if (uniqueSrc.has(src)) {
63+
continue;
64+
}
65+
66+
uniqueSrc.add(src);
67+
68+
try {
69+
const url = new URL(src);
70+
71+
const isAmplienceRequest = url.pathname.startsWith('/i/') || url.pathname.startsWith('/s/');
72+
const accountName = url.pathname.split('/')[2];
73+
74+
if (isAmplienceRequest) {
75+
const imageResult: ImageStatistics = {
76+
src,
77+
name: url.pathname.split('/')[3],
78+
types: {},
79+
sizes: {},
80+
completed: 0,
81+
auto: 'none',
82+
total: formatTests.length
83+
}
84+
85+
result.push(imageResult);
86+
87+
onChange(result);
88+
89+
const formatPromises = formatTests.map(async format => {
90+
url.searchParams.set('fmt', format);
91+
92+
const src = url.toString();
93+
94+
try {
95+
const response = await fetch(src, { headers: { Accept: getAcceptHeader() }});
96+
97+
const headLength = response.headers.get("content-length");
98+
const size = headLength ? Number(headLength) : (await response.arrayBuffer()).byteLength;
99+
100+
imageResult.sizes[format] = size;
101+
imageResult.types[format] = response.headers.get("content-type") ?? '';
102+
imageResult.completed++;
103+
104+
if (format === 'auto') {
105+
imageResult.auto = typeFromFormat[imageResult.types[format]] ?? 'none'
106+
}
107+
108+
onChange(result);
109+
} catch (e) {
110+
console.log(`Could not scan image ${image.currentSrc}`);
111+
}
112+
});
113+
114+
promises.push(...formatPromises);
115+
}
116+
} catch (e) {
117+
console.log(`Not a valid URL ${image.currentSrc}`);
118+
}
119+
}
120+
121+
onChange(result);
122+
123+
await Promise.all(promises);
124+
125+
return result;
126+
}
Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
import React, { FC } from 'react'
2+
import { ImageStatistics, typeFromFormat, formatColors } from './ImageStatistics';
3+
import { Theme, Tooltip } from '@mui/material';
4+
import { WithStyles, withStyles } from '@mui/styles';
5+
6+
const styles = (theme: Theme) => ({
7+
container: {
8+
width: '100%',
9+
display: 'flex',
10+
flexDirection: 'column' as 'column'
11+
},
12+
barBase: {
13+
display: 'flex',
14+
alignItems: 'center',
15+
justifyContent: 'space-between',
16+
color: '#444444',
17+
height: '20px',
18+
margin: '2px 0',
19+
fontSize: '12px',
20+
gap: '5px'
21+
},
22+
format: {
23+
fontSize: '12px',
24+
marginLeft: '4px',
25+
whiteSpace: 'nowrap' as 'nowrap'
26+
},
27+
size: {
28+
fontSize: '12px',
29+
marginRight: '4px',
30+
}
31+
});
32+
33+
interface Props extends WithStyles<typeof styles> {
34+
stat: ImageStatistics;
35+
}
36+
37+
interface OrderedFormat {
38+
key: string,
39+
size: number,
40+
auto: boolean,
41+
realKey: string | null
42+
}
43+
44+
function getRealType(stat: ImageStatistics, key: string): string | null {
45+
let type = stat.types[key];
46+
47+
const realKey = typeFromFormat[type] ?? key;
48+
49+
return key === 'auto' || realKey == key ? null : realKey;
50+
}
51+
52+
function getOrderedFormats(stat: ImageStatistics): OrderedFormat[] {
53+
// Formats ordered by size.
54+
const formatSizes = Object.keys(stat.sizes)
55+
.sort()
56+
.filter(key => key !== 'auto')
57+
.map(key => ({
58+
key,
59+
size: stat.sizes[key],
60+
same: [key],
61+
auto: key === stat.auto,
62+
realKey: getRealType(stat, key)
63+
}));
64+
65+
formatSizes.sort((a, b) => a.size - b.size);
66+
67+
return formatSizes;
68+
}
69+
70+
const ImageStatisticsBars: FC<Props> = ({stat, classes}) => {
71+
const ordered = getOrderedFormats(stat);
72+
const maxSize = ordered[ordered.length - 1].size;
73+
const maxKey = ordered[ordered.length - 1].key;
74+
// ordered.reverse();
75+
76+
return <div className={classes.container}>
77+
{
78+
ordered.map((elem, index) => {
79+
const size = elem.size;
80+
const name = elem.key;
81+
const invalid = elem.realKey != null;
82+
const titleName = invalid ? `"${name}" (got ${elem.realKey})` : name;
83+
const title = `${titleName}: ${elem.size} bytes (${Math.round(1000 * elem.size / maxSize) / 10}% of ${maxKey})`;
84+
85+
return <Tooltip key={elem.key} title={title}>
86+
<div className={classes.barBase} style={{
87+
backgroundColor: formatColors[invalid ? 'auto' : elem.key],
88+
width: `${(size / maxSize) * 100}%`,
89+
outline: invalid ? '1px solid red' : ''
90+
}}>
91+
<span>
92+
<span className={classes.format} style={{textDecoration: invalid ? 'line-through' : ''}}>{`${name}${elem.auto ? ' (auto)' : ''}`}</span>
93+
{invalid ? <span className={classes.format}>{elem.realKey}</span> : null}
94+
</span>
95+
<span className={classes.size}>{elem.size}</span>
96+
</div>
97+
</Tooltip>
98+
})
99+
}
100+
</div>
101+
}
102+
103+
export default withStyles(styles)(ImageStatisticsBars);

0 commit comments

Comments
 (0)