21
common/resources/client/admin/ads/ad-host.css
vendored
Executable file
21
common/resources/client/admin/ads/ad-host.css
vendored
Executable file
@@ -0,0 +1,21 @@
|
||||
.ad-host:empty {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.ad-host > *:not(img) {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
max-width: 1200px;
|
||||
min-height: 90px;
|
||||
max-height: 600px;
|
||||
}
|
||||
|
||||
.ad-host img {
|
||||
margin: auto;
|
||||
}
|
||||
|
||||
@media only screen and (max-width: 600px) {
|
||||
.ad-host > *:not(img) {
|
||||
max-width: 370px;
|
||||
}
|
||||
}
|
||||
123
common/resources/client/admin/ads/ad-host.tsx
Executable file
123
common/resources/client/admin/ads/ad-host.tsx
Executable file
@@ -0,0 +1,123 @@
|
||||
import {useAuth} from '../../auth/use-auth';
|
||||
import {memo, useEffect, useId, useMemo, useRef} from 'react';
|
||||
import lazyLoader from '../../utils/http/lazy-loader';
|
||||
import clsx from 'clsx';
|
||||
import {useSettings} from '../../core/settings/use-settings';
|
||||
import dot from 'dot-object';
|
||||
import {Settings} from '@common/core/settings/settings';
|
||||
import {getScrollParent} from '@react-aria/utils';
|
||||
|
||||
interface AdHostProps {
|
||||
slot: keyof Omit<NonNullable<Settings['ads']>, 'disable'>;
|
||||
className?: string;
|
||||
}
|
||||
export function AdHost({slot, className}: AdHostProps) {
|
||||
const settings = useSettings();
|
||||
const {isSubscribed} = useAuth();
|
||||
const adCode = useMemo(() => {
|
||||
return dot.pick(`ads.${slot}`, settings);
|
||||
}, [slot, settings]);
|
||||
|
||||
if (settings.ads?.disable || isSubscribed || !adCode) return null;
|
||||
|
||||
return <InvariantAd className={className} slot={slot} adCode={adCode} />;
|
||||
}
|
||||
|
||||
interface InvariantAdProps {
|
||||
slot: string;
|
||||
adCode: string;
|
||||
className?: string;
|
||||
}
|
||||
const InvariantAd = memo(
|
||||
({slot, adCode, className}: InvariantAdProps) => {
|
||||
const ref = useRef<HTMLDivElement>(null);
|
||||
|
||||
const id = useId();
|
||||
|
||||
useEffect(() => {
|
||||
if (ref.current) {
|
||||
loadAdScripts(adCode, ref.current).then(() => {
|
||||
executeAdJavascript(adCode, id);
|
||||
});
|
||||
}
|
||||
return () => {
|
||||
// @ts-ignore
|
||||
delete window['google_ad_modifications'];
|
||||
};
|
||||
}, [adCode, id]);
|
||||
|
||||
// remove height modifications added by adsense
|
||||
useEffect(() => {
|
||||
if (ref.current) {
|
||||
const scrollParent = getScrollParent(ref.current) as HTMLElement;
|
||||
if (scrollParent) {
|
||||
const observer = new MutationObserver(function () {
|
||||
scrollParent.style.height = '';
|
||||
scrollParent.style.minHeight = '';
|
||||
});
|
||||
observer.observe(scrollParent, {
|
||||
attributes: true,
|
||||
attributeFilter: ['style'],
|
||||
});
|
||||
return () => observer.disconnect();
|
||||
}
|
||||
}
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={ref}
|
||||
id={id}
|
||||
className={clsx(
|
||||
'ad-host flex max-h-[600px] min-h-90 w-full max-w-full items-center justify-center overflow-hidden',
|
||||
`${slot.replace(/\./g, '-')}-host`,
|
||||
className,
|
||||
)}
|
||||
dangerouslySetInnerHTML={{__html: getAdHtml(adCode)}}
|
||||
></div>
|
||||
);
|
||||
},
|
||||
() => {
|
||||
// never re-render
|
||||
return false;
|
||||
},
|
||||
);
|
||||
|
||||
function getAdHtml(adCode: string) {
|
||||
// strip out all script tags from ad code and leave only html
|
||||
return adCode
|
||||
?.replace(/<script\b[^<]*(?:(?!<\/script>)<[^<]*)*<\/script>/gi, '')
|
||||
.trim();
|
||||
}
|
||||
|
||||
// Load any external scripts needed by ad.
|
||||
function loadAdScripts(adCode: string, parentEl: HTMLDivElement): Promise<any> {
|
||||
const promises = [];
|
||||
|
||||
// load ad code script
|
||||
const pattern = /<script.*?src=['"](.*?)['"]/g;
|
||||
let match;
|
||||
|
||||
while ((match = pattern.exec(adCode))) {
|
||||
if (match[1]) {
|
||||
promises.push(lazyLoader.loadAsset(match[1], {type: 'js', parentEl}));
|
||||
}
|
||||
}
|
||||
|
||||
return Promise.all(promises);
|
||||
}
|
||||
|
||||
// Execute ad code javascript and replace document.write if needed.
|
||||
function executeAdJavascript(adCode: string, id: string) {
|
||||
// find any ad code javascript that needs to be executed
|
||||
const pattern = /<script\b[^>]*>([\s\S]*?)<\/script>/g;
|
||||
let content;
|
||||
|
||||
while ((content = pattern.exec(adCode))) {
|
||||
if (content[1]) {
|
||||
const r = `var d = document.createElement('div'); d.innerHTML = $1; document.getElementById('${id}').appendChild(d.firstChild);`;
|
||||
const toEval = content[1].replace(/document.write\((.+?)\);/, r);
|
||||
eval(toEval);
|
||||
}
|
||||
}
|
||||
}
|
||||
115
common/resources/client/admin/ads/ads-page.tsx
Executable file
115
common/resources/client/admin/ads/ads-page.tsx
Executable file
@@ -0,0 +1,115 @@
|
||||
import {useContext} from 'react';
|
||||
import {
|
||||
AdConfig,
|
||||
SiteConfigContext,
|
||||
} from '../../core/settings/site-config-context';
|
||||
import {Form} from '../../ui/forms/form';
|
||||
import {useForm} from 'react-hook-form';
|
||||
import {FormTextField} from '../../ui/forms/input-field/text-field/text-field';
|
||||
import {Trans} from '../../i18n/trans';
|
||||
import {Button} from '../../ui/buttons/button';
|
||||
import {FormSwitch} from '../../ui/forms/toggle/switch';
|
||||
import {useAdminSettings} from '../settings/requests/use-admin-settings';
|
||||
import {ProgressCircle} from '../../ui/progress/progress-circle';
|
||||
import {Settings} from '../../core/settings/settings';
|
||||
import {
|
||||
AdminSettingsWithFiles,
|
||||
useUpdateAdminSettings,
|
||||
} from '../settings/requests/update-admin-settings';
|
||||
import {DialogTrigger} from '../../ui/overlays/dialog/dialog-trigger';
|
||||
import {ImageZoomDialog} from '../../ui/overlays/dialog/image-zoom-dialog';
|
||||
import {StaticPageTitle} from '../../seo/static-page-title';
|
||||
|
||||
export function AdsPage() {
|
||||
const query = useAdminSettings();
|
||||
|
||||
return (
|
||||
<div className="container mx-auto p-12 md:p-24">
|
||||
<StaticPageTitle>
|
||||
<Trans message="Ads" />
|
||||
</StaticPageTitle>
|
||||
<h1 className="mb-20 text-2xl font-light md:mb-40 md:text-3xl">
|
||||
<Trans message="Predefined Ad slots" />
|
||||
</h1>
|
||||
{query.isLoading ? (
|
||||
<ProgressCircle isIndeterminate />
|
||||
) : (
|
||||
<AdsForm defaultValues={query.data?.client.ads || {}} />
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
interface AdsFormProps {
|
||||
defaultValues: Settings['ads'];
|
||||
}
|
||||
function AdsForm({defaultValues}: AdsFormProps) {
|
||||
const {
|
||||
admin: {ads},
|
||||
} = useContext(SiteConfigContext);
|
||||
|
||||
const form = useForm<AdminSettingsWithFiles>({
|
||||
defaultValues: {client: {ads: defaultValues}},
|
||||
});
|
||||
const updateSettings = useUpdateAdminSettings(form);
|
||||
|
||||
return (
|
||||
<Form
|
||||
form={form}
|
||||
onSubmit={value => {
|
||||
updateSettings.mutate(value);
|
||||
}}
|
||||
>
|
||||
{ads.map(ad => {
|
||||
return <AdSection key={ad.slot} adConfig={ad} />;
|
||||
})}
|
||||
<FormSwitch
|
||||
name="client.ads.disable"
|
||||
className="mb-30"
|
||||
description={
|
||||
<Trans message="Disable all add related functionality across the site." />
|
||||
}
|
||||
>
|
||||
<Trans message="Disable ads" />
|
||||
</FormSwitch>
|
||||
<Button
|
||||
type="submit"
|
||||
variant="flat"
|
||||
color="primary"
|
||||
disabled={updateSettings.isPending}
|
||||
>
|
||||
<Trans message="Save" />
|
||||
</Button>
|
||||
</Form>
|
||||
);
|
||||
}
|
||||
|
||||
interface AdSectionProps {
|
||||
adConfig: AdConfig;
|
||||
}
|
||||
function AdSection({adConfig}: AdSectionProps) {
|
||||
return (
|
||||
<div className="flex items-center gap-24">
|
||||
<FormTextField
|
||||
className="mb-30 flex-auto"
|
||||
name={`client.${adConfig.slot}`}
|
||||
inputElementType="textarea"
|
||||
rows={8}
|
||||
label={<Trans {...adConfig.description} />}
|
||||
/>
|
||||
<DialogTrigger type="modal">
|
||||
<button
|
||||
type="button"
|
||||
className="cursor-zoom-in overflow-hidden rounded outline-none transition hover:scale-105 focus-visible:ring max-md:hidden"
|
||||
>
|
||||
<img
|
||||
src={adConfig.image}
|
||||
className="h-[186px] w-auto border"
|
||||
alt="Ad slot example"
|
||||
/>
|
||||
</button>
|
||||
<ImageZoomDialog image={adConfig.image} />
|
||||
</DialogTrigger>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user