mirror of
https://github.com/aljazceru/CTFd.git
synced 2026-02-09 00:04:21 +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:
BIN
assets/img/favicon.ico
Normal file
BIN
assets/img/favicon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.1 KiB |
253
assets/js/challenges.js
Normal file
253
assets/js/challenges.js
Normal file
@@ -0,0 +1,253 @@
|
||||
import Alpine from "alpinejs";
|
||||
import dayjs from "dayjs";
|
||||
|
||||
import CTFd from "./index";
|
||||
|
||||
import { Modal, Tab } from "bootstrap";
|
||||
import highlight from "./theme/highlight";
|
||||
|
||||
function addTargetBlank(html) {
|
||||
let dom = new DOMParser();
|
||||
let view = dom.parseFromString(html, "text/html");
|
||||
let links = view.querySelectorAll('a[href*="://"]');
|
||||
links.forEach(link => {
|
||||
link.setAttribute("target", "_blank");
|
||||
});
|
||||
return view.documentElement.outerHTML;
|
||||
}
|
||||
|
||||
window.Alpine = Alpine;
|
||||
|
||||
Alpine.store("challenge", {
|
||||
data: {
|
||||
view: "",
|
||||
},
|
||||
});
|
||||
|
||||
Alpine.data("Hint", () => ({
|
||||
id: null,
|
||||
html: null,
|
||||
|
||||
async showHint(event) {
|
||||
if (event.target.open) {
|
||||
let response = await CTFd.pages.challenge.loadHint(this.id);
|
||||
let hint = response.data;
|
||||
if (hint.content) {
|
||||
this.html = addTargetBlank(hint.html);
|
||||
} else {
|
||||
let answer = await CTFd.pages.challenge.displayUnlock(this.id);
|
||||
if (answer) {
|
||||
let unlock = await CTFd.pages.challenge.loadUnlock(this.id);
|
||||
|
||||
if (unlock.success) {
|
||||
let response = await CTFd.pages.challenge.loadHint(this.id);
|
||||
let hint = response.data;
|
||||
this.html = addTargetBlank(hint.html);
|
||||
} else {
|
||||
event.target.open = false;
|
||||
CTFd._functions.challenge.displayUnlockError(unlock);
|
||||
}
|
||||
} else {
|
||||
event.target.open = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
}));
|
||||
|
||||
Alpine.data("Challenge", () => ({
|
||||
id: null,
|
||||
next_id: null,
|
||||
submission: "",
|
||||
tab: null,
|
||||
solves: [],
|
||||
response: null,
|
||||
|
||||
async init() {
|
||||
highlight();
|
||||
},
|
||||
|
||||
getStyles() {
|
||||
let styles = {
|
||||
"modal-dialog": true,
|
||||
};
|
||||
try {
|
||||
let size = CTFd.config.themeSettings.challenge_window_size;
|
||||
switch (size) {
|
||||
case "sm":
|
||||
styles["modal-sm"] = true;
|
||||
break;
|
||||
case "lg":
|
||||
styles["modal-lg"] = true;
|
||||
break;
|
||||
case "xl":
|
||||
styles["modal-xl"] = true;
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
} catch (error) {
|
||||
// Ignore errors with challenge window size
|
||||
console.log("Error processing challenge_window_size");
|
||||
console.log(error);
|
||||
}
|
||||
return styles;
|
||||
},
|
||||
|
||||
async init() {
|
||||
highlight();
|
||||
},
|
||||
|
||||
async showChallenge() {
|
||||
new Tab(this.$el).show();
|
||||
},
|
||||
|
||||
async showSolves() {
|
||||
this.solves = await CTFd.pages.challenge.loadSolves(this.id);
|
||||
this.solves.forEach(solve => {
|
||||
solve.date = dayjs(solve.date).format("MMMM Do, h:mm:ss A");
|
||||
return solve;
|
||||
});
|
||||
new Tab(this.$el).show();
|
||||
},
|
||||
|
||||
getNextId() {
|
||||
let data = Alpine.store("challenge").data;
|
||||
return data.next_id;
|
||||
},
|
||||
|
||||
async nextChallenge() {
|
||||
let modal = Modal.getOrCreateInstance("[x-ref='challengeWindow']");
|
||||
|
||||
// TODO: Get rid of this private attribute access
|
||||
// See https://github.com/twbs/bootstrap/issues/31266
|
||||
modal._element.addEventListener(
|
||||
"hidden.bs.modal",
|
||||
event => {
|
||||
// Dispatch load-challenge event to call loadChallenge in the ChallengeBoard
|
||||
Alpine.nextTick(() => {
|
||||
this.$dispatch("load-challenge", this.getNextId());
|
||||
});
|
||||
},
|
||||
{ once: true }
|
||||
);
|
||||
modal.hide();
|
||||
},
|
||||
|
||||
async submitChallenge() {
|
||||
this.response = await CTFd.pages.challenge.submitChallenge(
|
||||
this.id,
|
||||
this.submission
|
||||
);
|
||||
|
||||
await this.renderSubmissionResponse();
|
||||
},
|
||||
|
||||
async renderSubmissionResponse() {
|
||||
if (this.response.data.status === "correct") {
|
||||
this.submission = "";
|
||||
}
|
||||
|
||||
// Dispatch load-challenges event to call loadChallenges in the ChallengeBoard
|
||||
this.$dispatch("load-challenges");
|
||||
},
|
||||
}));
|
||||
|
||||
Alpine.data("ChallengeBoard", () => ({
|
||||
loaded: false,
|
||||
challenges: [],
|
||||
challenge: null,
|
||||
|
||||
async init() {
|
||||
this.challenges = await CTFd.pages.challenges.getChallenges();
|
||||
this.loaded = true;
|
||||
|
||||
if (window.location.hash) {
|
||||
let chalHash = decodeURIComponent(window.location.hash.substring(1));
|
||||
let idx = chalHash.lastIndexOf("-");
|
||||
if (idx >= 0) {
|
||||
let pieces = [chalHash.slice(0, idx), chalHash.slice(idx + 1)];
|
||||
let id = pieces[1];
|
||||
await this.loadChallenge(id);
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
getCategories() {
|
||||
const categories = [];
|
||||
|
||||
this.challenges.forEach(challenge => {
|
||||
const { category } = challenge;
|
||||
|
||||
if (!categories.includes(category)) {
|
||||
categories.push(category);
|
||||
}
|
||||
});
|
||||
|
||||
try {
|
||||
const f = CTFd.config.themeSettings.challenge_category_order;
|
||||
if (f) {
|
||||
const getSort = new Function(`return (${f})`);
|
||||
categories.sort(getSort());
|
||||
}
|
||||
} catch (error) {
|
||||
// Ignore errors with theme category sorting
|
||||
console.log("Error running challenge_category_order function");
|
||||
console.log(error);
|
||||
}
|
||||
|
||||
return categories;
|
||||
},
|
||||
|
||||
getChallenges(category) {
|
||||
let challenges = this.challenges;
|
||||
|
||||
if (category) {
|
||||
challenges = this.challenges.filter(challenge => challenge.category === category);
|
||||
}
|
||||
|
||||
try {
|
||||
const f = CTFd.config.themeSettings.challenge_order;
|
||||
if (f) {
|
||||
const getSort = new Function(`return (${f})`);
|
||||
challenges.sort(getSort());
|
||||
}
|
||||
} catch (error) {
|
||||
// Ignore errors with theme challenge sorting
|
||||
console.log("Error running challenge_order function");
|
||||
console.log(error);
|
||||
}
|
||||
|
||||
return challenges;
|
||||
},
|
||||
|
||||
async loadChallenges() {
|
||||
this.challenges = await CTFd.pages.challenges.getChallenges();
|
||||
},
|
||||
|
||||
async loadChallenge(challengeId) {
|
||||
await CTFd.pages.challenge.displayChallenge(challengeId, challenge => {
|
||||
challenge.data.view = addTargetBlank(challenge.data.view);
|
||||
Alpine.store("challenge").data = challenge.data;
|
||||
|
||||
// nextTick is required here because we're working in a callback
|
||||
Alpine.nextTick(() => {
|
||||
let modal = Modal.getOrCreateInstance("[x-ref='challengeWindow']");
|
||||
// TODO: Get rid of this private attribute access
|
||||
// See https://github.com/twbs/bootstrap/issues/31266
|
||||
modal._element.addEventListener(
|
||||
"hidden.bs.modal",
|
||||
event => {
|
||||
// Remove location hash
|
||||
history.replaceState(null, null, " ");
|
||||
},
|
||||
{ once: true }
|
||||
);
|
||||
modal.show();
|
||||
history.replaceState(null, null, `#${challenge.data.name}-${challengeId}`);
|
||||
});
|
||||
});
|
||||
},
|
||||
}));
|
||||
|
||||
Alpine.start();
|
||||
35
assets/js/index.js
Normal file
35
assets/js/index.js
Normal file
@@ -0,0 +1,35 @@
|
||||
import CTFd from "@ctfdio/ctfd-js";
|
||||
|
||||
import dayjs from "dayjs";
|
||||
import advancedFormat from "dayjs/plugin/advancedFormat";
|
||||
|
||||
import times from "./theme/times";
|
||||
import styles from "./theme/styles";
|
||||
import highlight from "./theme/highlight";
|
||||
|
||||
import alerts from "./utils/alerts";
|
||||
import tooltips from "./utils/tooltips";
|
||||
import collapse from "./utils/collapse";
|
||||
|
||||
import eventAlerts from "./utils/notifications/alerts";
|
||||
import eventToasts from "./utils/notifications/toasts";
|
||||
import eventRead from "./utils/notifications/read";
|
||||
|
||||
dayjs.extend(advancedFormat);
|
||||
CTFd.init(window.init);
|
||||
|
||||
(() => {
|
||||
styles();
|
||||
times();
|
||||
highlight();
|
||||
|
||||
alerts();
|
||||
tooltips();
|
||||
collapse();
|
||||
|
||||
eventRead();
|
||||
eventAlerts();
|
||||
eventToasts();
|
||||
})();
|
||||
|
||||
export default CTFd;
|
||||
33
assets/js/notifications.js
Normal file
33
assets/js/notifications.js
Normal file
@@ -0,0 +1,33 @@
|
||||
import Alpine from "alpinejs";
|
||||
import CTFd from "./index";
|
||||
|
||||
window.CTFd = CTFd;
|
||||
window.Alpine = Alpine;
|
||||
|
||||
// Get unread notifications from server
|
||||
let lastId = CTFd.events.counter.read.getLast();
|
||||
CTFd.fetch(`/api/v1/notifications?since_id=${lastId}`)
|
||||
.then(response => {
|
||||
return response.json();
|
||||
})
|
||||
.then(response => {
|
||||
// Get notifications from server and mark them as read
|
||||
let notifications = response.data;
|
||||
let read = CTFd.events.counter.read.getAll();
|
||||
notifications.forEach(n => {
|
||||
read.push(n.id);
|
||||
});
|
||||
CTFd.events.counter.read.setAll(read);
|
||||
|
||||
// Mark all unread as read
|
||||
CTFd.events.counter.unread.readAll();
|
||||
|
||||
// Broadcast our new count (which should be 0)
|
||||
let count = CTFd.events.counter.unread.getAll().length;
|
||||
CTFd.events.controller.broadcast("counter", {
|
||||
count: count,
|
||||
});
|
||||
Alpine.store("unread_count", count);
|
||||
});
|
||||
|
||||
Alpine.start();
|
||||
7
assets/js/page.js
Normal file
7
assets/js/page.js
Normal file
@@ -0,0 +1,7 @@
|
||||
import Alpine from "alpinejs";
|
||||
import CTFd from "./index";
|
||||
|
||||
window.CTFd = CTFd;
|
||||
window.Alpine = Alpine;
|
||||
|
||||
Alpine.start();
|
||||
20
assets/js/scoreboard.js
Normal file
20
assets/js/scoreboard.js
Normal file
@@ -0,0 +1,20 @@
|
||||
import Alpine from "alpinejs";
|
||||
import CTFd from "./index";
|
||||
import { getOption } from "./utils/graphs/echarts/scoreboard";
|
||||
import { embed } from "./utils/graphs/echarts";
|
||||
|
||||
window.Alpine = Alpine;
|
||||
window.CTFd = CTFd;
|
||||
|
||||
Alpine.data("ScoreboardDetail", () => ({
|
||||
data: null,
|
||||
|
||||
async init() {
|
||||
this.data = await CTFd.pages.scoreboard.getScoreboardDetail(10);
|
||||
|
||||
let option = getOption(CTFd.config.userMode, this.data);
|
||||
embed(this.$refs.scoregraph, option);
|
||||
},
|
||||
}));
|
||||
|
||||
Alpine.start();
|
||||
100
assets/js/settings.js
Normal file
100
assets/js/settings.js
Normal file
@@ -0,0 +1,100 @@
|
||||
import Alpine from "alpinejs";
|
||||
import { Modal } from "bootstrap";
|
||||
import { serializeJSON } from "@ctfdio/ctfd-js/forms";
|
||||
|
||||
import CTFd from "./index";
|
||||
import { copyToClipboard } from "./utils/clipboard";
|
||||
|
||||
window.Alpine = Alpine;
|
||||
|
||||
Alpine.data("SettingsForm", () => ({
|
||||
success: null,
|
||||
error: null,
|
||||
initial: null,
|
||||
errors: [],
|
||||
|
||||
init() {
|
||||
this.initial = serializeJSON(this.$el);
|
||||
},
|
||||
|
||||
async updateProfile() {
|
||||
this.success = null;
|
||||
this.error = null;
|
||||
this.errors = [];
|
||||
|
||||
let data = serializeJSON(this.$el, this.initial, true);
|
||||
|
||||
// Process fields[id] to fields: {}
|
||||
data.fields = [];
|
||||
for (const property in data) {
|
||||
if (property.match(/fields\[\d+\]/)) {
|
||||
let field = {};
|
||||
let id = parseInt(property.slice(7, -1));
|
||||
field["field_id"] = id;
|
||||
field["value"] = data[property];
|
||||
data.fields.push(field);
|
||||
delete data[property];
|
||||
}
|
||||
}
|
||||
|
||||
// Send API request
|
||||
const response = await CTFd.pages.settings.updateSettings(data);
|
||||
if (response.success) {
|
||||
this.success = true;
|
||||
this.error = false;
|
||||
|
||||
setTimeout(() => {
|
||||
this.success = null;
|
||||
this.error = null;
|
||||
}, 3000);
|
||||
} else {
|
||||
this.success = false;
|
||||
this.error = true;
|
||||
|
||||
Object.keys(response.errors).map(error => {
|
||||
const error_msg = response.errors[error];
|
||||
this.errors.push(error_msg);
|
||||
});
|
||||
}
|
||||
},
|
||||
}));
|
||||
|
||||
Alpine.data("TokensForm", () => ({
|
||||
token: null,
|
||||
|
||||
async generateToken() {
|
||||
const data = serializeJSON(this.$el);
|
||||
|
||||
if (!data.expiration) {
|
||||
delete data.expiration;
|
||||
}
|
||||
const response = await CTFd.pages.settings.generateToken(data);
|
||||
this.token = response.data.value;
|
||||
|
||||
new Modal(this.$refs.tokenModal).show();
|
||||
},
|
||||
|
||||
copyToken() {
|
||||
copyToClipboard(this.$refs.token);
|
||||
},
|
||||
}));
|
||||
|
||||
Alpine.data("Tokens", () => ({
|
||||
selectedTokenId: null,
|
||||
|
||||
async deleteTokenModal(tokenId) {
|
||||
this.selectedTokenId = tokenId;
|
||||
new Modal(this.$refs.confirmModal).show();
|
||||
},
|
||||
|
||||
async deleteSelectedToken() {
|
||||
await CTFd.pages.settings.deleteToken(this.selectedTokenId);
|
||||
const $token = this.$refs[`token-${this.selectedTokenId}`];
|
||||
|
||||
if ($token) {
|
||||
$token.remove();
|
||||
}
|
||||
},
|
||||
}));
|
||||
|
||||
Alpine.start();
|
||||
145
assets/js/setup.js
Normal file
145
assets/js/setup.js
Normal file
@@ -0,0 +1,145 @@
|
||||
import Alpine from "alpinejs";
|
||||
import dayjs from "dayjs";
|
||||
import { Tab } from "bootstrap";
|
||||
|
||||
import CTFd from "./index";
|
||||
|
||||
window.Alpine = Alpine;
|
||||
|
||||
Alpine.data("SetupForm", () => ({
|
||||
init() {
|
||||
// Bind Enter on any input to clicking the Next button
|
||||
this.$root.querySelectorAll("input").forEach(i =>
|
||||
i.addEventListener("keypress", e => {
|
||||
if (e.key == "Enter") {
|
||||
e.preventDefault();
|
||||
e.target.closest(".tab-pane").querySelector("button[data-href]").click();
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
// Register storage listener for MLC integration
|
||||
window.addEventListener("storage", function (event) {
|
||||
if (event.key == "integrations" && event.newValue) {
|
||||
let integration = JSON.parse(event.newValue);
|
||||
if (integration["name"] == "mlc") {
|
||||
$("#integration-mlc").text("Already Configured").attr("disabled", true);
|
||||
window.focus();
|
||||
localStorage.removeItem("integrations");
|
||||
}
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
validateFileSize(e, limit) {
|
||||
if (e.target.files[0].size > limit) {
|
||||
if (
|
||||
!confirm(
|
||||
`This image file is larger than ${
|
||||
limit / 1000
|
||||
}KB which may result in increased load times. Are you sure you'd like to use this file?`
|
||||
)
|
||||
) {
|
||||
e.target.value = "";
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
switchTab(e) {
|
||||
// Handle tab validation
|
||||
let valid_tab = true;
|
||||
let inputs = e.target
|
||||
.closest('[role="tabpanel"]')
|
||||
.querySelectorAll("input,textarea");
|
||||
|
||||
inputs.forEach(e => {
|
||||
if (e.checkValidity() === false) {
|
||||
valid_tab = false;
|
||||
}
|
||||
});
|
||||
|
||||
if (valid_tab == false) {
|
||||
return;
|
||||
}
|
||||
|
||||
let target = e.target.dataset.href;
|
||||
let tab = this.$root.querySelector(`[data-bs-target="${target}"]`);
|
||||
Tab.getOrCreateInstance(tab).show();
|
||||
},
|
||||
|
||||
setThemeColor(e) {
|
||||
document.querySelector("#config-color-input").value = e.target.value;
|
||||
},
|
||||
|
||||
resetThemeColor(_e) {
|
||||
document.querySelector("#config-color-input").value = "";
|
||||
document.querySelector("#config-color-picker").value = "";
|
||||
},
|
||||
|
||||
processDateTime(datetime) {
|
||||
return function (_event) {
|
||||
let date_picker = document.querySelector(`#${datetime}-date`);
|
||||
let time_picker = document.querySelector(`#${datetime}-time`);
|
||||
let unix_time = dayjs(
|
||||
`${date_picker.value} ${time_picker.value}`,
|
||||
"YYYY-MM-DD HH:mm"
|
||||
).unix();
|
||||
|
||||
if (isNaN(unix_time)) {
|
||||
document.querySelector(`#${datetime}-preview`).value = "";
|
||||
} else {
|
||||
document.querySelector(`#${datetime}-preview`).value = unix_time;
|
||||
}
|
||||
};
|
||||
},
|
||||
|
||||
mlcSetup() {
|
||||
let q = document.querySelector;
|
||||
let r = CTFd.config.urlRoot;
|
||||
let params = {
|
||||
name: q("#ctf_name").value,
|
||||
type: "jeopardy",
|
||||
description: q("#ctf_description").value,
|
||||
user_mode: q("#user_mode").value,
|
||||
event_url: window.location.origin + r,
|
||||
redirect_url: window.location.origin + r + "/redirect",
|
||||
integration_setup_url: window.location.origin + r + "/setup/integrations",
|
||||
start: q("#start-preview").value,
|
||||
end: q("#end-preview").value,
|
||||
platform: "CTFd",
|
||||
state: window.STATE,
|
||||
};
|
||||
|
||||
const ret = [];
|
||||
for (let p in params) {
|
||||
ret.push(encodeURIComponent(p) + "=" + encodeURIComponent(params[p]));
|
||||
}
|
||||
window.open(
|
||||
"https://www.majorleaguecyber.org/events/new?" + ret.join("&"),
|
||||
"_blank"
|
||||
);
|
||||
},
|
||||
|
||||
submitSetup(e) {
|
||||
if (document.querySelector("#newsletter-checkbox").checked) {
|
||||
let email = e.target.querySelector("input[name=email]").value;
|
||||
let params = {
|
||||
email: email,
|
||||
b_38e27f7d496889133d2214208_d7c3ed71f9: "",
|
||||
c: "jsonp_callback_" + Math.round(10000 * Math.random()),
|
||||
};
|
||||
const ret = [];
|
||||
for (let p in params) {
|
||||
ret.push(encodeURIComponent(p) + "=" + encodeURIComponent(params[p]));
|
||||
}
|
||||
|
||||
var script = document.createElement("script");
|
||||
script.src =
|
||||
"https://newsletters.ctfd.io/lists/ot889gr1sa0e1/subscribe/post-json?" +
|
||||
ret.join("&");
|
||||
document.head.appendChild(script);
|
||||
}
|
||||
},
|
||||
}));
|
||||
|
||||
Alpine.start();
|
||||
7
assets/js/teams/list.js
Normal file
7
assets/js/teams/list.js
Normal file
@@ -0,0 +1,7 @@
|
||||
import Alpine from "alpinejs";
|
||||
import CTFd from "../index";
|
||||
|
||||
window.CTFd = CTFd;
|
||||
window.Alpine = Alpine;
|
||||
|
||||
Alpine.start();
|
||||
201
assets/js/teams/private.js
Normal file
201
assets/js/teams/private.js
Normal file
@@ -0,0 +1,201 @@
|
||||
import Alpine from "alpinejs";
|
||||
import CTFd from "../index";
|
||||
import { Modal } from "bootstrap";
|
||||
import { serializeJSON } from "@ctfdio/ctfd-js/forms";
|
||||
import { copyToClipboard } from "../utils/clipboard";
|
||||
import { colorHash } from "@ctfdio/ctfd-js/ui";
|
||||
import { getOption as getUserScoreOption } from "../utils/graphs/echarts/userscore";
|
||||
import { embed } from "../utils/graphs/echarts";
|
||||
|
||||
window.Alpine = Alpine;
|
||||
window.CTFd = CTFd;
|
||||
|
||||
Alpine.store("inviteToken", "");
|
||||
|
||||
Alpine.data("TeamEditModal", () => ({
|
||||
success: null,
|
||||
error: null,
|
||||
initial: null,
|
||||
errors: [],
|
||||
|
||||
init() {
|
||||
this.initial = serializeJSON(this.$el.querySelector("form"));
|
||||
},
|
||||
|
||||
async updateProfile() {
|
||||
let data = serializeJSON(this.$el, this.initial, true);
|
||||
|
||||
data.fields = [];
|
||||
|
||||
for (const property in data) {
|
||||
if (property.match(/fields\[\d+\]/)) {
|
||||
let field = {};
|
||||
let id = parseInt(property.slice(7, -1));
|
||||
field["field_id"] = id;
|
||||
field["value"] = data[property];
|
||||
data.fields.push(field);
|
||||
delete data[property];
|
||||
}
|
||||
}
|
||||
|
||||
let response = await CTFd.pages.teams.updateTeamSettings(data);
|
||||
if (response.success) {
|
||||
this.success = true;
|
||||
this.error = false;
|
||||
setTimeout(() => {
|
||||
this.success = null;
|
||||
this.error = null;
|
||||
}, 3000);
|
||||
} else {
|
||||
this.success = false;
|
||||
this.error = true;
|
||||
Object.keys(response.errors).map(error => {
|
||||
const error_msg = response.errors[error];
|
||||
this.errors.push(error_msg);
|
||||
});
|
||||
}
|
||||
},
|
||||
}));
|
||||
|
||||
Alpine.data("TeamCaptainModal", () => ({
|
||||
success: null,
|
||||
error: null,
|
||||
errors: [],
|
||||
|
||||
async updateCaptain() {
|
||||
let data = serializeJSON(this.$el, null, true);
|
||||
let response = await CTFd.pages.teams.updateTeamSettings(data);
|
||||
|
||||
if (response.success) {
|
||||
window.location.reload();
|
||||
} else {
|
||||
this.success = false;
|
||||
this.error = true;
|
||||
Object.keys(response.errors).map(error => {
|
||||
const error_msg = response.errors[error];
|
||||
this.errors.push(error_msg);
|
||||
});
|
||||
}
|
||||
},
|
||||
}));
|
||||
|
||||
Alpine.data("TeamInviteModal", () => ({
|
||||
copy() {
|
||||
copyToClipboard(this.$refs.link);
|
||||
},
|
||||
}));
|
||||
|
||||
Alpine.data("TeamDisbandModal", () => ({
|
||||
errors: [],
|
||||
|
||||
async disbandTeam() {
|
||||
let response = await CTFd.pages.teams.disbandTeam();
|
||||
|
||||
if (response.success) {
|
||||
window.location.reload();
|
||||
} else {
|
||||
this.errors = response.errors[""];
|
||||
}
|
||||
},
|
||||
}));
|
||||
|
||||
Alpine.data("CaptainMenu", () => ({
|
||||
captain: false,
|
||||
|
||||
editTeam() {
|
||||
this.teamEditModal = new Modal(document.getElementById("team-edit-modal"));
|
||||
this.teamEditModal.show();
|
||||
},
|
||||
|
||||
chooseCaptain() {
|
||||
this.teamCaptainModal = new Modal(document.getElementById("team-captain-modal"));
|
||||
this.teamCaptainModal.show();
|
||||
},
|
||||
|
||||
async inviteMembers() {
|
||||
const response = await CTFd.pages.teams.getInviteToken();
|
||||
|
||||
if (response.success) {
|
||||
const code = response.data.code;
|
||||
const url = `${window.location.origin}${CTFd.config.urlRoot}/teams/invite?code=${code}`;
|
||||
|
||||
document.querySelector("#team-invite-modal input[name=link]").value = url;
|
||||
this.$store.inviteToken = url;
|
||||
this.teamInviteModal = new Modal(document.getElementById("team-invite-modal"));
|
||||
this.teamInviteModal.show();
|
||||
}
|
||||
},
|
||||
|
||||
disbandTeam() {
|
||||
this.teamDisbandModal = new Modal(document.getElementById("team-disband-modal"));
|
||||
this.teamDisbandModal.show();
|
||||
},
|
||||
}));
|
||||
|
||||
Alpine.data("TeamGraphs", () => ({
|
||||
solves: null,
|
||||
fails: null,
|
||||
awards: null,
|
||||
solveCount: 0,
|
||||
failCount: 0,
|
||||
awardCount: 0,
|
||||
|
||||
getSolvePercentage() {
|
||||
return ((this.solveCount / (this.solveCount + this.failCount)) * 100).toFixed(2);
|
||||
},
|
||||
|
||||
getFailPercentage() {
|
||||
return ((this.failCount / (this.solveCount + this.failCount)) * 100).toFixed(2);
|
||||
},
|
||||
|
||||
getCategoryBreakdown() {
|
||||
const categories = [];
|
||||
const breakdown = {};
|
||||
|
||||
this.solves.data.map(solve => {
|
||||
categories.push(solve.challenge.category);
|
||||
});
|
||||
|
||||
categories.forEach(category => {
|
||||
if (category in breakdown) {
|
||||
breakdown[category] += 1;
|
||||
} else {
|
||||
breakdown[category] = 1;
|
||||
}
|
||||
});
|
||||
|
||||
const data = [];
|
||||
for (const property in breakdown) {
|
||||
data.push({
|
||||
name: property,
|
||||
count: breakdown[property],
|
||||
percent: (breakdown[property] / categories.length) * 100,
|
||||
color: colorHash(property),
|
||||
});
|
||||
}
|
||||
|
||||
return data;
|
||||
},
|
||||
|
||||
async init() {
|
||||
this.solves = await CTFd.pages.teams.teamSolves("me");
|
||||
this.fails = await CTFd.pages.teams.teamFails("me");
|
||||
this.awards = await CTFd.pages.teams.teamAwards("me");
|
||||
|
||||
this.solveCount = this.solves.meta.count;
|
||||
this.failCount = this.fails.meta.count;
|
||||
this.awardCount = this.awards.meta.count;
|
||||
|
||||
embed(
|
||||
this.$refs.scoregraph,
|
||||
getUserScoreOption(
|
||||
CTFd.team.id,
|
||||
CTFd.team.name,
|
||||
this.solves.data,
|
||||
this.awards.data
|
||||
)
|
||||
);
|
||||
},
|
||||
}));
|
||||
|
||||
Alpine.start();
|
||||
76
assets/js/teams/public.js
Normal file
76
assets/js/teams/public.js
Normal file
@@ -0,0 +1,76 @@
|
||||
import CTFd from "../index";
|
||||
|
||||
import Alpine from "alpinejs";
|
||||
import { colorHash } from "@ctfdio/ctfd-js/ui";
|
||||
import { getOption as getUserScoreOption } from "../utils/graphs/echarts/userscore";
|
||||
import { embed } from "../utils/graphs/echarts";
|
||||
|
||||
window.Alpine = Alpine;
|
||||
|
||||
Alpine.data("TeamGraphs", () => ({
|
||||
solves: null,
|
||||
fails: null,
|
||||
awards: null,
|
||||
solveCount: 0,
|
||||
failCount: 0,
|
||||
awardCount: 0,
|
||||
|
||||
getSolvePercentage() {
|
||||
return ((this.solveCount / (this.solveCount + this.failCount)) * 100).toFixed(2);
|
||||
},
|
||||
|
||||
getFailPercentage() {
|
||||
return ((this.failCount / (this.solveCount + this.failCount)) * 100).toFixed(2);
|
||||
},
|
||||
|
||||
getCategoryBreakdown() {
|
||||
const categories = [];
|
||||
const breakdown = {};
|
||||
|
||||
this.solves.data.map(solve => {
|
||||
categories.push(solve.challenge.category);
|
||||
});
|
||||
|
||||
categories.forEach(category => {
|
||||
if (category in breakdown) {
|
||||
breakdown[category] += 1;
|
||||
} else {
|
||||
breakdown[category] = 1;
|
||||
}
|
||||
});
|
||||
|
||||
const data = [];
|
||||
for (const property in breakdown) {
|
||||
data.push({
|
||||
name: property,
|
||||
count: breakdown[property],
|
||||
percent: ((breakdown[property] / categories.length) * 100).toFixed(2),
|
||||
color: colorHash(property),
|
||||
});
|
||||
}
|
||||
|
||||
return data;
|
||||
},
|
||||
|
||||
async init() {
|
||||
this.solves = await CTFd.pages.teams.teamSolves(window.TEAM.id);
|
||||
this.fails = await CTFd.pages.teams.teamFails(window.TEAM.id);
|
||||
this.awards = await CTFd.pages.teams.teamAwards(window.TEAM.id);
|
||||
|
||||
this.solveCount = this.solves.meta.count;
|
||||
this.failCount = this.fails.meta.count;
|
||||
this.awardCount = this.awards.meta.count;
|
||||
|
||||
embed(
|
||||
this.$refs.scoregraph,
|
||||
getUserScoreOption(
|
||||
window.TEAM.id,
|
||||
window.TEAM.name,
|
||||
this.solves.data,
|
||||
this.awards.data
|
||||
)
|
||||
);
|
||||
},
|
||||
}));
|
||||
|
||||
Alpine.start();
|
||||
12
assets/js/theme/highlight.js
Normal file
12
assets/js/theme/highlight.js
Normal file
@@ -0,0 +1,12 @@
|
||||
import CTFd from "../index";
|
||||
import lolight from "lolight";
|
||||
|
||||
export default () => {
|
||||
if (
|
||||
// default to true if config is not defined yet
|
||||
!CTFd.config.themeSettings.hasOwnProperty("use_builtin_code_highlighter") ||
|
||||
CTFd.config.themeSettings.use_builtin_code_highlighter === true
|
||||
) {
|
||||
lolight("pre code");
|
||||
}
|
||||
};
|
||||
27
assets/js/theme/styles.js
Normal file
27
assets/js/theme/styles.js
Normal file
@@ -0,0 +1,27 @@
|
||||
export default () => {
|
||||
document.querySelectorAll(".form-control").forEach($el => {
|
||||
$el.addEventListener("onfocus", () => {
|
||||
$el.classList.remove("input-filled-invalid");
|
||||
$el.classList.add("input-filled-valid");
|
||||
});
|
||||
|
||||
$el.addEventListener("onblur", () => {
|
||||
if ($el.nodeValue === "") {
|
||||
$el.classList.remove("input-filled-valid");
|
||||
$el.classList.remove("input-filled-invalid");
|
||||
}
|
||||
});
|
||||
|
||||
if ($el.nodeValue) {
|
||||
$el.classList.add("input-filled-valid");
|
||||
}
|
||||
});
|
||||
|
||||
document.querySelectorAll(".page-select").forEach($el => {
|
||||
if ($el.nodeValue) {
|
||||
const url = new URL(window.location);
|
||||
url.searchParams.set("page", $el.nodeValue);
|
||||
window.location.href = url.toString();
|
||||
}
|
||||
});
|
||||
};
|
||||
12
assets/js/theme/times.js
Normal file
12
assets/js/theme/times.js
Normal file
@@ -0,0 +1,12 @@
|
||||
import dayjs from "dayjs";
|
||||
import advancedFormat from "dayjs/plugin/advancedFormat";
|
||||
|
||||
dayjs.extend(advancedFormat);
|
||||
|
||||
export default () => {
|
||||
document.querySelectorAll("[data-time]").forEach($el => {
|
||||
const time = $el.getAttribute("data-time");
|
||||
const format = $el.getAttribute("data-time-format") || "MMMM Do, h:mm:ss A";
|
||||
$el.innerText = dayjs(time).format(format);
|
||||
});
|
||||
};
|
||||
7
assets/js/users/list.js
Normal file
7
assets/js/users/list.js
Normal file
@@ -0,0 +1,7 @@
|
||||
import Alpine from "alpinejs";
|
||||
import CTFd from "../index";
|
||||
|
||||
window.CTFd = CTFd;
|
||||
window.Alpine = Alpine;
|
||||
|
||||
Alpine.start();
|
||||
79
assets/js/users/private.js
Normal file
79
assets/js/users/private.js
Normal file
@@ -0,0 +1,79 @@
|
||||
import Alpine from "alpinejs";
|
||||
import CTFd from "../index";
|
||||
import { colorHash } from "@ctfdio/ctfd-js/ui";
|
||||
import { getOption as getUserScoreOption } from "../utils/graphs/echarts/userscore";
|
||||
import { embed } from "../utils/graphs/echarts";
|
||||
|
||||
window.Alpine = Alpine;
|
||||
|
||||
Alpine.data("UserGraphs", () => ({
|
||||
solves: null,
|
||||
fails: null,
|
||||
awards: null,
|
||||
solveCount: 0,
|
||||
failCount: 0,
|
||||
awardCount: 0,
|
||||
|
||||
getSolvePercentage() {
|
||||
return ((this.solveCount / (this.solveCount + this.failCount)) * 100).toFixed(2);
|
||||
},
|
||||
|
||||
getFailPercentage() {
|
||||
return ((this.failCount / (this.solveCount + this.failCount)) * 100).toFixed(2);
|
||||
},
|
||||
|
||||
getCategoryBreakdown() {
|
||||
const categories = [];
|
||||
const breakdown = {};
|
||||
|
||||
this.solves.data.map(solve => {
|
||||
categories.push(solve.challenge.category);
|
||||
});
|
||||
|
||||
categories.forEach(category => {
|
||||
if (category in breakdown) {
|
||||
breakdown[category] += 1;
|
||||
} else {
|
||||
breakdown[category] = 1;
|
||||
}
|
||||
});
|
||||
|
||||
const data = [];
|
||||
for (const property in breakdown) {
|
||||
const percent = Number((breakdown[property] / categories.length) * 100).toFixed(
|
||||
2
|
||||
);
|
||||
|
||||
data.push({
|
||||
name: property,
|
||||
count: breakdown[property],
|
||||
color: colorHash(property),
|
||||
percent,
|
||||
});
|
||||
}
|
||||
|
||||
return data;
|
||||
},
|
||||
|
||||
async init() {
|
||||
this.solves = await CTFd.pages.users.userSolves("me");
|
||||
this.fails = await CTFd.pages.users.userFails("me");
|
||||
this.awards = await CTFd.pages.users.userAwards("me");
|
||||
|
||||
this.solveCount = this.solves.meta.count;
|
||||
this.failCount = this.fails.meta.count;
|
||||
this.awardCount = this.awards.meta.count;
|
||||
|
||||
embed(
|
||||
this.$refs.scoregraph,
|
||||
getUserScoreOption(
|
||||
CTFd.user.id,
|
||||
CTFd.user.name,
|
||||
this.solves.data,
|
||||
this.awards.data
|
||||
)
|
||||
);
|
||||
},
|
||||
}));
|
||||
|
||||
Alpine.start();
|
||||
79
assets/js/users/public.js
Normal file
79
assets/js/users/public.js
Normal file
@@ -0,0 +1,79 @@
|
||||
import CTFd from "../index";
|
||||
|
||||
import Alpine from "alpinejs";
|
||||
import { colorHash } from "@ctfdio/ctfd-js/ui";
|
||||
import { getOption as getUserScoreOption } from "../utils/graphs/echarts/userscore";
|
||||
import { embed } from "../utils/graphs/echarts";
|
||||
|
||||
window.Alpine = Alpine;
|
||||
|
||||
Alpine.data("UserGraphs", () => ({
|
||||
solves: null,
|
||||
fails: null,
|
||||
awards: null,
|
||||
solveCount: 0,
|
||||
failCount: 0,
|
||||
awardCount: 0,
|
||||
|
||||
getSolvePercentage() {
|
||||
return ((this.solveCount / (this.solveCount + this.failCount)) * 100).toFixed(2);
|
||||
},
|
||||
|
||||
getFailPercentage() {
|
||||
return ((this.failCount / (this.solveCount + this.failCount)) * 100).toFixed(2);
|
||||
},
|
||||
|
||||
getCategoryBreakdown() {
|
||||
const categories = [];
|
||||
const breakdown = {};
|
||||
|
||||
this.solves.data.map(solve => {
|
||||
categories.push(solve.challenge.category);
|
||||
});
|
||||
|
||||
categories.forEach(category => {
|
||||
if (category in breakdown) {
|
||||
breakdown[category] += 1;
|
||||
} else {
|
||||
breakdown[category] = 1;
|
||||
}
|
||||
});
|
||||
|
||||
const data = [];
|
||||
for (const property in breakdown) {
|
||||
const percent = Number((breakdown[property] / categories.length) * 100).toFixed(
|
||||
2
|
||||
);
|
||||
data.push({
|
||||
name: property,
|
||||
count: breakdown[property],
|
||||
color: colorHash(property),
|
||||
percent,
|
||||
});
|
||||
}
|
||||
|
||||
return data;
|
||||
},
|
||||
|
||||
async init() {
|
||||
this.solves = await CTFd.pages.users.userSolves(window.USER.id);
|
||||
this.fails = await CTFd.pages.users.userFails(window.USER.id);
|
||||
this.awards = await CTFd.pages.users.userAwards(window.USER.id);
|
||||
|
||||
this.solveCount = this.solves.meta.count;
|
||||
this.failCount = this.fails.meta.count;
|
||||
this.awardCount = this.awards.meta.count;
|
||||
|
||||
embed(
|
||||
this.$refs.scoregraph,
|
||||
getUserScoreOption(
|
||||
window.USER.id,
|
||||
window.USER.name,
|
||||
this.solves.data,
|
||||
this.awards.data
|
||||
)
|
||||
);
|
||||
},
|
||||
}));
|
||||
|
||||
Alpine.start();
|
||||
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);
|
||||
});
|
||||
};
|
||||
20
assets/scss/includes/components/_challenge.scss
Normal file
20
assets/scss/includes/components/_challenge.scss
Normal file
@@ -0,0 +1,20 @@
|
||||
/* Challenge styles
|
||||
-------------------------------------------------- */
|
||||
|
||||
.challenge-desc {
|
||||
overflow-wrap: anywhere;
|
||||
|
||||
img {
|
||||
max-width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.challenge-button {
|
||||
box-shadow: 3px 3px 3px var(--bs-secondary);
|
||||
}
|
||||
|
||||
.challenge-solved {
|
||||
background-color: #37d63e !important;
|
||||
opacity: 0.6;
|
||||
border: none;
|
||||
}
|
||||
23
assets/scss/includes/components/_graphs.scss
Normal file
23
assets/scss/includes/components/_graphs.scss
Normal file
@@ -0,0 +1,23 @@
|
||||
/* Graph styles
|
||||
-------------------------------------------------- */
|
||||
|
||||
#score-graph {
|
||||
min-height: 400px;
|
||||
display: block;
|
||||
clear: both;
|
||||
}
|
||||
|
||||
#solves-graph {
|
||||
display: block;
|
||||
height: 350px;
|
||||
}
|
||||
|
||||
#keys-pie-graph {
|
||||
min-height: 400px;
|
||||
display: block;
|
||||
}
|
||||
|
||||
#categories-pie-graph {
|
||||
min-height: 400px;
|
||||
display: block;
|
||||
}
|
||||
29
assets/scss/includes/components/_jumbotron.scss
Normal file
29
assets/scss/includes/components/_jumbotron.scss
Normal file
@@ -0,0 +1,29 @@
|
||||
/* Jumbotron styles
|
||||
-------------------------------------------------- */
|
||||
|
||||
// Move down content because we have a fixed navbar that is 3.5rem tall
|
||||
main {
|
||||
padding-top: 3.5rem;
|
||||
}
|
||||
|
||||
.jumbotron {
|
||||
padding: 2rem 1rem;
|
||||
margin-bottom: 2rem;
|
||||
text-align: center;
|
||||
|
||||
background-color: var(--bs-dark);
|
||||
color: var(--bs-white);
|
||||
}
|
||||
|
||||
@media (min-width: 576px) {
|
||||
.jumbotron {
|
||||
padding: 4rem 2rem;
|
||||
}
|
||||
}
|
||||
|
||||
// @media (min-width: 768px) {
|
||||
// .jumbotron {
|
||||
// // I kind of like this, but it could be discussed
|
||||
// clip-path: polygon(0 0, 100% 0, 100% 90%, 0% 100%);
|
||||
// }
|
||||
// }
|
||||
31
assets/scss/includes/components/_sticky-footer.scss
Normal file
31
assets/scss/includes/components/_sticky-footer.scss
Normal file
@@ -0,0 +1,31 @@
|
||||
/* Sticky footer styles
|
||||
-------------------------------------------------- */
|
||||
html {
|
||||
position: relative;
|
||||
min-height: 100%;
|
||||
}
|
||||
|
||||
main {
|
||||
/* Margin bottom by footer height */
|
||||
margin-bottom: 100px;
|
||||
}
|
||||
|
||||
.footer {
|
||||
position: absolute;
|
||||
|
||||
/* prevent scrollbars from showing on pages that don't use the full page height */
|
||||
bottom: 1px;
|
||||
|
||||
width: 100%;
|
||||
|
||||
/* Set the fixed height of the footer here */
|
||||
height: 60px;
|
||||
|
||||
/* Vertically center the text there */
|
||||
line-height: 60px;
|
||||
|
||||
/* Avoid covering things */
|
||||
z-index: -20;
|
||||
|
||||
/*background-color: #f5f5f5;*/
|
||||
}
|
||||
33
assets/scss/includes/components/_table.scss
Normal file
33
assets/scss/includes/components/_table.scss
Normal file
@@ -0,0 +1,33 @@
|
||||
/* Table styles
|
||||
-------------------------------------------------- */
|
||||
|
||||
// Center text in tables, after the first cell
|
||||
// Override with text-center on cells
|
||||
.table tr > td:not(:first-of-type) {
|
||||
text-align: center !important;
|
||||
}
|
||||
|
||||
// Vertically center text in cells
|
||||
.table td,
|
||||
th {
|
||||
vertical-align: middle !important;
|
||||
}
|
||||
|
||||
// Add some default spacing
|
||||
.table > tbody > tr > td {
|
||||
padding: 0.8rem 1rem !important;
|
||||
}
|
||||
|
||||
// Remove border line from thead of all tables
|
||||
.table > tbody {
|
||||
border-top: none !important;
|
||||
}
|
||||
|
||||
// Add some padding from the edge of table
|
||||
.table > thead > tr > td:first-of-type {
|
||||
padding-left: 1rem !important;
|
||||
}
|
||||
|
||||
.table > thead > th {
|
||||
white-space: nowrap;
|
||||
}
|
||||
42
assets/scss/includes/icons/_award-icons.scss
Normal file
42
assets/scss/includes/icons/_award-icons.scss
Normal file
@@ -0,0 +1,42 @@
|
||||
.award-icon {
|
||||
font-family: "Font Awesome 6 Free";
|
||||
font-weight: 900;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
display: inline-block;
|
||||
font-style: normal;
|
||||
font-variant: normal;
|
||||
text-rendering: auto;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.award-shield:before {
|
||||
content: "\f3ed";
|
||||
}
|
||||
.award-bug:before {
|
||||
content: "\f188";
|
||||
}
|
||||
.award-crown:before {
|
||||
content: "\f521";
|
||||
}
|
||||
.award-crosshairs:before {
|
||||
content: "\f05b";
|
||||
}
|
||||
.award-ban:before {
|
||||
content: "\f05e";
|
||||
}
|
||||
.award-brain:before {
|
||||
content: "\f5dc";
|
||||
}
|
||||
.award-lightning:before {
|
||||
content: "\f0e7";
|
||||
}
|
||||
.award-code:before {
|
||||
content: "\f121";
|
||||
}
|
||||
.award-cowboy:before {
|
||||
content: "\f8c0";
|
||||
}
|
||||
.award-angry:before {
|
||||
content: "\f556";
|
||||
}
|
||||
511
assets/scss/includes/icons/_flag-icons.scss
Normal file
511
assets/scss/includes/icons/_flag-icons.scss
Normal file
@@ -0,0 +1,511 @@
|
||||
$flags: (
|
||||
ad: "🇦🇩",
|
||||
// Andorra
|
||||
ae: "🇦🇪",
|
||||
// United Arab Emirates
|
||||
af: "🇦🇫",
|
||||
// Afghanistan
|
||||
ag: "🇦🇬",
|
||||
// Antigua and Barbuda
|
||||
ai: "🇦🇮",
|
||||
// Anguilla
|
||||
al: "🇦🇱",
|
||||
// Albania
|
||||
am: "🇦🇲",
|
||||
// Armenia
|
||||
ao: "🇦🇴",
|
||||
// Angola
|
||||
aq: "🇦🇶",
|
||||
// Antarctica
|
||||
ar: "🇦🇷",
|
||||
// Argentina
|
||||
as: "🇦🇸",
|
||||
// American Samoa
|
||||
at: "🇦🇹",
|
||||
// Austria
|
||||
au: "🇦🇺",
|
||||
// Australia
|
||||
aw: "🇦🇼",
|
||||
// Aruba
|
||||
ax: "🇦🇽",
|
||||
// Åland Islands
|
||||
az: "🇦🇿",
|
||||
// Azerbaijan
|
||||
ba: "🇧🇦",
|
||||
// Bosnia and Herzegovina
|
||||
bb: "🇧🇧",
|
||||
// Barbados
|
||||
bd: "🇧🇩",
|
||||
// Bangladesh
|
||||
be: "🇧🇪",
|
||||
// Belgium
|
||||
bf: "🇧🇫",
|
||||
// Burkina Faso
|
||||
bg: "🇧🇬",
|
||||
// Bulgaria
|
||||
bh: "🇧🇭",
|
||||
// Bahrain
|
||||
bi: "🇧🇮",
|
||||
// Burundi
|
||||
bj: "🇧🇯",
|
||||
// Benin
|
||||
bl: "🇧🇱",
|
||||
// Saint Barthélemy
|
||||
bm: "🇧🇲",
|
||||
// Bermuda
|
||||
bn: "🇧🇳",
|
||||
// Brunei Darussalam
|
||||
bo: "🇧🇴",
|
||||
// Bolivia
|
||||
bq: "🇧🇶",
|
||||
// Bonaire, Sint Eustatius and Saba
|
||||
br: "🇧🇷",
|
||||
// Brazil
|
||||
bs: "🇧🇸",
|
||||
// Bahamas
|
||||
bt: "🇧🇹",
|
||||
// Bhutan
|
||||
bv: "🇧🇻",
|
||||
// Bouvet Island
|
||||
bw: "🇧🇼",
|
||||
// Botswana
|
||||
by: "🇧🇾",
|
||||
// Belarus
|
||||
bz: "🇧🇿",
|
||||
// Belize
|
||||
ca: "🇨🇦",
|
||||
// Canada
|
||||
cc: "🇨🇨",
|
||||
// Cocos (Keeling) Islands
|
||||
cd: "🇨🇩",
|
||||
// Congo
|
||||
cf: "🇨🇫",
|
||||
// Central African Republic
|
||||
cg: "🇨🇬",
|
||||
// Congo
|
||||
ch: "🇨🇭",
|
||||
// Switzerland
|
||||
ci: "🇨🇮",
|
||||
// Côte D'Ivoire
|
||||
ck: "🇨🇰",
|
||||
// Cook Islands
|
||||
cl: "🇨🇱",
|
||||
// Chile
|
||||
cm: "🇨🇲",
|
||||
// Cameroon
|
||||
cn: "🇨🇳",
|
||||
// China
|
||||
co: "🇨🇴",
|
||||
// Colombia
|
||||
cr: "🇨🇷",
|
||||
// Costa Rica
|
||||
cu: "🇨🇺",
|
||||
// Cuba
|
||||
cv: "🇨🇻",
|
||||
// Cape Verde
|
||||
cw: "🇨🇼",
|
||||
// Curaçao
|
||||
cx: "🇨🇽",
|
||||
// Christmas Island
|
||||
cy: "🇨🇾",
|
||||
// Cyprus
|
||||
cz: "🇨🇿",
|
||||
// Czech Republic
|
||||
de: "🇩🇪",
|
||||
// Germany
|
||||
dj: "🇩🇯",
|
||||
// Djibouti
|
||||
dk: "🇩🇰",
|
||||
// Denmark
|
||||
dm: "🇩🇲",
|
||||
// Dominica
|
||||
do: "🇩🇴",
|
||||
// Dominican Republic
|
||||
dz: "🇩🇿",
|
||||
// Algeria
|
||||
ec: "🇪🇨",
|
||||
// Ecuador
|
||||
ee: "🇪🇪",
|
||||
// Estonia
|
||||
eg: "🇪🇬",
|
||||
// Egypt
|
||||
eh: "🇪🇭",
|
||||
// Western Sahara
|
||||
er: "🇪🇷",
|
||||
// Eritrea
|
||||
es: "🇪🇸",
|
||||
// Spain
|
||||
et: "🇪🇹",
|
||||
// Ethiopia
|
||||
fi: "🇫🇮",
|
||||
// Finland
|
||||
fj: "🇫🇯",
|
||||
// Fiji
|
||||
fk: "🇫🇰",
|
||||
// Falkland Islands (Malvinas)
|
||||
fm: "🇫🇲",
|
||||
// Micronesia
|
||||
fo: "🇫🇴",
|
||||
// Faroe Islands
|
||||
fr: "🇫🇷",
|
||||
// France
|
||||
ga: "🇬🇦",
|
||||
// Gabon
|
||||
gb: "🇬🇧",
|
||||
// United Kingdom
|
||||
gd: "🇬🇩",
|
||||
// Grenada
|
||||
ge: "🇬🇪",
|
||||
// Georgia
|
||||
gf: "🇬🇫",
|
||||
// French Guiana
|
||||
gg: "🇬🇬",
|
||||
// Guernsey
|
||||
gh: "🇬🇭",
|
||||
// Ghana
|
||||
gi: "🇬🇮",
|
||||
// Gibraltar
|
||||
gl: "🇬🇱",
|
||||
// Greenland
|
||||
gm: "🇬🇲",
|
||||
// Gambia
|
||||
gn: "🇬🇳",
|
||||
// Guinea
|
||||
gp: "🇬🇵",
|
||||
// Guadeloupe
|
||||
gq: "🇬🇶",
|
||||
// Equatorial Guinea
|
||||
gr: "🇬🇷",
|
||||
// Greece
|
||||
gs: "🇬🇸",
|
||||
// South Georgia
|
||||
gt: "🇬🇹",
|
||||
// Guatemala
|
||||
gu: "🇬🇺",
|
||||
// Guam
|
||||
gw: "🇬🇼",
|
||||
// Guinea-Bissau
|
||||
gy: "🇬🇾",
|
||||
// Guyana
|
||||
hk: "🇭🇰",
|
||||
// Hong Kong
|
||||
hm: "🇭🇲",
|
||||
// Heard Island and Mcdonald Islands
|
||||
hn: "🇭🇳",
|
||||
// Honduras
|
||||
hr: "🇭🇷",
|
||||
// Croatia
|
||||
ht: "🇭🇹",
|
||||
// Haiti
|
||||
hu: "🇭🇺",
|
||||
// Hungary
|
||||
id: "🇮🇩",
|
||||
// Indonesia
|
||||
ie: "🇮🇪",
|
||||
// Ireland
|
||||
il: "🇮🇱",
|
||||
// Israel
|
||||
im: "🇮🇲",
|
||||
// Isle of Man
|
||||
in: "🇮🇳",
|
||||
// India
|
||||
io: "🇮🇴",
|
||||
// British Indian Ocean Territory
|
||||
iq: "🇮🇶",
|
||||
// Iraq
|
||||
ir: "🇮🇷",
|
||||
// Iran
|
||||
is: "🇮🇸",
|
||||
// Iceland
|
||||
it: "🇮🇹",
|
||||
// Italy
|
||||
je: "🇯🇪",
|
||||
// Jersey
|
||||
jm: "🇯🇲",
|
||||
// Jamaica
|
||||
jo: "🇯🇴",
|
||||
// Jordan
|
||||
jp: "🇯🇵",
|
||||
// Japan
|
||||
ke: "🇰🇪",
|
||||
// Kenya
|
||||
kg: "🇰🇬",
|
||||
// Kyrgyzstan
|
||||
kh: "🇰🇭",
|
||||
// Cambodia
|
||||
ki: "🇰🇮",
|
||||
// Kiribati
|
||||
km: "🇰🇲",
|
||||
// Comoros
|
||||
kn: "🇰🇳",
|
||||
// Saint Kitts and Nevis
|
||||
kp: "🇰🇵",
|
||||
// North Korea
|
||||
kr: "🇰🇷",
|
||||
// South Korea
|
||||
kw: "🇰🇼",
|
||||
// Kuwait
|
||||
ky: "🇰🇾",
|
||||
// Cayman Islands
|
||||
kz: "🇰🇿",
|
||||
// Kazakhstan
|
||||
la: "🇱🇦",
|
||||
// Lao People's Democratic Republic
|
||||
lb: "🇱🇧",
|
||||
// Lebanon
|
||||
lc: "🇱🇨",
|
||||
// Saint Lucia
|
||||
li: "🇱🇮",
|
||||
// Liechtenstein
|
||||
lk: "🇱🇰",
|
||||
// Sri Lanka
|
||||
lr: "🇱🇷",
|
||||
// Liberia
|
||||
ls: "🇱🇸",
|
||||
// Lesotho
|
||||
lt: "🇱🇹",
|
||||
// Lithuania
|
||||
lu: "🇱🇺",
|
||||
// Luxembourg
|
||||
lv: "🇱🇻",
|
||||
// Latvia
|
||||
ly: "🇱🇾",
|
||||
// Libya
|
||||
ma: "🇲🇦",
|
||||
// Morocco
|
||||
mc: "🇲🇨",
|
||||
// Monaco
|
||||
md: "🇲🇩",
|
||||
// Moldova
|
||||
me: "🇲🇪",
|
||||
// Montenegro
|
||||
mf: "🇲🇫",
|
||||
// Saint Martin (French Part)
|
||||
mg: "🇲🇬",
|
||||
// Madagascar
|
||||
mh: "🇲🇭",
|
||||
// Marshall Islands
|
||||
mk: "🇲🇰",
|
||||
// Macedonia
|
||||
ml: "🇲🇱",
|
||||
// Mali
|
||||
mm: "🇲🇲",
|
||||
// Myanmar
|
||||
mn: "🇲🇳",
|
||||
// Mongolia
|
||||
mo: "🇲🇴",
|
||||
// Macao
|
||||
mp: "🇲🇵",
|
||||
// Northern Mariana Islands
|
||||
mq: "🇲🇶",
|
||||
// Martinique
|
||||
mr: "🇲🇷",
|
||||
// Mauritania
|
||||
ms: "🇲🇸",
|
||||
// Montserrat
|
||||
mt: "🇲🇹",
|
||||
// Malta
|
||||
mu: "🇲🇺",
|
||||
// Mauritius
|
||||
mv: "🇲🇻",
|
||||
// Maldives
|
||||
mw: "🇲🇼",
|
||||
// Malawi
|
||||
mx: "🇲🇽",
|
||||
// Mexico
|
||||
my: "🇲🇾",
|
||||
// Malaysia
|
||||
mz: "🇲🇿",
|
||||
// Mozambique
|
||||
na: "🇳🇦",
|
||||
// Namibia
|
||||
nc: "🇳🇨",
|
||||
// New Caledonia
|
||||
ne: "🇳🇪",
|
||||
// Niger
|
||||
nf: "🇳🇫",
|
||||
// Norfolk Island
|
||||
ng: "🇳🇬",
|
||||
// Nigeria
|
||||
ni: "🇳🇮",
|
||||
// Nicaragua
|
||||
nl: "🇳🇱",
|
||||
// Netherlands
|
||||
no: "🇳🇴",
|
||||
// Norway
|
||||
np: "🇳🇵",
|
||||
// Nepal
|
||||
nr: "🇳🇷",
|
||||
// Nauru
|
||||
nu: "🇳🇺",
|
||||
// Niue
|
||||
nz: "🇳🇿",
|
||||
// New Zealand
|
||||
om: "🇴🇲",
|
||||
// Oman
|
||||
pa: "🇵🇦",
|
||||
// Panama
|
||||
pe: "🇵🇪",
|
||||
// Peru
|
||||
pf: "🇵🇫",
|
||||
// French Polynesia
|
||||
pg: "🇵🇬",
|
||||
// Papua New Guinea
|
||||
ph: "🇵🇭",
|
||||
// Philippines
|
||||
pk: "🇵🇰",
|
||||
// Pakistan
|
||||
pl: "🇵🇱",
|
||||
// Poland
|
||||
pm: "🇵🇲",
|
||||
// Saint Pierre and Miquelon
|
||||
pn: "🇵🇳",
|
||||
// Pitcairn
|
||||
pr: "🇵🇷",
|
||||
// Puerto Rico
|
||||
ps: "🇵🇸",
|
||||
// Palestinian Territory
|
||||
pt: "🇵🇹",
|
||||
// Portugal
|
||||
pw: "🇵🇼",
|
||||
// Palau
|
||||
py: "🇵🇾",
|
||||
// Paraguay
|
||||
qa: "🇶🇦",
|
||||
// Qatar
|
||||
re: "🇷🇪",
|
||||
// Réunion
|
||||
ro: "🇷🇴",
|
||||
// Romania
|
||||
rs: "🇷🇸",
|
||||
// Serbia
|
||||
ru: "🇷🇺",
|
||||
// Russia
|
||||
rw: "🇷🇼",
|
||||
// Rwanda
|
||||
sa: "🇸🇦",
|
||||
// Saudi Arabia
|
||||
sb: "🇸🇧",
|
||||
// Solomon Islands
|
||||
sc: "🇸🇨",
|
||||
// Seychelles
|
||||
sd: "🇸🇩",
|
||||
// Sudan
|
||||
se: "🇸🇪",
|
||||
// Sweden
|
||||
sg: "🇸🇬",
|
||||
// Singapore
|
||||
sh: "🇸🇭",
|
||||
// Saint Helena, Ascension and Tristan Da Cunha
|
||||
si: "🇸🇮",
|
||||
// Slovenia
|
||||
sj: "🇸🇯",
|
||||
// Svalbard and Jan Mayen
|
||||
sk: "🇸🇰",
|
||||
// Slovakia
|
||||
sl: "🇸🇱",
|
||||
// Sierra Leone
|
||||
sm: "🇸🇲",
|
||||
// San Marino
|
||||
sn: "🇸🇳",
|
||||
// Senegal
|
||||
so: "🇸🇴",
|
||||
// Somalia
|
||||
sr: "🇸🇷",
|
||||
// Suriname
|
||||
ss: "🇸🇸",
|
||||
// South Sudan
|
||||
st: "🇸🇹",
|
||||
// Sao Tome and Principe
|
||||
sv: "🇸🇻",
|
||||
// El Salvador
|
||||
sx: "🇸🇽",
|
||||
// Sint Maarten (Dutch Part)
|
||||
sy: "🇸🇾",
|
||||
// Syrian Arab Republic
|
||||
sz: "🇸🇿",
|
||||
// Swaziland
|
||||
tc: "🇹🇨",
|
||||
// Turks and Caicos Islands
|
||||
td: "🇹🇩",
|
||||
// Chad
|
||||
tf: "🇹🇫",
|
||||
// French Southern Territories
|
||||
tg: "🇹🇬",
|
||||
// Togo
|
||||
th: "🇹🇭",
|
||||
// Thailand
|
||||
tj: "🇹🇯",
|
||||
// Tajikistan
|
||||
tk: "🇹🇰",
|
||||
// Tokelau
|
||||
tl: "🇹🇱",
|
||||
// Timor-Leste
|
||||
tm: "🇹🇲",
|
||||
// Turkmenistan
|
||||
tn: "🇹🇳",
|
||||
// Tunisia
|
||||
to: "🇹🇴",
|
||||
// Tonga
|
||||
tr: "🇹🇷",
|
||||
// Turkey
|
||||
tt: "🇹🇹",
|
||||
// Trinidad and Tobago
|
||||
tv: "🇹🇻",
|
||||
// Tuvalu
|
||||
tw: "🇹🇼",
|
||||
// Taiwan
|
||||
tz: "🇹🇿",
|
||||
// Tanzania
|
||||
ua: "🇺🇦",
|
||||
// Ukraine
|
||||
ug: "🇺🇬",
|
||||
// Uganda
|
||||
um: "🇺🇲",
|
||||
// United States Minor Outlying Islands
|
||||
us: "🇺🇸",
|
||||
// United States
|
||||
uy: "🇺🇾",
|
||||
// Uruguay
|
||||
uz: "🇺🇿",
|
||||
// Uzbekistan
|
||||
va: "🇻🇦",
|
||||
// Vatican City
|
||||
vc: "🇻🇨",
|
||||
// Saint Vincent and The Grenadines
|
||||
ve: "🇻🇪",
|
||||
// Venezuela
|
||||
vg: "🇻🇬",
|
||||
// Virgin Islands, British
|
||||
vi: "🇻🇮",
|
||||
// Virgin Islands, U.S.
|
||||
vn: "🇻🇳",
|
||||
// Viet Nam
|
||||
vu: "🇻🇺",
|
||||
// Vanuatu
|
||||
wf: "🇼🇫",
|
||||
// Wallis and Futuna
|
||||
ws: "🇼🇸",
|
||||
// Samoa
|
||||
ye: "🇾🇪",
|
||||
// Yemen
|
||||
yt: "🇾🇹",
|
||||
// Mayotte
|
||||
za: "🇿🇦",
|
||||
// South Africa
|
||||
zm: "🇿🇲",
|
||||
// Zambia
|
||||
zw: "🇿🇼",
|
||||
// Zimbabwe
|
||||
);
|
||||
|
||||
[class^="flag-"] {
|
||||
font-style: normal;
|
||||
}
|
||||
|
||||
// generate classes
|
||||
@each $name, $icon in $flags {
|
||||
.flag-#{$name}:before {
|
||||
content: $icon;
|
||||
}
|
||||
}
|
||||
7
assets/scss/includes/utils/_cursors.scss
Normal file
7
assets/scss/includes/utils/_cursors.scss
Normal file
@@ -0,0 +1,7 @@
|
||||
.cursor-pointer {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.cursor-help {
|
||||
cursor: help;
|
||||
}
|
||||
34
assets/scss/includes/utils/_fonts.scss
Normal file
34
assets/scss/includes/utils/_fonts.scss
Normal file
@@ -0,0 +1,34 @@
|
||||
/* Handle offline font loading */
|
||||
// $fa-font-path: "../../../node_modules/@fortawesome/fontawesome-free/webfonts" !default;
|
||||
// $fa-font-display: auto !default;
|
||||
// @import "../../../node_modules/@fortawesome/fontawesome-free/scss/fontawesome.scss";
|
||||
// @import "../../../node_modules/@fortawesome/fontawesome-free/scss/regular.scss";
|
||||
// @import "../../../node_modules/@fortawesome/fontawesome-free/scss/solid.scss";
|
||||
// @import "../../../node_modules/@fortawesome/fontawesome-free/scss/brands.scss";
|
||||
|
||||
@use "~/@fontsource/lato/scss/mixins" as Lato;
|
||||
@use "~/@fontsource/raleway/scss/mixins" as Raleway;
|
||||
|
||||
// Include both normal and bold weights
|
||||
@include Lato.fontFace($fontDir: "../webfonts", $weight: 400);
|
||||
@include Lato.fontFace($fontDir: "../webfonts", $weight: 700);
|
||||
|
||||
@include Raleway.fontFace($fontDir: "../webfonts");
|
||||
|
||||
$fa-font-path: "../webfonts";
|
||||
@import "~/@fortawesome/fontawesome-free/scss/fontawesome.scss";
|
||||
@import "~/@fortawesome/fontawesome-free/scss/solid.scss";
|
||||
@import "~/@fortawesome/fontawesome-free/scss/brands.scss";
|
||||
|
||||
// @import "~/@fontsource/lato/index.css";
|
||||
// @import "~/@fontsource/lato/index.css";
|
||||
|
||||
html,
|
||||
body,
|
||||
.container {
|
||||
font-family: "Lato", sans-serif;
|
||||
}
|
||||
|
||||
.jumbotron .container {
|
||||
font-family: "Raleway", sans-serif;
|
||||
}
|
||||
30
assets/scss/includes/utils/_lolight.scss
Normal file
30
assets/scss/includes/utils/_lolight.scss
Normal file
@@ -0,0 +1,30 @@
|
||||
.ll-nam {
|
||||
/* words */
|
||||
color: #24292f;
|
||||
}
|
||||
.ll-num {
|
||||
/* numbers */
|
||||
color: #005cc5;
|
||||
}
|
||||
.ll-str {
|
||||
/* strings */
|
||||
color: #0a3069;
|
||||
}
|
||||
.ll-rex {
|
||||
/* regular expressions */
|
||||
color: #032f62;
|
||||
}
|
||||
.ll-pct {
|
||||
/* operators, punctation */
|
||||
color: #0550ae;
|
||||
}
|
||||
.ll-key {
|
||||
/* keywords */
|
||||
color: #d73a49;
|
||||
font-weight: normal;
|
||||
}
|
||||
.ll-com {
|
||||
/* comments */
|
||||
color: #6a737d;
|
||||
font-style: italic;
|
||||
}
|
||||
15
assets/scss/includes/utils/_min-height.scss
Normal file
15
assets/scss/includes/utils/_min-height.scss
Normal file
@@ -0,0 +1,15 @@
|
||||
.min-vh-0 {
|
||||
min-height: 0vh !important;
|
||||
}
|
||||
.min-vh-25 {
|
||||
min-height: 25vh !important;
|
||||
}
|
||||
.min-vh-50 {
|
||||
min-height: 50vh !important;
|
||||
}
|
||||
.min-vh-75 {
|
||||
min-height: 75vh !important;
|
||||
}
|
||||
.min-vh-100 {
|
||||
min-height: 100vh !important;
|
||||
}
|
||||
15
assets/scss/includes/utils/_opacity.scss
Normal file
15
assets/scss/includes/utils/_opacity.scss
Normal file
@@ -0,0 +1,15 @@
|
||||
.opacity-0 {
|
||||
opacity: 0 !important;
|
||||
}
|
||||
.opacity-25 {
|
||||
opacity: 0.25 !important;
|
||||
}
|
||||
.opacity-50 {
|
||||
opacity: 0.5 !important;
|
||||
}
|
||||
.opacity-75 {
|
||||
opacity: 0.75 !important;
|
||||
}
|
||||
.opacity-100 {
|
||||
opacity: 1 !important;
|
||||
}
|
||||
85
assets/scss/main.scss
Normal file
85
assets/scss/main.scss
Normal file
@@ -0,0 +1,85 @@
|
||||
@use "bootstrap/scss/bootstrap" as * with (
|
||||
$info: #5c728f
|
||||
);
|
||||
|
||||
@use "includes/components/table";
|
||||
@use "includes/components/jumbotron";
|
||||
@use "includes/components/challenge";
|
||||
@use "includes/components/sticky-footer";
|
||||
@use "includes/components/graphs";
|
||||
|
||||
@use "includes/utils/fonts";
|
||||
@use "includes/utils/opacity";
|
||||
@use "includes/utils/min-height";
|
||||
@use "includes/utils/cursors";
|
||||
@use "includes/utils/lolight";
|
||||
|
||||
@use "includes/icons/award-icons";
|
||||
@use "includes/icons/flag-icons";
|
||||
|
||||
h1,
|
||||
h2 {
|
||||
font-weight: 500;
|
||||
letter-spacing: 2px;
|
||||
}
|
||||
|
||||
a {
|
||||
color: #337ab7;
|
||||
text-decoration: none !important;
|
||||
}
|
||||
|
||||
blockquote {
|
||||
border-left: 4px solid $secondary;
|
||||
padding-left: 15px;
|
||||
}
|
||||
|
||||
.fa-spin.spinner {
|
||||
text-align: center;
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.badge-notification {
|
||||
vertical-align: top;
|
||||
margin-left: -1.5em;
|
||||
font-size: 50%;
|
||||
}
|
||||
|
||||
select.form-control {
|
||||
height: auto !important;
|
||||
}
|
||||
|
||||
.form-select {
|
||||
background: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 4 5'%3e%3cpath fill='%23343a40' d='M2 0L0 2h4zm0 5L0 3h4z'/%3e%3c/svg%3e")
|
||||
no-repeat right 0.75rem center/8px 10px !important;
|
||||
}
|
||||
|
||||
// HMM
|
||||
|
||||
.input-filled-valid {
|
||||
background-color: transparent !important;
|
||||
border-color: #a3d39c;
|
||||
box-shadow: 0 0 0 0.1rem #a3d39c;
|
||||
transition: background-color 0.3s, border-color 0.3s;
|
||||
}
|
||||
|
||||
.input-filled-invalid {
|
||||
background-color: transparent !important;
|
||||
border-color: #d46767;
|
||||
box-shadow: 0 0 0 0.1rem #d46767;
|
||||
transition: background-color 0.3s, border-color 0.3s;
|
||||
}
|
||||
|
||||
.form-control {
|
||||
padding: 0.8em !important;
|
||||
background: #f0f0f0;
|
||||
color: #aaa;
|
||||
/* Behavior changed in Bootstrap v4.1.3. See https://github.com/twbs/bootstrap/issues/27629 */
|
||||
height: auto !important;
|
||||
}
|
||||
|
||||
.form-control:focus {
|
||||
background-color: transparent;
|
||||
border-color: #a3d39c;
|
||||
box-shadow: 0 0 0 0.1rem #a3d39c;
|
||||
transition: background-color 0.3s, border-color 0.3s;
|
||||
}
|
||||
Reference in New Issue
Block a user