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:
Kevin Chung
2023-06-11 15:56:28 -04:00
parent 692c4b086c
commit a64e7d51ef
815 changed files with 683 additions and 101218 deletions

View 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);
});
};

View 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);
});
}

View 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 });
});
};

View 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;
}

View 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();
}
});
}

View 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;
}

View 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;
}

View 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;
}

View 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;
}

View 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;
}

View 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,
},
];
}

View 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
View 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;
}

View 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();
};
};

View 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);
});
};

View 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();
};
};

View 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);
});
};