mirror of
https://github.com/aljazceru/CTFd.git
synced 2026-02-20 13:44:22 +01:00
Squashed 'CTFd/themes/core-beta/' changes from 9126d77d..5ce3003b
5ce3003b Merge pull request #47 from aCursedComrade/patch-1 c9887cb1 Fix team template git-subtree-dir: CTFd/themes/core-beta git-subtree-split: 5ce3003b4d68352e629ee2d390bc999e7d6b071e
This commit is contained in:
8
assets/js/utils/alerts.js
Normal file
8
assets/js/utils/alerts.js
Normal file
@@ -0,0 +1,8 @@
|
||||
import { Alert } from "bootstrap";
|
||||
|
||||
export default () => {
|
||||
const alertList = [].slice.call(document.querySelectorAll(".alert"));
|
||||
alertList.map(function (element) {
|
||||
return new Alert(element);
|
||||
});
|
||||
};
|
||||
15
assets/js/utils/clipboard.js
Normal file
15
assets/js/utils/clipboard.js
Normal file
@@ -0,0 +1,15 @@
|
||||
import { Tooltip } from "bootstrap";
|
||||
|
||||
export function copyToClipboard($input) {
|
||||
const tooltip = new Tooltip($input, {
|
||||
title: "Copied!",
|
||||
trigger: "manual",
|
||||
});
|
||||
|
||||
navigator.clipboard.writeText($input.value).then(() => {
|
||||
tooltip.show();
|
||||
setTimeout(() => {
|
||||
tooltip.hide();
|
||||
}, 1500);
|
||||
});
|
||||
}
|
||||
8
assets/js/utils/collapse.js
Normal file
8
assets/js/utils/collapse.js
Normal file
@@ -0,0 +1,8 @@
|
||||
import { Collapse } from "bootstrap";
|
||||
|
||||
export default () => {
|
||||
const collapseList = [].slice.call(document.querySelectorAll(".collapse"));
|
||||
collapseList.map(element => {
|
||||
return new Collapse(element, { toggle: false });
|
||||
});
|
||||
};
|
||||
103
assets/js/utils/graphs/echarts/categories.js
Normal file
103
assets/js/utils/graphs/echarts/categories.js
Normal file
@@ -0,0 +1,103 @@
|
||||
import { colorHash } from "@ctfdio/ctfd-js/ui";
|
||||
|
||||
export function getOption(solves) {
|
||||
let option = {
|
||||
title: {
|
||||
left: "center",
|
||||
text: "Category Breakdown",
|
||||
},
|
||||
tooltip: {
|
||||
trigger: "item",
|
||||
},
|
||||
toolbox: {
|
||||
show: true,
|
||||
feature: {
|
||||
saveAsImage: {},
|
||||
},
|
||||
},
|
||||
legend: {
|
||||
type: "scroll",
|
||||
orient: "vertical",
|
||||
top: "middle",
|
||||
right: 0,
|
||||
data: [],
|
||||
},
|
||||
series: [
|
||||
{
|
||||
name: "Category Breakdown",
|
||||
type: "pie",
|
||||
radius: ["30%", "50%"],
|
||||
avoidLabelOverlap: false,
|
||||
label: {
|
||||
show: false,
|
||||
position: "center",
|
||||
},
|
||||
itemStyle: {
|
||||
normal: {
|
||||
label: {
|
||||
show: true,
|
||||
formatter: function (data) {
|
||||
return `${data.percent}% (${data.value})`;
|
||||
},
|
||||
},
|
||||
labelLine: {
|
||||
show: true,
|
||||
},
|
||||
},
|
||||
emphasis: {
|
||||
label: {
|
||||
show: true,
|
||||
position: "center",
|
||||
textStyle: {
|
||||
fontSize: "14",
|
||||
fontWeight: "normal",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
emphasis: {
|
||||
label: {
|
||||
show: true,
|
||||
fontSize: "30",
|
||||
fontWeight: "bold",
|
||||
},
|
||||
},
|
||||
labelLine: {
|
||||
show: false,
|
||||
},
|
||||
data: [],
|
||||
},
|
||||
],
|
||||
};
|
||||
const categories = [];
|
||||
|
||||
for (let i = 0; i < solves.length; i++) {
|
||||
categories.push(solves[i].challenge.category);
|
||||
}
|
||||
|
||||
const keys = categories.filter((elem, pos) => {
|
||||
return categories.indexOf(elem) == pos;
|
||||
});
|
||||
|
||||
const counts = [];
|
||||
for (let i = 0; i < keys.length; i++) {
|
||||
let count = 0;
|
||||
for (let x = 0; x < categories.length; x++) {
|
||||
if (categories[x] == keys[i]) {
|
||||
count++;
|
||||
}
|
||||
}
|
||||
counts.push(count);
|
||||
}
|
||||
|
||||
keys.forEach((category, index) => {
|
||||
option.legend.data.push(category);
|
||||
option.series[0].data.push({
|
||||
value: counts[index],
|
||||
name: category,
|
||||
itemStyle: { color: colorHash(category) },
|
||||
});
|
||||
});
|
||||
|
||||
return option;
|
||||
}
|
||||
44
assets/js/utils/graphs/echarts/index.js
Normal file
44
assets/js/utils/graphs/echarts/index.js
Normal file
@@ -0,0 +1,44 @@
|
||||
import * as echarts from "echarts/core";
|
||||
import { LineChart } from "echarts/charts";
|
||||
import {
|
||||
TitleComponent,
|
||||
TooltipComponent,
|
||||
GridComponent,
|
||||
DatasetComponent,
|
||||
TransformComponent,
|
||||
LegendComponent,
|
||||
ToolboxComponent,
|
||||
DataZoomComponent,
|
||||
} from "echarts/components";
|
||||
// Features like Universal Transition and Label Layout
|
||||
import { LabelLayout, UniversalTransition } from "echarts/features";
|
||||
// Import the Canvas renderer
|
||||
// Note that introducing the CanvasRenderer or SVGRenderer is a required step
|
||||
import { CanvasRenderer } from "echarts/renderers";
|
||||
|
||||
// Register the required components
|
||||
echarts.use([
|
||||
LineChart,
|
||||
TitleComponent,
|
||||
TooltipComponent,
|
||||
GridComponent,
|
||||
DatasetComponent,
|
||||
TransformComponent,
|
||||
LegendComponent,
|
||||
ToolboxComponent,
|
||||
DataZoomComponent,
|
||||
LabelLayout,
|
||||
UniversalTransition,
|
||||
CanvasRenderer,
|
||||
]);
|
||||
|
||||
export function embed(target, option) {
|
||||
let chart = echarts.init(target);
|
||||
chart.setOption(option);
|
||||
|
||||
window.addEventListener("resize", () => {
|
||||
if (chart) {
|
||||
chart.resize();
|
||||
}
|
||||
});
|
||||
}
|
||||
106
assets/js/utils/graphs/echarts/scoreboard.js
Normal file
106
assets/js/utils/graphs/echarts/scoreboard.js
Normal file
@@ -0,0 +1,106 @@
|
||||
import { colorHash } from "@ctfdio/ctfd-js/ui";
|
||||
import dayjs from "dayjs";
|
||||
|
||||
export function cumulativeSum(arr) {
|
||||
let result = arr.concat();
|
||||
for (let i = 0; i < arr.length; i++) {
|
||||
result[i] = arr.slice(0, i + 1).reduce(function (p, i) {
|
||||
return p + i;
|
||||
});
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
export function getOption(mode, places) {
|
||||
let option = {
|
||||
title: {
|
||||
left: "center",
|
||||
text: "Top 10 " + (mode === "teams" ? "Teams" : "Users"),
|
||||
},
|
||||
tooltip: {
|
||||
trigger: "axis",
|
||||
axisPointer: {
|
||||
type: "cross",
|
||||
},
|
||||
},
|
||||
legend: {
|
||||
type: "scroll",
|
||||
orient: "horizontal",
|
||||
align: "left",
|
||||
bottom: 35,
|
||||
data: [],
|
||||
},
|
||||
toolbox: {
|
||||
feature: {
|
||||
dataZoom: {
|
||||
yAxisIndex: "none",
|
||||
},
|
||||
saveAsImage: {},
|
||||
},
|
||||
},
|
||||
grid: {
|
||||
containLabel: true,
|
||||
},
|
||||
xAxis: [
|
||||
{
|
||||
type: "time",
|
||||
boundaryGap: false,
|
||||
data: [],
|
||||
},
|
||||
],
|
||||
yAxis: [
|
||||
{
|
||||
type: "value",
|
||||
},
|
||||
],
|
||||
dataZoom: [
|
||||
{
|
||||
id: "dataZoomX",
|
||||
type: "slider",
|
||||
xAxisIndex: [0],
|
||||
filterMode: "filter",
|
||||
height: 20,
|
||||
top: 35,
|
||||
fillerColor: "rgba(233, 236, 241, 0.4)",
|
||||
},
|
||||
],
|
||||
series: [],
|
||||
};
|
||||
|
||||
const teams = Object.keys(places);
|
||||
for (let i = 0; i < teams.length; i++) {
|
||||
const team_score = [];
|
||||
const times = [];
|
||||
for (let j = 0; j < places[teams[i]]["solves"].length; j++) {
|
||||
team_score.push(places[teams[i]]["solves"][j].value);
|
||||
const date = dayjs(places[teams[i]]["solves"][j].date);
|
||||
times.push(date.toDate());
|
||||
}
|
||||
|
||||
const total_scores = cumulativeSum(team_score);
|
||||
let scores = times.map(function (e, i) {
|
||||
return [e, total_scores[i]];
|
||||
});
|
||||
|
||||
option.legend.data.push(places[teams[i]]["name"]);
|
||||
|
||||
const data = {
|
||||
name: places[teams[i]]["name"],
|
||||
type: "line",
|
||||
label: {
|
||||
normal: {
|
||||
position: "top",
|
||||
},
|
||||
},
|
||||
itemStyle: {
|
||||
normal: {
|
||||
color: colorHash(places[teams[i]]["name"] + places[teams[i]]["id"]),
|
||||
},
|
||||
},
|
||||
data: scores,
|
||||
};
|
||||
option.series.push(data);
|
||||
}
|
||||
|
||||
return option;
|
||||
}
|
||||
81
assets/js/utils/graphs/echarts/solve-percentage.js
Normal file
81
assets/js/utils/graphs/echarts/solve-percentage.js
Normal file
@@ -0,0 +1,81 @@
|
||||
export function getOption(solves, fails) {
|
||||
let option = {
|
||||
title: {
|
||||
left: "center",
|
||||
text: "Solve Percentages",
|
||||
},
|
||||
tooltip: {
|
||||
trigger: "item",
|
||||
},
|
||||
toolbox: {
|
||||
show: true,
|
||||
feature: {
|
||||
saveAsImage: {},
|
||||
},
|
||||
},
|
||||
legend: {
|
||||
orient: "vertical",
|
||||
top: "middle",
|
||||
right: 0,
|
||||
data: ["Fails", "Solves"],
|
||||
},
|
||||
series: [
|
||||
{
|
||||
name: "Solve Percentages",
|
||||
type: "pie",
|
||||
radius: ["30%", "50%"],
|
||||
avoidLabelOverlap: false,
|
||||
label: {
|
||||
show: false,
|
||||
position: "center",
|
||||
},
|
||||
itemStyle: {
|
||||
normal: {
|
||||
label: {
|
||||
show: true,
|
||||
formatter: function (data) {
|
||||
return `${data.name} - ${data.value} (${data.percent}%)`;
|
||||
},
|
||||
},
|
||||
labelLine: {
|
||||
show: true,
|
||||
},
|
||||
},
|
||||
emphasis: {
|
||||
label: {
|
||||
show: true,
|
||||
position: "center",
|
||||
textStyle: {
|
||||
fontSize: "14",
|
||||
fontWeight: "normal",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
emphasis: {
|
||||
label: {
|
||||
show: true,
|
||||
fontSize: "30",
|
||||
fontWeight: "bold",
|
||||
},
|
||||
},
|
||||
labelLine: {
|
||||
show: false,
|
||||
},
|
||||
data: [
|
||||
{
|
||||
value: fails,
|
||||
name: "Fails",
|
||||
itemStyle: { color: "rgb(207, 38, 0)" },
|
||||
},
|
||||
{
|
||||
value: solves,
|
||||
name: "Solves",
|
||||
itemStyle: { color: "rgb(0, 209, 64)" },
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
return option;
|
||||
}
|
||||
102
assets/js/utils/graphs/echarts/userscore.js
Normal file
102
assets/js/utils/graphs/echarts/userscore.js
Normal file
@@ -0,0 +1,102 @@
|
||||
import { colorHash } from "@ctfdio/ctfd-js/ui";
|
||||
import { cumulativeSum } from "./scoreboard";
|
||||
import dayjs from "dayjs";
|
||||
|
||||
export function getOption(id, name, solves, awards) {
|
||||
let option = {
|
||||
title: {
|
||||
left: "center",
|
||||
text: "Score over Time",
|
||||
},
|
||||
tooltip: {
|
||||
trigger: "axis",
|
||||
axisPointer: {
|
||||
type: "cross",
|
||||
},
|
||||
},
|
||||
legend: {
|
||||
type: "scroll",
|
||||
orient: "horizontal",
|
||||
align: "left",
|
||||
bottom: 0,
|
||||
data: [name],
|
||||
},
|
||||
toolbox: {
|
||||
feature: {
|
||||
saveAsImage: {},
|
||||
},
|
||||
},
|
||||
grid: {
|
||||
containLabel: true,
|
||||
},
|
||||
xAxis: [
|
||||
{
|
||||
type: "category",
|
||||
boundaryGap: false,
|
||||
data: [],
|
||||
},
|
||||
],
|
||||
yAxis: [
|
||||
{
|
||||
type: "value",
|
||||
},
|
||||
],
|
||||
dataZoom: [
|
||||
{
|
||||
id: "dataZoomX",
|
||||
type: "slider",
|
||||
xAxisIndex: [0],
|
||||
filterMode: "filter",
|
||||
height: 20,
|
||||
top: 35,
|
||||
fillerColor: "rgba(233, 236, 241, 0.4)",
|
||||
},
|
||||
],
|
||||
series: [],
|
||||
};
|
||||
|
||||
const times = [];
|
||||
const scores = [];
|
||||
const total = solves.concat(awards);
|
||||
|
||||
total.sort((a, b) => {
|
||||
return new Date(a.date) - new Date(b.date);
|
||||
});
|
||||
|
||||
for (let i = 0; i < total.length; i++) {
|
||||
const date = dayjs(total[i].date);
|
||||
times.push(date.toDate());
|
||||
try {
|
||||
scores.push(total[i].challenge.value);
|
||||
} catch (e) {
|
||||
scores.push(total[i].value);
|
||||
}
|
||||
}
|
||||
|
||||
times.forEach(time => {
|
||||
option.xAxis[0].data.push(time);
|
||||
});
|
||||
|
||||
option.series.push({
|
||||
name: name,
|
||||
type: "line",
|
||||
label: {
|
||||
normal: {
|
||||
show: true,
|
||||
position: "top",
|
||||
},
|
||||
},
|
||||
areaStyle: {
|
||||
normal: {
|
||||
color: colorHash(name + id),
|
||||
},
|
||||
},
|
||||
itemStyle: {
|
||||
normal: {
|
||||
color: colorHash(name + id),
|
||||
},
|
||||
},
|
||||
data: cumulativeSum(scores),
|
||||
});
|
||||
return option;
|
||||
}
|
||||
105
assets/js/utils/graphs/vega/categories.js
Normal file
105
assets/js/utils/graphs/vega/categories.js
Normal file
@@ -0,0 +1,105 @@
|
||||
export function getSpec(description, values) {
|
||||
return {
|
||||
$schema: "https://vega.github.io/schema/vega-lite/v5.json",
|
||||
description: description,
|
||||
data: {
|
||||
values: values,
|
||||
},
|
||||
width: "container",
|
||||
|
||||
layer: [
|
||||
{
|
||||
params: [
|
||||
{
|
||||
name: "category",
|
||||
select: {
|
||||
type: "point",
|
||||
fields: ["category"],
|
||||
},
|
||||
bind: "legend",
|
||||
},
|
||||
],
|
||||
mark: {
|
||||
type: "arc",
|
||||
innerRadius: 50,
|
||||
outerRadius: 95,
|
||||
stroke: "#fff",
|
||||
},
|
||||
encoding: {
|
||||
opacity: {
|
||||
condition: {
|
||||
param: "category",
|
||||
value: 1,
|
||||
},
|
||||
value: 0.2,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
mark: {
|
||||
type: "text",
|
||||
radius: 105,
|
||||
},
|
||||
encoding: {
|
||||
text: {
|
||||
field: "value",
|
||||
type: "quantitative",
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
encoding: {
|
||||
theta: {
|
||||
field: "value",
|
||||
type: "quantitative",
|
||||
stack: true,
|
||||
},
|
||||
color: {
|
||||
field: "category",
|
||||
type: "nominal",
|
||||
// scale: {
|
||||
// domain: ["Solves", "Fails"],
|
||||
// range: ["#00d13f", "#cf2600"],
|
||||
// },
|
||||
legend: {
|
||||
orient: "bottom",
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export function getValues(solves) {
|
||||
const solvesData = solves.data;
|
||||
const categories = [];
|
||||
|
||||
for (let i = 0; i < solvesData.length; i++) {
|
||||
categories.push(solvesData[i].challenge.category);
|
||||
}
|
||||
|
||||
const keys = categories.filter((elem, pos) => {
|
||||
return categories.indexOf(elem) == pos;
|
||||
});
|
||||
|
||||
const counts = [];
|
||||
for (let i = 0; i < keys.length; i++) {
|
||||
let count = 0;
|
||||
for (let x = 0; x < categories.length; x++) {
|
||||
if (categories[x] == keys[i]) {
|
||||
count++;
|
||||
}
|
||||
}
|
||||
counts.push(count);
|
||||
}
|
||||
|
||||
let values = [];
|
||||
|
||||
keys.forEach((category, index) => {
|
||||
values.push({
|
||||
category: category,
|
||||
value: counts[index],
|
||||
});
|
||||
});
|
||||
|
||||
return values;
|
||||
}
|
||||
53
assets/js/utils/graphs/vega/scoreboard.js
Normal file
53
assets/js/utils/graphs/vega/scoreboard.js
Normal file
@@ -0,0 +1,53 @@
|
||||
import { cumulativeSum } from "../math";
|
||||
|
||||
export function getSpec(description, values) {
|
||||
let spec = {
|
||||
$schema: "https://vega.github.io/schema/vega-lite/v5.json",
|
||||
description: description,
|
||||
data: { values: values },
|
||||
mark: "line",
|
||||
width: "container",
|
||||
encoding: {
|
||||
x: { field: "date", type: "temporal" },
|
||||
y: { field: "score", type: "quantitative" },
|
||||
color: {
|
||||
field: "name",
|
||||
type: "nominal",
|
||||
legend: {
|
||||
orient: "bottom",
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
return spec;
|
||||
}
|
||||
|
||||
export function getValues(scoreboardDetail) {
|
||||
const teams = Object.keys(scoreboardDetail);
|
||||
let values = [];
|
||||
|
||||
for (let i = 0; i < teams.length; i++) {
|
||||
const team = scoreboardDetail[teams[i]];
|
||||
const team_score = [];
|
||||
const times = [];
|
||||
for (let j = 0; j < team["solves"].length; j++) {
|
||||
team_score.push(team["solves"][j].value);
|
||||
times.push(team["solves"][j].date);
|
||||
// const date = dayjs(team["solves"][j].date);
|
||||
// times.push(date.toDate());
|
||||
}
|
||||
|
||||
const total_scores = cumulativeSum(team_score);
|
||||
const team_name = team["name"];
|
||||
let scores = times.map(function (e, i) {
|
||||
return {
|
||||
name: team_name,
|
||||
score: total_scores[i],
|
||||
date: e,
|
||||
};
|
||||
});
|
||||
values = values.concat(scores);
|
||||
}
|
||||
|
||||
return values;
|
||||
}
|
||||
83
assets/js/utils/graphs/vega/solve-percentage.js
Normal file
83
assets/js/utils/graphs/vega/solve-percentage.js
Normal file
@@ -0,0 +1,83 @@
|
||||
export function getSpec(description, values) {
|
||||
return {
|
||||
$schema: "https://vega.github.io/schema/vega-lite/v5.json",
|
||||
description: description,
|
||||
data: {
|
||||
values: values,
|
||||
},
|
||||
width: "container",
|
||||
|
||||
layer: [
|
||||
{
|
||||
params: [
|
||||
{
|
||||
name: "category",
|
||||
select: {
|
||||
type: "point",
|
||||
fields: ["category"],
|
||||
},
|
||||
bind: "legend",
|
||||
},
|
||||
],
|
||||
mark: {
|
||||
type: "arc",
|
||||
innerRadius: 50,
|
||||
outerRadius: 95,
|
||||
stroke: "#fff",
|
||||
},
|
||||
encoding: {
|
||||
opacity: {
|
||||
condition: {
|
||||
param: "category",
|
||||
value: 1,
|
||||
},
|
||||
value: 0.2,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
mark: {
|
||||
type: "text",
|
||||
radius: 105,
|
||||
},
|
||||
encoding: {
|
||||
text: {
|
||||
field: "value",
|
||||
type: "quantitative",
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
encoding: {
|
||||
theta: {
|
||||
field: "value",
|
||||
type: "quantitative",
|
||||
stack: true,
|
||||
},
|
||||
color: {
|
||||
field: "category",
|
||||
type: "nominal",
|
||||
scale: {
|
||||
domain: ["Solves", "Fails"],
|
||||
range: ["#00d13f", "#cf2600"],
|
||||
},
|
||||
legend: {
|
||||
orient: "bottom",
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export function getValues(solves, fails) {
|
||||
return [
|
||||
{
|
||||
category: "Solves",
|
||||
value: solves.meta.count,
|
||||
},
|
||||
{
|
||||
category: "Fails",
|
||||
value: fails.meta.count,
|
||||
},
|
||||
];
|
||||
}
|
||||
60
assets/js/utils/graphs/vega/userscore.js
Normal file
60
assets/js/utils/graphs/vega/userscore.js
Normal file
@@ -0,0 +1,60 @@
|
||||
import { cumulativeSum } from "../math";
|
||||
|
||||
export function getSpec(description, values) {
|
||||
return {
|
||||
$schema: "https://vega.github.io/schema/vega-lite/v5.json",
|
||||
description: description,
|
||||
data: {
|
||||
values: values,
|
||||
},
|
||||
width: "container",
|
||||
mark: {
|
||||
type: "area",
|
||||
line: true,
|
||||
point: true,
|
||||
// interpolate: "step-after",
|
||||
tooltip: { content: "data", nearest: true },
|
||||
},
|
||||
encoding: {
|
||||
x: { field: "time", type: "temporal" },
|
||||
y: { field: "score", type: "quantitative" },
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export function getValues(solves, awards) {
|
||||
const times = [];
|
||||
let scores = [];
|
||||
const solvesData2 = solves.data;
|
||||
const awardsData = awards.data;
|
||||
const total = solvesData2.concat(awardsData);
|
||||
|
||||
total.sort((a, b) => {
|
||||
return new Date(a.date) - new Date(b.date);
|
||||
});
|
||||
|
||||
for (let i = 0; i < total.length; i++) {
|
||||
// const date = dayjs(total[i].date);
|
||||
// times.push(date.toDate());
|
||||
const date = total[i].date;
|
||||
times.push(date);
|
||||
try {
|
||||
scores.push(total[i].challenge.value);
|
||||
} catch (e) {
|
||||
scores.push(total[i].value);
|
||||
}
|
||||
}
|
||||
|
||||
scores = cumulativeSum(scores);
|
||||
|
||||
let values = [];
|
||||
times.forEach((time, index) => {
|
||||
// option.xAxis[0].data.push(time);
|
||||
values.push({
|
||||
time: time,
|
||||
score: scores[index],
|
||||
});
|
||||
});
|
||||
|
||||
return values;
|
||||
}
|
||||
9
assets/js/utils/math.js
Normal file
9
assets/js/utils/math.js
Normal file
@@ -0,0 +1,9 @@
|
||||
export function cumulativeSum(arr) {
|
||||
let result = arr.concat();
|
||||
for (let i = 0; i < arr.length; i++) {
|
||||
result[i] = arr.slice(0, i + 1).reduce(function (p, i) {
|
||||
return p + i;
|
||||
});
|
||||
}
|
||||
return result;
|
||||
}
|
||||
23
assets/js/utils/notifications/alerts.js
Normal file
23
assets/js/utils/notifications/alerts.js
Normal file
@@ -0,0 +1,23 @@
|
||||
import Alpine from "alpinejs";
|
||||
import { Modal } from "bootstrap";
|
||||
|
||||
import CTFd from "../../index";
|
||||
|
||||
export default () => {
|
||||
Alpine.store("modal", { title: "", html: "" });
|
||||
|
||||
CTFd._functions.events.eventAlert = data => {
|
||||
Alpine.store("modal", data);
|
||||
let modal = new Modal(document.querySelector("[x-ref='modal']"));
|
||||
// TODO: Get rid of this private attribute access
|
||||
// See https://github.com/twbs/bootstrap/issues/31266
|
||||
modal._element.addEventListener(
|
||||
"hidden.bs.modal",
|
||||
event => {
|
||||
CTFd._functions.events.eventRead(data.id);
|
||||
},
|
||||
{ once: true }
|
||||
);
|
||||
modal.show();
|
||||
};
|
||||
};
|
||||
19
assets/js/utils/notifications/read.js
Normal file
19
assets/js/utils/notifications/read.js
Normal file
@@ -0,0 +1,19 @@
|
||||
import Alpine from "alpinejs";
|
||||
import CTFd from "../../index";
|
||||
|
||||
export default () => {
|
||||
CTFd._functions.events.eventCount = count => {
|
||||
Alpine.store("unread_count", count);
|
||||
};
|
||||
|
||||
CTFd._functions.events.eventRead = eventId => {
|
||||
CTFd.events.counter.read.add(eventId);
|
||||
let count = CTFd.events.counter.unread.getAll().length;
|
||||
CTFd.events.controller.broadcast("counter", { count: count });
|
||||
Alpine.store("unread_count", count);
|
||||
};
|
||||
|
||||
document.addEventListener("alpine:init", () => {
|
||||
CTFd._functions.events.eventCount(CTFd.events.counter.unread.getAll().length);
|
||||
});
|
||||
};
|
||||
28
assets/js/utils/notifications/toasts.js
Normal file
28
assets/js/utils/notifications/toasts.js
Normal file
@@ -0,0 +1,28 @@
|
||||
import Alpine from "alpinejs";
|
||||
import { Toast } from "bootstrap";
|
||||
import CTFd from "../../index";
|
||||
|
||||
export default () => {
|
||||
Alpine.store("toast", { title: "", html: "" });
|
||||
|
||||
CTFd._functions.events.eventToast = data => {
|
||||
Alpine.store("toast", data);
|
||||
let toast = new Toast(document.querySelector("[x-ref='toast']"));
|
||||
// TODO: Get rid of this private attribute access
|
||||
// See https://github.com/twbs/bootstrap/issues/31266
|
||||
let close = toast._element.querySelector("[data-bs-dismiss='toast']");
|
||||
let handler = event => {
|
||||
CTFd._functions.events.eventRead(data.id);
|
||||
};
|
||||
close.addEventListener("click", handler, { once: true });
|
||||
toast._element.addEventListener(
|
||||
"hidden.bs.toast",
|
||||
event => {
|
||||
close.removeEventListener("click", handler);
|
||||
},
|
||||
{ once: true }
|
||||
);
|
||||
|
||||
toast.show();
|
||||
};
|
||||
};
|
||||
10
assets/js/utils/tooltips.js
Normal file
10
assets/js/utils/tooltips.js
Normal file
@@ -0,0 +1,10 @@
|
||||
import { Tooltip } from "bootstrap";
|
||||
|
||||
export default () => {
|
||||
const tooltipList = [].slice.call(
|
||||
document.querySelectorAll('[data-bs-toggle="tooltip"]')
|
||||
);
|
||||
tooltipList.map(element => {
|
||||
return new Tooltip(element);
|
||||
});
|
||||
};
|
||||
Reference in New Issue
Block a user