diff --git a/lib/sponsors.interfaces.ts b/lib/sponsors.interfaces.ts new file mode 100644 index 000000000..2b0a01949 --- /dev/null +++ b/lib/sponsors.interfaces.ts @@ -0,0 +1,49 @@ +// Copyright (c) 2022, Compiler Explorer Authors +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are met: +// +// * Redistributions of source code must retain the above copyright notice, +// this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright +// notice, this list of conditions and the following disclaimer in the +// documentation and/or other materials provided with the distribution. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +// ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE +// LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +// CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +// SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +// CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +// ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +// POSSIBILITY OF SUCH DAMAGE. + +export type Sponsor = { + name: string; + description?: string; + img?: string; + icon?: string; + icon_dark?: string; + url?: string; + onclick: string; + priority: number; + topIcon: boolean; + sideBySide: boolean; + statsId?: string; +}; + +export type Level = { + name: string; + description: string; + class?: string; + sponsors: Sponsor[]; +}; + +export type Sponsors = { + icons: Sponsor[]; + levels: Level[]; +}; diff --git a/lib/sponsors.js b/lib/sponsors.ts similarity index 63% rename from lib/sponsors.js rename to lib/sponsors.ts index a81c453dc..80dbf7a19 100644 --- a/lib/sponsors.js +++ b/lib/sponsors.ts @@ -24,40 +24,39 @@ import yaml from 'yaml'; -function namify(mapOrString) { - if (typeof mapOrString == 'string') return {name: mapOrString}; - return mapOrString; +import {Sponsor, Sponsors} from './sponsors.interfaces'; + +export function parse(mapOrString: Record | string): Sponsor { + if (typeof mapOrString == 'string') mapOrString = {name: mapOrString}; + return { + name: mapOrString.name, + description: mapOrString.description, + url: mapOrString.url, + onclick: mapOrString.url ? `window.onSponsorClick(${JSON.stringify(mapOrString.url)});` : '', + img: mapOrString.img, + icon: mapOrString.icon || mapOrString.img, + icon_dark: mapOrString.icon_dark, + topIcon: !!mapOrString.topIcon, + sideBySide: !!mapOrString.sideBySide, + priority: mapOrString.priority || 0, + statsId: mapOrString.statsId, + }; } -function clickify(sponsor) { - if (sponsor.url) { - sponsor.onclick = `window.onSponsorClick(${JSON.stringify(sponsor)});`; - } - return sponsor; -} - -function getIconUrl(sponsor) { - const icon = sponsor.icon || sponsor.img; - if (icon) { - sponsor.icon = icon; - } - return sponsor; -} - -function compareSponsors(lhs, rhs) { - const lhsPrio = lhs.priority || 0; - const rhsPrio = rhs.priority || 0; +function compareSponsors(lhs: Sponsor, rhs: Sponsor): number { + const lhsPrio = lhs.priority; + const rhsPrio = rhs.priority; if (lhsPrio !== rhsPrio) return rhsPrio - lhsPrio; return lhs.name.localeCompare(rhs.name); } -export function loadSponsorsFromString(stringConfig) { +export function loadSponsorsFromString(stringConfig: string): Sponsors { const sponsorConfig = yaml.parse(stringConfig); sponsorConfig.icons = []; for (const level of sponsorConfig.levels) { - for (const required of ['name', 'description']) + for (const required of ['name', 'description', 'sponsors']) if (!level[required]) throw new Error(`Level is missing '${required}'`); - level.sponsors = level.sponsors.map(namify).map(clickify).map(getIconUrl).sort(compareSponsors); + level.sponsors = level.sponsors.map(parse).sort(compareSponsors); sponsorConfig.icons.push(...level.sponsors.filter(sponsor => sponsor.topIcon && sponsor.icon)); } return sponsorConfig; diff --git a/static/main.js b/static/main.js index 4c02fa8d6..836cb410e 100644 --- a/static/main.js +++ b/static/main.js @@ -672,15 +672,15 @@ function start() { $('[name="editor-btn-toolbar"]').addClass('d-none'); } - window.onSponsorClick = function (sponsor) { + window.onSponsorClick = function (sponsorUrl) { analytics.proxy('send', { hitType: 'event', eventCategory: 'Sponsors', eventAction: 'click', - eventLabel: sponsor.url, + eventLabel: sponsorUrl, transport: 'beacon', }); - window.open(sponsor.url); + window.open(sponsorUrl); }; if (options.pageloadUrl) { diff --git a/test/sponsors-test.js b/test/sponsors-test.js index 082b8444e..6bf29e7ef 100644 --- a/test/sponsors-test.js +++ b/test/sponsors-test.js @@ -22,9 +22,54 @@ // ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE // POSSIBILITY OF SUCH DAMAGE. -import {loadSponsorsFromString} from '../lib/sponsors'; +import {loadSponsorsFromString, parse} from '../lib/sponsors'; + +import {should} from './utils'; describe('Sponsors', () => { + it('should expand names to objects', () => { + parse('moo').name.should.eq('moo'); + }); + it('should handle just names', () => { + parse({name: 'moo'}).name.should.eq('moo'); + }); + it('should default empty params', () => { + const obj = parse('moo'); + should.equal(obj.description, undefined); + should.equal(obj.url, undefined); + obj.onclick.should.eq(''); + should.equal(obj.img, undefined); + should.equal(obj.icon, undefined); + should.equal(obj.icon_dark, undefined); + obj.topIcon.should.be.false; + obj.sideBySide.should.be.false; + should.equal(obj.statsId, undefined); + }); + it('should pass through descriptions', () => { + parse({name: 'moo', description: 'desc'}).description.should.eq('desc'); + }); + it('should pass through icons', () => { + parse({name: 'bob', icon: 'icon'}).icon.should.eq('icon'); + }); + it('should pick icons over images', () => { + parse({name: 'bob', img: 'img', icon: 'icon'}).icon.should.eq('icon'); + }); + it('should pick icons if not img', () => { + parse({name: 'bob', img: 'img'}).icon.should.eq('img'); + }); + it('should pick dark icons if specified', () => { + parse({name: 'bob', icon: 'icon', icon_dark: 'icon_dark'}).icon_dark.should.eq('icon_dark'); + }); + it('should handle topIcons', () => { + parse({name: 'bob', topIcon: true}).topIcon.should.be.true; + }); + it('should handle clicks', () => { + parse({ + name: 'bob', + url: 'https://some.host/click', + }).onclick.should.eq('window.onSponsorClick("https://some.host/click");'); + }); + it('should load a simple example', () => { const sample = loadSponsorsFromString(` --- @@ -49,19 +94,6 @@ levels: sample.levels[1].name.should.eq('Patreons'); }); - it('should expand names to objects', () => { - const folks = loadSponsorsFromString(` ---- -levels: - - name: a - description: d - sponsors: - - Just a string - - name: An object - `).levels[0].sponsors; - folks.should.deep.equalInAnyOrder([{name: 'An object'}, {name: 'Just a string'}]); - }); - it('should sort sponsors by name', () => { const peeps = loadSponsorsFromString(` --- @@ -74,7 +106,7 @@ levels: - A - B `).levels[0].sponsors; - peeps.should.deep.equals([{name: 'A'}, {name: 'B'}, {name: 'C'}, {name: 'D'}]); + peeps.map(sponsor => sponsor.name).should.deep.equals(['A', 'B', 'C', 'D']); }); it('should sort sponsors by priority then name', () => { const peeps = loadSponsorsFromString(` @@ -90,44 +122,15 @@ levels: - name: B priority: 50 `).levels[0].sponsors; - peeps.should.deep.equals([ - {name: 'D', priority: 100}, - {name: 'B', priority: 50}, - {name: 'C', priority: 50}, - ]); - }); - it('should pick icon over img', () => { - const things = loadSponsorsFromString(` ---- -levels: - - name: a - description: d - sponsors: - - name: one - img: image - - name: two - img: not_an_icon - icon: icon - `).levels[0].sponsors; - things.should.deep.equalInAnyOrder([ - {name: 'one', icon: 'image', img: 'image'}, - {name: 'two', icon: 'icon', img: 'not_an_icon'}, - ]); - }); - - it('should pick up dark icon if set', () => { - const things = loadSponsorsFromString(` ---- -levels: - - name: a - description: d - sponsors: - - name: batman - img: not_an_icon - icon: icon - icon_dark: dark - `).levels[0].sponsors; - things.should.deep.equalInAnyOrder([{name: 'batman', icon: 'icon', icon_dark: 'dark', img: 'not_an_icon'}]); + peeps + .map(sponsor => { + return {name: sponsor.name, priority: sponsor.priority}; + }) + .should.deep.equals([ + {name: 'D', priority: 100}, + {name: 'B', priority: 50}, + {name: 'C', priority: 50}, + ]); }); it('should pick out the top level icons', () => { @@ -154,9 +157,6 @@ levels: - name: five topIcon: true `).icons; - icons.should.deep.equalInAnyOrder([ - {name: 'one', icon: 'pick_me', img: 'pick_me', topIcon: true}, - {name: 'four', icon: 'pick_me_also', img: 'pick_me_also', topIcon: true}, - ]); + icons.map(s => s.name).should.deep.equals(['one', 'four']); }); });