mirror of
https://github.com/compiler-explorer/compiler-explorer.git
synced 2025-12-27 10:33:59 -05:00
Sponsorship changes (#4274)
Rotate sponsors on the top of the page: - `topIcon` -> `topIconShowEvery` - `Sponsors` becomes an interface with methods to get levels, pick sponsors etc - A somewhat acceptable algorithm for generating a "fair choice" of sponsors given the constraints: - "show at least 1 in N times" - don't unfairly show one sponsor more than any other at the same level - Sponsor icons are loaded dynamically via a "bits" handler (introducing a short delay in them appearing), but this means the index.html doesn't change on every load, so it's still cacheable
This commit is contained in:
@@ -1,4 +1,5 @@
|
|||||||
---
|
---
|
||||||
|
maxTopIcons: 3
|
||||||
levels:
|
levels:
|
||||||
- name: Corporate Sponsors
|
- name: Corporate Sponsors
|
||||||
description: 'Enormous thanks to our corporate sponsors. Please visit their websites below:'
|
description: 'Enormous thanks to our corporate sponsors. Please visit their websites below:'
|
||||||
@@ -9,7 +10,7 @@ levels:
|
|||||||
img: https://static.ce-cdn.net/SOLID+SANDS-LOGO-RGB-500px.png
|
img: https://static.ce-cdn.net/SOLID+SANDS-LOGO-RGB-500px.png
|
||||||
url: https://solidsands.com/
|
url: https://solidsands.com/
|
||||||
priority: 100
|
priority: 100
|
||||||
topIcon: true
|
topIconShowEvery: 3
|
||||||
statsId: solid_sands
|
statsId: solid_sands
|
||||||
- name: Intel
|
- name: Intel
|
||||||
description: We engineer solutions for our customers’ greatest challenges with reliable, cloud to edge computing, inspired by Moore’s Law.
|
description: We engineer solutions for our customers’ greatest challenges with reliable, cloud to edge computing, inspired by Moore’s Law.
|
||||||
@@ -17,7 +18,7 @@ levels:
|
|||||||
img: https://static.ce-cdn.net/intel/logo-classicblue-3000px.png
|
img: https://static.ce-cdn.net/intel/logo-classicblue-3000px.png
|
||||||
url: https://intel.com/
|
url: https://intel.com/
|
||||||
priority: 400
|
priority: 400
|
||||||
topIcon: true
|
topIconShowEvery: 1
|
||||||
sideBySide: true
|
sideBySide: true
|
||||||
statsId: intel
|
statsId: intel
|
||||||
- name: Backtrace
|
- name: Backtrace
|
||||||
@@ -29,7 +30,7 @@ levels:
|
|||||||
img: https://static.ce-cdn.net/bt/BT-logo (1).png
|
img: https://static.ce-cdn.net/bt/BT-logo (1).png
|
||||||
url: https://backtrace.io/sign-up?utm_source=website&utm_medium=paiddisplay&utm_campaign=00O8Y000008JiuiUAC&utm_content=godboltbannerad
|
url: https://backtrace.io/sign-up?utm_source=website&utm_medium=paiddisplay&utm_campaign=00O8Y000008JiuiUAC&utm_content=godboltbannerad
|
||||||
priority: 500
|
priority: 500
|
||||||
topIcon: true
|
topIconShowEvery: 3
|
||||||
sideBySide: true
|
sideBySide: true
|
||||||
statsId: backtrace
|
statsId: backtrace
|
||||||
- name: Patreon Legends
|
- name: Patreon Legends
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ export type Sponsor = {
|
|||||||
url?: string;
|
url?: string;
|
||||||
onclick: string;
|
onclick: string;
|
||||||
priority: number;
|
priority: number;
|
||||||
topIcon: boolean;
|
topIconShowEvery: number;
|
||||||
sideBySide: boolean;
|
sideBySide: boolean;
|
||||||
statsId?: string;
|
statsId?: string;
|
||||||
};
|
};
|
||||||
@@ -43,7 +43,10 @@ export type Level = {
|
|||||||
sponsors: Sponsor[];
|
sponsors: Sponsor[];
|
||||||
};
|
};
|
||||||
|
|
||||||
export type Sponsors = {
|
export interface Sponsors {
|
||||||
icons: Sponsor[];
|
getLevels(): Level[];
|
||||||
levels: Level[];
|
|
||||||
};
|
pickTopIcons(): Sponsor[];
|
||||||
|
|
||||||
|
getAllTopIcons(): Sponsor[];
|
||||||
|
}
|
||||||
|
|||||||
109
lib/sponsors.ts
109
lib/sponsors.ts
@@ -24,7 +24,7 @@
|
|||||||
|
|
||||||
import yaml from 'yaml';
|
import yaml from 'yaml';
|
||||||
|
|
||||||
import {Sponsor, Sponsors} from './sponsors.interfaces';
|
import {Level, Sponsor, Sponsors} from './sponsors.interfaces';
|
||||||
|
|
||||||
export function parse(mapOrString: Record<string, any> | string): Sponsor {
|
export function parse(mapOrString: Record<string, any> | string): Sponsor {
|
||||||
if (typeof mapOrString == 'string') mapOrString = {name: mapOrString};
|
if (typeof mapOrString == 'string') mapOrString = {name: mapOrString};
|
||||||
@@ -36,7 +36,7 @@ export function parse(mapOrString: Record<string, any> | string): Sponsor {
|
|||||||
img: mapOrString.img,
|
img: mapOrString.img,
|
||||||
icon: mapOrString.icon || mapOrString.img,
|
icon: mapOrString.icon || mapOrString.img,
|
||||||
icon_dark: mapOrString.icon_dark,
|
icon_dark: mapOrString.icon_dark,
|
||||||
topIcon: !!mapOrString.topIcon,
|
topIconShowEvery: mapOrString.topIconShowEvery || 0,
|
||||||
sideBySide: !!mapOrString.sideBySide,
|
sideBySide: !!mapOrString.sideBySide,
|
||||||
priority: mapOrString.priority || 0,
|
priority: mapOrString.priority || 0,
|
||||||
statsId: mapOrString.statsId,
|
statsId: mapOrString.statsId,
|
||||||
@@ -50,14 +50,113 @@ function compareSponsors(lhs: Sponsor, rhs: Sponsor): number {
|
|||||||
return lhs.name.localeCompare(rhs.name);
|
return lhs.name.localeCompare(rhs.name);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function calcMean(values: number[]): number {
|
||||||
|
return values.reduce((x, y) => x + y, 0) / values.length;
|
||||||
|
}
|
||||||
|
|
||||||
|
function squareSumFromMean(values: number[]): number {
|
||||||
|
const mean = calcMean(values);
|
||||||
|
return values.reduce((x, y) => x + (y - mean) * (y - mean), 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
function standardDeviation(values: number[]): number {
|
||||||
|
return values.length < 2 ? 0 : Math.sqrt(squareSumFromMean(values) / (values.length - 1));
|
||||||
|
}
|
||||||
|
|
||||||
|
// A sponsor icon set is ok if:
|
||||||
|
// - each sponsor is shown at least every "topIconShowEvery"
|
||||||
|
// - the standard deviation for the number of showings between sponsors at the same "show every' is not too high: that
|
||||||
|
// is we fairly distribute showings of sponsors at the same level
|
||||||
|
function sponsorIconSetsOk(
|
||||||
|
sponsorAppearanceCount: Map<Sponsor, number>,
|
||||||
|
totalAppearances: number,
|
||||||
|
maxStandardDeviation: number,
|
||||||
|
): boolean {
|
||||||
|
const countsByShowEvery: Map<number, number[]> = new Map();
|
||||||
|
for (const [icon, count] of sponsorAppearanceCount.entries()) {
|
||||||
|
const seenEvery = count > 0 ? totalAppearances / count : Infinity;
|
||||||
|
if (seenEvery > icon.topIconShowEvery) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const others = countsByShowEvery.get(icon.topIconShowEvery) || [];
|
||||||
|
others.push(seenEvery);
|
||||||
|
countsByShowEvery.set(icon.topIconShowEvery, others);
|
||||||
|
}
|
||||||
|
return Math.max(...[...countsByShowEvery.values()].map(standardDeviation)) <= maxStandardDeviation;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function makeIconSets(
|
||||||
|
icons: Sponsor[],
|
||||||
|
maxIcons: number,
|
||||||
|
maxIters = 100,
|
||||||
|
maxStandardDeviation = 0.5,
|
||||||
|
): Sponsor[][] {
|
||||||
|
const result: Sponsor[][] = [];
|
||||||
|
const sponsorAppearanceCount: Map<Sponsor, number> = new Map();
|
||||||
|
for (const icon of icons) sponsorAppearanceCount.set(icon, 0);
|
||||||
|
while (!sponsorIconSetsOk(sponsorAppearanceCount, result.length, maxStandardDeviation)) {
|
||||||
|
if (result.length > maxIters) {
|
||||||
|
throw new Error(`Unable to find a solution in ${maxIters}`);
|
||||||
|
}
|
||||||
|
const toPick = icons.map(icon => {
|
||||||
|
return {
|
||||||
|
icon: icon,
|
||||||
|
// Number of times we'd expect to see this, divided by number of times we saw it
|
||||||
|
error: result.length / icon.topIconShowEvery / (sponsorAppearanceCount.get(icon) || 0.00001),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
toPick.sort((lhs, rhs) => rhs.error - lhs.error);
|
||||||
|
const chosen = toPick
|
||||||
|
.slice(0, maxIcons)
|
||||||
|
.map(x => x.icon)
|
||||||
|
.sort(compareSponsors);
|
||||||
|
for (const c of chosen) sponsorAppearanceCount.set(c, (sponsorAppearanceCount.get(c) || 0) + 1);
|
||||||
|
result.push(chosen);
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
class SponsorsImpl implements Sponsors {
|
||||||
|
private readonly _levels: Level[];
|
||||||
|
private readonly _icons: Sponsor[];
|
||||||
|
private readonly _iconSets: Sponsor[][];
|
||||||
|
private _nextSet: number;
|
||||||
|
|
||||||
|
constructor(levels: Level[], maxTopIcons) {
|
||||||
|
this._levels = levels;
|
||||||
|
this._icons = [];
|
||||||
|
for (const level of levels) {
|
||||||
|
this._icons.push(...level.sponsors.filter(sponsor => sponsor.topIconShowEvery && sponsor.icon));
|
||||||
|
}
|
||||||
|
this._iconSets = makeIconSets(this._icons, maxTopIcons);
|
||||||
|
this._nextSet = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
getLevels(): Level[] {
|
||||||
|
return this._levels;
|
||||||
|
}
|
||||||
|
|
||||||
|
getAllTopIcons(): Sponsor[] {
|
||||||
|
return this._icons;
|
||||||
|
}
|
||||||
|
|
||||||
|
pickTopIcons(): Sponsor[] {
|
||||||
|
const result = this._iconSets[this._nextSet];
|
||||||
|
this._nextSet = (this._nextSet + 1) % this._iconSets.length;
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function loadSponsorsFromLevels(levels: Level[], maxTopIcons: number): Sponsors {
|
||||||
|
return new SponsorsImpl(levels, maxTopIcons);
|
||||||
|
}
|
||||||
|
|
||||||
export function loadSponsorsFromString(stringConfig: string): Sponsors {
|
export function loadSponsorsFromString(stringConfig: string): Sponsors {
|
||||||
const sponsorConfig = yaml.parse(stringConfig);
|
const sponsorConfig = yaml.parse(stringConfig);
|
||||||
sponsorConfig.icons = [];
|
|
||||||
for (const level of sponsorConfig.levels) {
|
for (const level of sponsorConfig.levels) {
|
||||||
for (const required of ['name', 'description', 'sponsors'])
|
for (const required of ['name', 'description', 'sponsors'])
|
||||||
if (!level[required]) throw new Error(`Level is missing '${required}'`);
|
if (!level[required]) throw new Error(`Level is missing '${required}'`);
|
||||||
level.sponsors = level.sponsors.map(parse).sort(compareSponsors);
|
level.sponsors = level.sponsors.map(parse).sort(compareSponsors);
|
||||||
sponsorConfig.icons.push(...level.sponsors.filter(sponsor => sponsor.topIcon && sponsor.icon));
|
|
||||||
}
|
}
|
||||||
return sponsorConfig;
|
return loadSponsorsFromLevels(sponsorConfig.levels, sponsorConfig.maxTopIcons || 3);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -196,6 +196,14 @@ function setupButtons(options, hub) {
|
|||||||
alertSystem.alert('Changelog', $(require('./generated/changelog.pug').default.text));
|
alertSystem.alert('Changelog', $(require('./generated/changelog.pug').default.text));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
$.get(window.location.origin + window.httpRoot + 'bits/icons.html')
|
||||||
|
.done(function (data) {
|
||||||
|
$('#ces .ces-icons').html(data);
|
||||||
|
})
|
||||||
|
.fail(function (err) {
|
||||||
|
Sentry.captureException(err);
|
||||||
|
});
|
||||||
|
|
||||||
$('#ces').on('click', function () {
|
$('#ces').on('click', function () {
|
||||||
$.get(window.location.origin + window.httpRoot + 'bits/sponsors.html')
|
$.get(window.location.origin + window.httpRoot + 'bits/sponsors.html')
|
||||||
.done(function (data) {
|
.done(function (data) {
|
||||||
|
|||||||
@@ -22,9 +22,11 @@
|
|||||||
// ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
|
// ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
|
||||||
// POSSIBILITY OF SUCH DAMAGE.
|
// POSSIBILITY OF SUCH DAMAGE.
|
||||||
|
|
||||||
import {loadSponsorsFromString, parse} from '../lib/sponsors';
|
import fs from 'fs';
|
||||||
|
|
||||||
import {should} from './utils';
|
import {loadSponsorsFromString, makeIconSets, parse} from '../lib/sponsors';
|
||||||
|
|
||||||
|
import {resolvePathFromTestRoot, should} from './utils';
|
||||||
|
|
||||||
describe('Sponsors', () => {
|
describe('Sponsors', () => {
|
||||||
it('should expand names to objects', () => {
|
it('should expand names to objects', () => {
|
||||||
@@ -41,7 +43,7 @@ describe('Sponsors', () => {
|
|||||||
should.equal(obj.img, undefined);
|
should.equal(obj.img, undefined);
|
||||||
should.equal(obj.icon, undefined);
|
should.equal(obj.icon, undefined);
|
||||||
should.equal(obj.icon_dark, undefined);
|
should.equal(obj.icon_dark, undefined);
|
||||||
obj.topIcon.should.be.false;
|
obj.topIconShowEvery.should.eq(0);
|
||||||
obj.sideBySide.should.be.false;
|
obj.sideBySide.should.be.false;
|
||||||
should.equal(obj.statsId, undefined);
|
should.equal(obj.statsId, undefined);
|
||||||
});
|
});
|
||||||
@@ -64,7 +66,7 @@ describe('Sponsors', () => {
|
|||||||
parse({name: 'bob', icon: 'icon', icon_dark: 'icon_dark'}).icon_dark.should.eq('icon_dark');
|
parse({name: 'bob', icon: 'icon', icon_dark: 'icon_dark'}).icon_dark.should.eq('icon_dark');
|
||||||
});
|
});
|
||||||
it('should handle topIcons', () => {
|
it('should handle topIcons', () => {
|
||||||
parse({name: 'bob', topIcon: true}).topIcon.should.be.true;
|
parse({name: 'bob', topIconShowEvery: 2}).topIconShowEvery.should.eq(2);
|
||||||
});
|
});
|
||||||
it('should handle clicks', () => {
|
it('should handle clicks', () => {
|
||||||
parse({
|
parse({
|
||||||
@@ -92,9 +94,10 @@ levels:
|
|||||||
- Yay
|
- Yay
|
||||||
`);
|
`);
|
||||||
sample.should.not.be.null;
|
sample.should.not.be.null;
|
||||||
sample.levels.length.should.eq(2);
|
const levels = sample.getLevels();
|
||||||
sample.levels[0].name.should.eq('Patreon Legends');
|
levels.length.should.eq(2);
|
||||||
sample.levels[1].name.should.eq('Patreons');
|
levels[0].name.should.eq('Patreon Legends');
|
||||||
|
levels[1].name.should.eq('Patreons');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should sort sponsors by name', () => {
|
it('should sort sponsors by name', () => {
|
||||||
@@ -108,7 +111,7 @@ levels:
|
|||||||
- C
|
- C
|
||||||
- A
|
- A
|
||||||
- B
|
- B
|
||||||
`).levels[0].sponsors;
|
`).getLevels()[0].sponsors;
|
||||||
peeps.map(sponsor => sponsor.name).should.deep.equals(['A', 'B', 'C', 'D']);
|
peeps.map(sponsor => sponsor.name).should.deep.equals(['A', 'B', 'C', 'D']);
|
||||||
});
|
});
|
||||||
it('should sort sponsors by priority then name', () => {
|
it('should sort sponsors by priority then name', () => {
|
||||||
@@ -124,7 +127,7 @@ levels:
|
|||||||
priority: 50
|
priority: 50
|
||||||
- name: B
|
- name: B
|
||||||
priority: 50
|
priority: 50
|
||||||
`).levels[0].sponsors;
|
`).getLevels()[0].sponsors;
|
||||||
peeps
|
peeps
|
||||||
.map(sponsor => {
|
.map(sponsor => {
|
||||||
return {name: sponsor.name, priority: sponsor.priority};
|
return {name: sponsor.name, priority: sponsor.priority};
|
||||||
@@ -136,7 +139,7 @@ levels:
|
|||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should pick out the top level icons', () => {
|
it('should pick out all the top level icons', () => {
|
||||||
const icons = loadSponsorsFromString(`
|
const icons = loadSponsorsFromString(`
|
||||||
---
|
---
|
||||||
levels:
|
levels:
|
||||||
@@ -145,7 +148,7 @@ levels:
|
|||||||
sponsors:
|
sponsors:
|
||||||
- name: one
|
- name: one
|
||||||
img: pick_me
|
img: pick_me
|
||||||
topIcon: true
|
topIconShowEvery: 1
|
||||||
- name: two
|
- name: two
|
||||||
img: not_me
|
img: not_me
|
||||||
- name: another level
|
- name: another level
|
||||||
@@ -153,13 +156,96 @@ levels:
|
|||||||
sponsors:
|
sponsors:
|
||||||
- name: three
|
- name: three
|
||||||
img: not_me_either
|
img: not_me_either
|
||||||
topIcon: false
|
topIconShowEvery: 0
|
||||||
- name: four
|
- name: four
|
||||||
img: pick_me_also
|
img: pick_me_also
|
||||||
topIcon: true
|
topIconShowEvery: 2
|
||||||
- name: five
|
- name: five
|
||||||
topIcon: true
|
topIconShowEvery: 3
|
||||||
`).icons;
|
`).getAllTopIcons();
|
||||||
icons.map(s => s.name).should.deep.equals(['one', 'four']);
|
icons.map(s => s.name).should.deep.equals(['one', 'four']);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should pick icons appropriately when all required every 3', () => {
|
||||||
|
const sponsor1 = parse({name: 'Sponsor1', topIconShowEvery: 3, icon: '1'});
|
||||||
|
const sponsor2 = parse({name: 'Sponsor2', topIconShowEvery: 3, icon: '2'});
|
||||||
|
const sponsor3 = parse({name: 'Sponsor3', topIconShowEvery: 3, icon: '3'});
|
||||||
|
const icons = [sponsor1, sponsor2, sponsor3];
|
||||||
|
makeIconSets(icons, 10).should.deep.eq([icons]);
|
||||||
|
makeIconSets(icons, 3).should.deep.eq([icons]);
|
||||||
|
makeIconSets(icons, 2).should.deep.eq([
|
||||||
|
[sponsor1, sponsor2],
|
||||||
|
[sponsor1, sponsor3],
|
||||||
|
[sponsor2, sponsor3],
|
||||||
|
]);
|
||||||
|
makeIconSets(icons, 1).should.deep.eq([[sponsor1], [sponsor2], [sponsor3]]);
|
||||||
|
});
|
||||||
|
it('should pick icons appropriately when not required on different schedules', () => {
|
||||||
|
const sponsor1 = parse({name: 'Sponsor1', topIconShowEvery: 1, icon: '1'});
|
||||||
|
const sponsor2 = parse({name: 'Sponsor2', topIconShowEvery: 2, icon: '2'});
|
||||||
|
const sponsor3 = parse({name: 'Sponsor3', topIconShowEvery: 3, icon: '3'});
|
||||||
|
const icons = [sponsor1, sponsor2, sponsor3];
|
||||||
|
makeIconSets(icons, 10).should.deep.eq([icons]);
|
||||||
|
makeIconSets(icons, 3).should.deep.eq([icons]);
|
||||||
|
makeIconSets(icons, 2).should.deep.eq([
|
||||||
|
[sponsor1, sponsor2],
|
||||||
|
[sponsor1, sponsor3],
|
||||||
|
]);
|
||||||
|
(() => makeIconSets(icons, 1)).should.throw();
|
||||||
|
});
|
||||||
|
it('should pick icons appropriately with a lot of sponsors on representative schedules', () => {
|
||||||
|
const sponsor1 = parse({name: 'Sponsor1', topIconShowEvery: 1, icon: '1'});
|
||||||
|
const sponsor2 = parse({name: 'Sponsor2', topIconShowEvery: 3, icon: '2'});
|
||||||
|
const sponsor3 = parse({name: 'Sponsor3', topIconShowEvery: 3, icon: '3'});
|
||||||
|
const sponsor4 = parse({name: 'Sponsor4', topIconShowEvery: 3, icon: '3'});
|
||||||
|
const sponsor5 = parse({name: 'Sponsor5', topIconShowEvery: 3, icon: '3'});
|
||||||
|
const icons = [sponsor1, sponsor2, sponsor3, sponsor4, sponsor5];
|
||||||
|
makeIconSets(icons, 10).should.deep.eq([icons]);
|
||||||
|
makeIconSets(icons, 3).should.deep.eq([
|
||||||
|
[sponsor1, sponsor2, sponsor3],
|
||||||
|
[sponsor1, sponsor4, sponsor5],
|
||||||
|
]);
|
||||||
|
(() => makeIconSets(icons, 1)).should.throw();
|
||||||
|
});
|
||||||
|
it('should handle alternating', () => {
|
||||||
|
const sponsor1 = parse({name: 'Sponsor1', topIconShowEvery: 1, icon: '1'});
|
||||||
|
const sponsor2 = parse({name: 'Sponsor2', topIconShowEvery: 1, icon: '2'});
|
||||||
|
const sponsor3 = parse({name: 'Sponsor3', topIconShowEvery: 2, icon: '3'});
|
||||||
|
const sponsor4 = parse({name: 'Sponsor4', topIconShowEvery: 2, icon: '4'});
|
||||||
|
const icons = [sponsor1, sponsor2, sponsor3, sponsor4];
|
||||||
|
makeIconSets(icons, 4).should.deep.eq([icons]);
|
||||||
|
makeIconSets(icons, 3).should.deep.eq([
|
||||||
|
[sponsor1, sponsor2, sponsor3],
|
||||||
|
[sponsor1, sponsor2, sponsor4],
|
||||||
|
]);
|
||||||
|
(() => makeIconSets(icons, 2)).should.throw();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Our specific sponsor file', () => {
|
||||||
|
const stringConfig = fs.readFileSync(resolvePathFromTestRoot('../etc/config/sponsors.yaml')).toString();
|
||||||
|
it('should parse the current config', () => {
|
||||||
|
loadSponsorsFromString(stringConfig);
|
||||||
|
});
|
||||||
|
it('should pick appropriate sponsor icons', () => {
|
||||||
|
const numLoads = 100;
|
||||||
|
const expectedNumIcons = 3;
|
||||||
|
|
||||||
|
const sponsors = loadSponsorsFromString(stringConfig);
|
||||||
|
const picks = [];
|
||||||
|
for (let load = 0; load < numLoads; ++load) {
|
||||||
|
picks.push(sponsors.pickTopIcons());
|
||||||
|
}
|
||||||
|
const countBySponsor = new Map();
|
||||||
|
for (const pick of picks) {
|
||||||
|
for (const sponsor of pick) {
|
||||||
|
countBySponsor.set(sponsor, (countBySponsor.get(sponsor) || 0) + 1);
|
||||||
|
}
|
||||||
|
pick.length.should.eq(expectedNumIcons);
|
||||||
|
}
|
||||||
|
for (const topIcon of sponsors.getAllTopIcons()) {
|
||||||
|
const appearsEvery = countBySponsor.get(topIcon) / numLoads;
|
||||||
|
appearsEvery.should.lte(topIcon.topIconShowEvery);
|
||||||
|
}
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
6
views/bits/icons.pug
Normal file
6
views/bits/icons.pug
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
each icon in sponsors.pickTopIcons()
|
||||||
|
if !icon.icon_dark || icon.icon === icon.icon_dark
|
||||||
|
img.ces-icon(src=icon.icon alt=icon.name data-statsid=icon.statsId)
|
||||||
|
else
|
||||||
|
img.ces-icon.theme-light-only(src=icon.icon alt=icon.name data-statsid=icon.statsId)
|
||||||
|
img.ces-icon.theme-dark-only(src=icon.icon_dark alt=icon.name data-statsid=icon.statsId)
|
||||||
@@ -18,7 +18,7 @@ block content
|
|||||||
a(href="mailto:matt@godbolt.org") email Matt
|
a(href="mailto:matt@godbolt.org") email Matt
|
||||||
| for corporate sponsorship.
|
| for corporate sponsorship.
|
||||||
.mt-6
|
.mt-6
|
||||||
each level, index in sponsors.levels
|
each level, index in sponsors.getLevels()
|
||||||
.ces-item-block(class=level.class id="ces_hop_" + index)
|
.ces-item-block(class=level.class id="ces_hop_" + index)
|
||||||
h2.ces-item-name= level.name
|
h2.ces-item-name= level.name
|
||||||
div.ces-item-description= level.description
|
div.ces-item-description= level.description
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
div.ces-level-selectors.d-flex.justify-content-center.mt-3
|
div.ces-level-selectors.d-flex.justify-content-center.mt-3
|
||||||
div.btn-group.btn-group
|
div.btn-group.btn-group
|
||||||
each level, index in sponsors.levels
|
each level, index in sponsors.getLevels()
|
||||||
button.btn.btn-primary(onclick='document.getElementById("ces_hop_' + index + '").scrollIntoView({behavior:"smooth"});')=level.name
|
button.btn.btn-primary(onclick='document.getElementById("ces_hop_' + index + '").scrollIntoView({behavior:"smooth"});')=level.name
|
||||||
|
|
||||||
block content
|
block content
|
||||||
|
|||||||
@@ -1,12 +1,6 @@
|
|||||||
.ces-icons
|
.ces-icons
|
||||||
each icon in sponsors.icons
|
if noscript
|
||||||
if noscript
|
each icon in sponsors.pickTopIcons()
|
||||||
a(href=icon.url)
|
a(href=icon.url)
|
||||||
img.ces-icon(src=icon.icon alt=icon.name data-statsid=icon.statsId)
|
img.ces-icon(src=icon.icon alt=icon.name data-statsid=icon.statsId)
|
||||||
span
|
span
|
||||||
else
|
|
||||||
if !icon.icon_dark || icon.icon === icon.icon_dark
|
|
||||||
img.ces-icon(src=icon.icon alt=icon.name data-statsid=icon.statsId)
|
|
||||||
else
|
|
||||||
img.ces-icon.theme-light-only(src=icon.icon alt=icon.name data-statsid=icon.statsId)
|
|
||||||
img.ces-icon.theme-dark-only(src=icon.icon_dark alt=icon.name data-statsid=icon.statsId)
|
|
||||||
|
|||||||
Reference in New Issue
Block a user