new text ad provider (#3303)
* new text ad provider * fix test * fixes for production * Add loading skeleton to SevioTextAd with script-driven timeout fallback Co-authored-by: Hanzo Dev <[email protected]> * effect fix --------- Co-authored-by: Hanzo Dev <[email protected]>
@@ -5,6 +5,18 @@ import type { AdTextProviders } from 'types/client/adProviders';
|
||||
import app from '../app';
|
||||
import { getEnvValue } from '../utils';
|
||||
|
||||
const SEVIO_ZONE = '179b0a0e-19b0-4b2a-ad15-7446870bf189';
|
||||
const SEVIO_INVENTORY_ID = '65597ae2-67b8-404b-ac28-43d5029389da';
|
||||
const SEVIO_ACCOUNT_ID = 'e08c2e1e-2213-49d8-a397-c2e32094fba4';
|
||||
const SEVIO_AD_TYPE = 'native';
|
||||
|
||||
interface SevioConfig {
|
||||
readonly zone: string;
|
||||
readonly inventoryId: string;
|
||||
readonly accountId: string;
|
||||
readonly adType: string;
|
||||
}
|
||||
|
||||
const provider: AdTextProviders = (() => {
|
||||
const envValue = getEnvValue('NEXT_PUBLIC_AD_TEXT_PROVIDER') as AdTextProviders;
|
||||
return envValue && SUPPORTED_AD_TEXT_PROVIDERS.includes(envValue) ? envValue : 'coinzilla';
|
||||
@@ -12,12 +24,18 @@ const provider: AdTextProviders = (() => {
|
||||
|
||||
const title = 'Text ads';
|
||||
|
||||
const config: Feature<{ provider: AdTextProviders }> = (() => {
|
||||
const config: Feature<{ provider: AdTextProviders; sevio: SevioConfig }> = (() => {
|
||||
if (!app.isPrivateMode && provider !== 'none') {
|
||||
return Object.freeze({
|
||||
title,
|
||||
isEnabled: true,
|
||||
provider,
|
||||
sevio: {
|
||||
zone: SEVIO_ZONE,
|
||||
inventoryId: SEVIO_INVENTORY_ID,
|
||||
accountId: SEVIO_ACCOUNT_ID,
|
||||
adType: SEVIO_AD_TYPE,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -213,6 +213,8 @@
|
||||
"SEMRESATTRS",
|
||||
"Semrush",
|
||||
"servedbyadbutler",
|
||||
"sevio",
|
||||
"sevioads",
|
||||
"Shavuha",
|
||||
"Shavukha",
|
||||
"shibarium",
|
||||
|
||||
@@ -13,6 +13,7 @@ declare global {
|
||||
export interface Window {
|
||||
ethereum?: WalletProvider | undefined;
|
||||
coinzilla_display: Array<CPreferences>;
|
||||
sevioads: Array<Array<Record<string, string>>> | undefined;
|
||||
ga?: {
|
||||
getAll: () => Array<{ get: (prop: string) => string }>;
|
||||
};
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
export const duck = {
|
||||
ad: {
|
||||
name: 'Hello utia!',
|
||||
description_short: 'Utia is the best! Go with utia! Utia is the best! Go with utia!',
|
||||
thumbnail: 'http://localhost:3100/utia.jpg',
|
||||
url: 'https://test.url',
|
||||
cta_button: 'Click me!',
|
||||
},
|
||||
thumbnail: 'http://localhost:3100/utia.jpg',
|
||||
sponsored: 'Hello utia!',
|
||||
title: 'Utia is the best! Go with utia! Utia is the best! Go with utia!',
|
||||
clickURL: 'https://test.url',
|
||||
ctatext: 'Click me!',
|
||||
};
|
||||
|
||||
@@ -12,6 +12,9 @@ export function ad(): CspDev.DirectiveDescriptor {
|
||||
'*.coinzilla.com',
|
||||
'https://request-global.czilladx.com',
|
||||
|
||||
// sevio (coinzilla text ad)
|
||||
'*.adx.ws',
|
||||
|
||||
// adbutler
|
||||
'servedbyadbutler.com',
|
||||
|
||||
@@ -37,6 +40,9 @@ export function ad(): CspDev.DirectiveDescriptor {
|
||||
|
||||
// slise
|
||||
'*.slise.xyz',
|
||||
|
||||
// sevio
|
||||
'cdn.adx.ws',
|
||||
],
|
||||
'img-src': [
|
||||
// coinzilla
|
||||
@@ -44,6 +50,9 @@ export function ad(): CspDev.DirectiveDescriptor {
|
||||
|
||||
// adbutler
|
||||
'servedbyadbutler.com',
|
||||
|
||||
// sevio
|
||||
'*.adx.ws',
|
||||
],
|
||||
'font-src': [
|
||||
// coinzilla
|
||||
|
||||
@@ -4,14 +4,47 @@ import * as textAdMock from 'mocks/ad/textAd';
|
||||
|
||||
export type MockTextAdFixture = () => Promise<void>;
|
||||
|
||||
const SEVIO_LOADER_URL = 'https://cdn.adx.ws/scripts/loader.js';
|
||||
|
||||
const fixture: TestFixture<MockTextAdFixture, { page: Page }> = async({ page }, use) => {
|
||||
await use(async() => {
|
||||
const { thumbnail, sponsored, title, clickURL, ctatext } = textAdMock.duck;
|
||||
|
||||
await page.route('https://request-global.czilladx.com/serve/native.php?z=19260bf627546ab7242', (route) => route.fulfill({
|
||||
status: 200,
|
||||
json: textAdMock.duck,
|
||||
}));
|
||||
await page.route(textAdMock.duck.ad.thumbnail, (route) => {
|
||||
const adHtml = [
|
||||
`<strong>Ads:</strong> `,
|
||||
// eslint-disable-next-line max-len
|
||||
`<img src="${ thumbnail }" width="20" height="20" style="display: inline-block; vertical-align: text-bottom; margin-right: 4px; margin-left: 7px;" alt="" />`,
|
||||
`${ sponsored } \u2013 ${ title } `,
|
||||
`<a href="${ clickURL }" target="_blank">${ ctatext }</a>`,
|
||||
].join('');
|
||||
|
||||
// Route the Sevio loader script so that onLoad fires (not onError), which starts
|
||||
// the ad-served timeout in the component.
|
||||
await page.route(SEVIO_LOADER_URL, (route) => {
|
||||
return route.fulfill({ status: 200, contentType: 'application/javascript', body: '' });
|
||||
});
|
||||
|
||||
await page.evaluate((html) => {
|
||||
const queue: Array<Array<Record<string, string>>> = [];
|
||||
queue.push = () => {
|
||||
// Defer injection so the MutationObserver (set up in the next useEffect) is
|
||||
// already observing the div when the HTML arrives.
|
||||
setTimeout(() => {
|
||||
const container = document.querySelector('.sevioads');
|
||||
if (container) {
|
||||
container.innerHTML = html;
|
||||
}
|
||||
}, 0);
|
||||
return queue.length;
|
||||
};
|
||||
Object.defineProperty(window, 'sevioads', {
|
||||
value: queue,
|
||||
writable: true,
|
||||
configurable: true,
|
||||
});
|
||||
}, adHtml);
|
||||
|
||||
await page.route(thumbnail, (route) => {
|
||||
return route.fulfill({
|
||||
status: 200,
|
||||
path: './playwright/mocks/image_s.jpg',
|
||||
|
||||
|
Before Width: | Height: | Size: 98 KiB After Width: | Height: | Size: 98 KiB |
|
Before Width: | Height: | Size: 55 KiB After Width: | Height: | Size: 56 KiB |
|
Before Width: | Height: | Size: 62 KiB After Width: | Height: | Size: 62 KiB |
|
Before Width: | Height: | Size: 63 KiB After Width: | Height: | Size: 64 KiB |
|
Before Width: | Height: | Size: 60 KiB After Width: | Height: | Size: 60 KiB |
|
Before Width: | Height: | Size: 46 KiB After Width: | Height: | Size: 47 KiB |
|
Before Width: | Height: | Size: 114 KiB After Width: | Height: | Size: 115 KiB |
|
Before Width: | Height: | Size: 58 KiB After Width: | Height: | Size: 59 KiB |
|
Before Width: | Height: | Size: 48 KiB After Width: | Height: | Size: 49 KiB |
|
Before Width: | Height: | Size: 63 KiB After Width: | Height: | Size: 64 KiB |
|
Before Width: | Height: | Size: 60 KiB After Width: | Height: | Size: 61 KiB |
|
Before Width: | Height: | Size: 80 KiB After Width: | Height: | Size: 80 KiB |
|
Before Width: | Height: | Size: 69 KiB After Width: | Height: | Size: 69 KiB |
|
Before Width: | Height: | Size: 70 KiB After Width: | Height: | Size: 70 KiB |
|
Before Width: | Height: | Size: 63 KiB After Width: | Height: | Size: 63 KiB |
|
Before Width: | Height: | Size: 48 KiB After Width: | Height: | Size: 48 KiB |
|
Before Width: | Height: | Size: 40 KiB After Width: | Height: | Size: 41 KiB |
|
Before Width: | Height: | Size: 38 KiB After Width: | Height: | Size: 39 KiB |
|
Before Width: | Height: | Size: 37 KiB After Width: | Height: | Size: 38 KiB |
|
Before Width: | Height: | Size: 107 KiB After Width: | Height: | Size: 107 KiB |
|
Before Width: | Height: | Size: 42 KiB After Width: | Height: | Size: 43 KiB |
|
Before Width: | Height: | Size: 136 KiB After Width: | Height: | Size: 136 KiB |
|
Before Width: | Height: | Size: 46 KiB After Width: | Height: | Size: 47 KiB |
|
Before Width: | Height: | Size: 69 KiB After Width: | Height: | Size: 69 KiB |
|
Before Width: | Height: | Size: 37 KiB After Width: | Height: | Size: 37 KiB |
|
Before Width: | Height: | Size: 48 KiB After Width: | Height: | Size: 48 KiB |
|
Before Width: | Height: | Size: 75 KiB After Width: | Height: | Size: 76 KiB |
|
Before Width: | Height: | Size: 79 KiB After Width: | Height: | Size: 79 KiB |
|
Before Width: | Height: | Size: 64 KiB After Width: | Height: | Size: 65 KiB |
|
Before Width: | Height: | Size: 84 KiB After Width: | Height: | Size: 85 KiB |
|
Before Width: | Height: | Size: 59 KiB After Width: | Height: | Size: 59 KiB |
|
Before Width: | Height: | Size: 75 KiB After Width: | Height: | Size: 76 KiB |
|
Before Width: | Height: | Size: 59 KiB After Width: | Height: | Size: 60 KiB |
|
Before Width: | Height: | Size: 76 KiB After Width: | Height: | Size: 76 KiB |
|
Before Width: | Height: | Size: 58 KiB After Width: | Height: | Size: 59 KiB |
|
Before Width: | Height: | Size: 46 KiB After Width: | Height: | Size: 46 KiB |
|
Before Width: | Height: | Size: 43 KiB After Width: | Height: | Size: 43 KiB |
|
Before Width: | Height: | Size: 49 KiB After Width: | Height: | Size: 49 KiB |
|
Before Width: | Height: | Size: 69 KiB After Width: | Height: | Size: 69 KiB |
|
Before Width: | Height: | Size: 43 KiB After Width: | Height: | Size: 43 KiB |
|
Before Width: | Height: | Size: 73 KiB After Width: | Height: | Size: 74 KiB |
|
Before Width: | Height: | Size: 73 KiB After Width: | Height: | Size: 74 KiB |
|
Before Width: | Height: | Size: 109 KiB After Width: | Height: | Size: 110 KiB |
|
Before Width: | Height: | Size: 38 KiB After Width: | Height: | Size: 38 KiB |
|
Before Width: | Height: | Size: 42 KiB After Width: | Height: | Size: 43 KiB |
|
Before Width: | Height: | Size: 52 KiB After Width: | Height: | Size: 53 KiB |
|
Before Width: | Height: | Size: 125 KiB After Width: | Height: | Size: 125 KiB |
|
Before Width: | Height: | Size: 83 KiB After Width: | Height: | Size: 84 KiB |
|
Before Width: | Height: | Size: 63 KiB After Width: | Height: | Size: 64 KiB |
|
Before Width: | Height: | Size: 48 KiB After Width: | Height: | Size: 48 KiB |
|
Before Width: | Height: | Size: 21 KiB After Width: | Height: | Size: 21 KiB |
|
Before Width: | Height: | Size: 117 KiB After Width: | Height: | Size: 117 KiB |
|
Before Width: | Height: | Size: 192 KiB After Width: | Height: | Size: 192 KiB |
|
Before Width: | Height: | Size: 123 KiB After Width: | Height: | Size: 123 KiB |
|
Before Width: | Height: | Size: 58 KiB After Width: | Height: | Size: 58 KiB |
|
Before Width: | Height: | Size: 50 KiB After Width: | Height: | Size: 50 KiB |
|
Before Width: | Height: | Size: 58 KiB After Width: | Height: | Size: 59 KiB |
|
Before Width: | Height: | Size: 56 KiB After Width: | Height: | Size: 56 KiB |
|
Before Width: | Height: | Size: 58 KiB After Width: | Height: | Size: 58 KiB |
|
Before Width: | Height: | Size: 70 KiB After Width: | Height: | Size: 71 KiB |
|
Before Width: | Height: | Size: 18 KiB After Width: | Height: | Size: 18 KiB |
|
Before Width: | Height: | Size: 10 KiB After Width: | Height: | Size: 10 KiB |
|
Before Width: | Height: | Size: 9.7 KiB After Width: | Height: | Size: 10 KiB |
@@ -1,111 +0,0 @@
|
||||
import { Box, Text, chakra } from '@chakra-ui/react';
|
||||
import React, { useEffect } from 'react';
|
||||
|
||||
import { Image } from 'toolkit/chakra/image';
|
||||
import { Link } from 'toolkit/chakra/link';
|
||||
import { Skeleton } from 'toolkit/chakra/skeleton';
|
||||
import { ndash } from 'toolkit/utils/htmlEntities';
|
||||
import { isBrowser } from 'toolkit/utils/isBrowser';
|
||||
|
||||
type AdData = {
|
||||
ad: {
|
||||
name: string;
|
||||
description_short: string;
|
||||
thumbnail: string;
|
||||
url: string;
|
||||
cta_button: string;
|
||||
impressionUrl?: string;
|
||||
};
|
||||
};
|
||||
|
||||
// const MOCK: AdData = {
|
||||
// ad: {
|
||||
// url: 'https://unsplash.com/s/photos/cute-kitten',
|
||||
// thumbnail: 'https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/network-icons/gnosis.svg',
|
||||
// name: 'All about kitties',
|
||||
// description_short: 'To see millions picture of cute kitties',
|
||||
// cta_button: 'click here',
|
||||
// },
|
||||
// };
|
||||
|
||||
const CoinzillaTextAd = ({ className }: { className?: string }) => {
|
||||
const [ adData, setAdData ] = React.useState<AdData | null>(null);
|
||||
const [ isLoading, setIsLoading ] = React.useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
if (isBrowser()) {
|
||||
fetch('https://request-global.czilladx.com/serve/native.php?z=19260bf627546ab7242')
|
||||
.then(res => res.status === 200 ? res.json() : null)
|
||||
.then((_data) => {
|
||||
const data = _data as AdData;
|
||||
setAdData(data);
|
||||
if (data?.ad?.impressionUrl) {
|
||||
fetch(data.ad.impressionUrl);
|
||||
}
|
||||
})
|
||||
.finally(() => {
|
||||
// setAdData(MOCK);
|
||||
setIsLoading(false);
|
||||
});
|
||||
}
|
||||
}, [ ]);
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<Skeleton
|
||||
loading
|
||||
className={ className }
|
||||
h={{ base: 12, lg: 6 }}
|
||||
w="100%"
|
||||
flexGrow={ 1 }
|
||||
maxW="800px"
|
||||
display="block"
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (!adData) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const urlObject = new URL(adData.ad.url);
|
||||
|
||||
return (
|
||||
<Box
|
||||
className={ className }
|
||||
display={{ base: 'flex', lg: 'block' }}
|
||||
textStyle={{ base: 'xs', lg: 'md' }}
|
||||
color={{ base: 'text.secondary', lg: 'text.primary' }}
|
||||
>
|
||||
<Text
|
||||
as="span"
|
||||
whiteSpace="pre-wrap"
|
||||
fontWeight={ 700 }
|
||||
mr={ 3 }
|
||||
display={{ base: 'none', lg: 'inline' }}
|
||||
>
|
||||
Ads:
|
||||
</Text>
|
||||
{ urlObject.hostname === 'nifty.ink' ?
|
||||
<Text as="span" mr={ 1 }>🎨</Text> : (
|
||||
<Image
|
||||
src={ adData.ad.thumbnail }
|
||||
width="20px"
|
||||
height="20px"
|
||||
display="inline-block"
|
||||
verticalAlign="text-bottom"
|
||||
flexShrink={ 0 }
|
||||
alignSelf={{ base: 'flex-start', lg: 'center' }}
|
||||
mr={ 1 }
|
||||
alt=""
|
||||
/>
|
||||
) }
|
||||
<Box display={{ base: 'block', lg: 'inline' }}>
|
||||
<Text as="span" whiteSpace="pre-wrap">{ `${ adData.ad.name } ${ ndash } ${ adData.ad.description_short } ` }</Text>
|
||||
<Link href={ adData.ad.url } external noIcon>{ adData.ad.cta_button }</Link>
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export default chakra(CoinzillaTextAd);
|
||||
@@ -0,0 +1,126 @@
|
||||
import { Box, chakra } from '@chakra-ui/react';
|
||||
import Script from 'next/script';
|
||||
import React, { useCallback, useEffect, useRef } from 'react';
|
||||
|
||||
import config from 'configs/app';
|
||||
import { Skeleton } from 'toolkit/chakra/skeleton';
|
||||
|
||||
const adTextFeature = config.features.adsText;
|
||||
|
||||
const AD_LOAD_TIMEOUT = 1_000;
|
||||
|
||||
// Sevio native ad template (configured on the Sevio platform):
|
||||
// <img src="[%thumbnail%]" width="20" height="20" style="display: inline-block; vertical-align: text-bottom; margin-right: 4px;" alt="" />
|
||||
// <strong>Sponsored:</strong> [%sponsored%] – [%title%] <a href="[%clickURL%]" target="_blank">[%ctatext%]</a>
|
||||
|
||||
type Status = 'loading' | 'loaded' | 'empty';
|
||||
|
||||
const SevioTextAd = ({ className }: { className?: string }) => {
|
||||
const [ status, setStatus ] = React.useState<Status>('loading');
|
||||
const adRef = useRef<HTMLDivElement>(null);
|
||||
const timeoutRef = useRef<ReturnType<typeof setTimeout>>(undefined);
|
||||
|
||||
useEffect(() => {
|
||||
if (!adTextFeature.isEnabled) {
|
||||
return;
|
||||
}
|
||||
const { zone, adType, inventoryId, accountId } = adTextFeature.sevio;
|
||||
window.sevioads = window.sevioads || [];
|
||||
window.sevioads.push([ { zone, adType, inventoryId, accountId } ]);
|
||||
}, []);
|
||||
|
||||
// Watch for Sevio injecting content into the ad div.
|
||||
// When it does, cancel the timeout and mark the ad as loaded.
|
||||
useEffect(() => {
|
||||
const node = adRef.current;
|
||||
if (!node) {
|
||||
return;
|
||||
}
|
||||
|
||||
const observer = new MutationObserver(() => {
|
||||
if (node.childNodes.length > 0) {
|
||||
clearTimeout(timeoutRef.current);
|
||||
setStatus('loaded');
|
||||
observer.disconnect();
|
||||
}
|
||||
});
|
||||
|
||||
observer.observe(node, { childList: true });
|
||||
|
||||
return () => {
|
||||
observer.disconnect();
|
||||
};
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
clearTimeout(timeoutRef.current);
|
||||
};
|
||||
}, []);
|
||||
|
||||
// Once the Sevio script is loaded, give it a short window to inject the ad.
|
||||
// If nothing appears within that window, assume no ad was served and hide the skeleton.
|
||||
const handleScriptLoad = useCallback(() => {
|
||||
timeoutRef.current = setTimeout(() => {
|
||||
setStatus((prev) => prev === 'loading' ? 'empty' : prev);
|
||||
}, AD_LOAD_TIMEOUT);
|
||||
}, []);
|
||||
|
||||
// Script failed to load — no ad will ever appear, skip straight to empty.
|
||||
const handleScriptError = useCallback(() => {
|
||||
setStatus('empty');
|
||||
}, []);
|
||||
|
||||
if (!adTextFeature.isEnabled || status === 'empty') {
|
||||
return null;
|
||||
}
|
||||
|
||||
const { zone } = adTextFeature.sevio;
|
||||
|
||||
return (
|
||||
<>
|
||||
{ status === 'loading' && (
|
||||
<Skeleton
|
||||
loading
|
||||
className={ className }
|
||||
h={{ base: 12, lg: 6 }}
|
||||
w="100%"
|
||||
flexGrow={ 1 }
|
||||
maxW="800px"
|
||||
display="block"
|
||||
/>
|
||||
) }
|
||||
<Box
|
||||
className={ status === 'loaded' ? className : undefined }
|
||||
display={ status === 'loaded' ? undefined : 'none' }
|
||||
textStyle={{ base: 'xs', lg: 'md' }}
|
||||
color={{ base: 'text.secondary', lg: 'text.primary' }}
|
||||
css={{
|
||||
'& .sevioads *': {
|
||||
fontFamily: 'inherit',
|
||||
fontSize: 'inherit',
|
||||
},
|
||||
'& .sevioads strong': {
|
||||
color: 'inherit',
|
||||
},
|
||||
'& .sevioads a': {
|
||||
color: 'link.primary',
|
||||
_hover: {
|
||||
color: 'link.primary.hover',
|
||||
},
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Script
|
||||
strategy="lazyOnload"
|
||||
src="https://cdn.adx.ws/scripts/loader.js"
|
||||
onLoad={ handleScriptLoad }
|
||||
onError={ handleScriptError }
|
||||
/>
|
||||
<div ref={ adRef } className="sevioads" data-zone={ zone }/>
|
||||
</Box>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default chakra(SevioTextAd);
|
||||
@@ -5,7 +5,7 @@ import config from 'configs/app';
|
||||
import { useAppContext } from 'lib/contexts/app';
|
||||
import * as cookies from 'lib/cookies';
|
||||
|
||||
import CoinzillaTextAd from './CoinzillaTextAd';
|
||||
import SevioTextAd from './SevioTextAd';
|
||||
|
||||
const TextAd = ({ className }: { className?: string }) => {
|
||||
const hasAdblockCookie = cookies.get(cookies.NAMES.ADBLOCK_DETECTED, useAppContext().cookies);
|
||||
@@ -14,7 +14,7 @@ const TextAd = ({ className }: { className?: string }) => {
|
||||
return null;
|
||||
}
|
||||
|
||||
return <CoinzillaTextAd className={ className }/>;
|
||||
return <SevioTextAd className={ className }/>;
|
||||
};
|
||||
|
||||
export default chakra(TextAd);
|
||||
|
||||
|
Before Width: | Height: | Size: 32 KiB After Width: | Height: | Size: 32 KiB |
|
Before Width: | Height: | Size: 35 KiB After Width: | Height: | Size: 35 KiB |
|
Before Width: | Height: | Size: 36 KiB After Width: | Height: | Size: 36 KiB |
|
Before Width: | Height: | Size: 18 KiB After Width: | Height: | Size: 18 KiB |
|
Before Width: | Height: | Size: 34 KiB After Width: | Height: | Size: 34 KiB |
|
Before Width: | Height: | Size: 17 KiB After Width: | Height: | Size: 17 KiB |
|
Before Width: | Height: | Size: 36 KiB After Width: | Height: | Size: 36 KiB |
|
Before Width: | Height: | Size: 27 KiB After Width: | Height: | Size: 27 KiB |
|
Before Width: | Height: | Size: 42 KiB After Width: | Height: | Size: 42 KiB |
|
Before Width: | Height: | Size: 20 KiB After Width: | Height: | Size: 20 KiB |
|
Before Width: | Height: | Size: 21 KiB After Width: | Height: | Size: 21 KiB |
|
Before Width: | Height: | Size: 24 KiB After Width: | Height: | Size: 24 KiB |
|
Before Width: | Height: | Size: 37 KiB After Width: | Height: | Size: 37 KiB |
|
Before Width: | Height: | Size: 32 KiB After Width: | Height: | Size: 32 KiB |
|
Before Width: | Height: | Size: 18 KiB After Width: | Height: | Size: 18 KiB |
|
Before Width: | Height: | Size: 35 KiB After Width: | Height: | Size: 35 KiB |
|
Before Width: | Height: | Size: 36 KiB After Width: | Height: | Size: 36 KiB |
|
Before Width: | Height: | Size: 25 KiB After Width: | Height: | Size: 25 KiB |
|
Before Width: | Height: | Size: 19 KiB After Width: | Height: | Size: 19 KiB |
|
Before Width: | Height: | Size: 34 KiB After Width: | Height: | Size: 34 KiB |
|
Before Width: | Height: | Size: 22 KiB After Width: | Height: | Size: 22 KiB |
|
Before Width: | Height: | Size: 23 KiB After Width: | Height: | Size: 23 KiB |
|
Before Width: | Height: | Size: 37 KiB After Width: | Height: | Size: 37 KiB |
|
Before Width: | Height: | Size: 39 KiB After Width: | Height: | Size: 39 KiB |