Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
da9f128
feat: Enable Vue Router for history and permalinks
google-labs-jules[bot] Nov 24, 2025
cb6253e
Merge pull request #2 from datalets/feat/vue-router-history-permalinks
loleg Nov 25, 2025
79f04e6
JS libraries
loleg Dec 5, 2025
f3b4910
Yarn upgrade
loleg Dec 8, 2025
6274bd1
Merge branch 'main' of github.com:datalets/backboard
loleg Mar 7, 2026
e3f3579
Yarn upgrade
loleg Mar 7, 2026
22a1dab
Yarn upgrade
loleg May 3, 2026
025368c
🔒 Add sandbox attribute to iframes to mitigate security risks
google-labs-jules[bot] May 8, 2026
fe297a2
feat: make timer length configurable via environment variable
google-labs-jules[bot] May 8, 2026
95c1568
⚡ Optimize activities assignment loop in Challenges.vue
google-labs-jules[bot] May 8, 2026
de2c2f9
Merge pull request #7 from datalets/perf-optimize-challenges-loop-183…
loleg May 26, 2026
61b4dcb
Merge pull request #5 from datalets/fix-security-vulnerability-iframe…
loleg May 26, 2026
a0fdf2c
Update src/components/Previews.vue
loleg May 26, 2026
5c4f001
Merge pull request #6 from datalets/feat-configurable-timer-length-39…
loleg May 26, 2026
7d4af40
Remove onMounted in App.vue
loleg May 26, 2026
80a8c31
perf: optimize project and activity processing in Challenges.vue
google-labs-jules[bot] May 26, 2026
cc8dc57
fix: address XSS vulnerability in iframe src
google-labs-jules[bot] May 26, 2026
f75e031
Merge branch 'main' into fix-xss-iframe-src-7443788764402903990
loleg May 26, 2026
b3bf7ef
Merge pull request #9 from datalets/fix-xss-iframe-src-74437887644029…
loleg May 26, 2026
c7d1321
Merge branch 'main' into perf-optimize-challenges-processing-15405012…
loleg May 26, 2026
43830a6
Merge pull request #8 from datalets/perf-optimize-challenges-processi…
loleg May 26, 2026
3e0bcb6
Event display cleanup
loleg May 26, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
"private": true,
"type": "module",
"engines": {
"node": "^20.19.0 || >=22.12.0"
"node": "^24 || >=22.12.0"
},
"scripts": {
"dev": "vite",
Expand Down
13 changes: 5 additions & 8 deletions src/App.vue
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<template>
<div id="app" :class="darkClass">
<Challenges
<router-view
@closeToolbar="toggleOptions"
@previewOff="previewOff"
@previewOn="previewOn"
Expand All @@ -22,16 +22,13 @@
</template>

<script>
import { ref, onMounted } from "vue"
import Challenges from "./components/Challenges.vue"
import { ref } from "vue"

export default {
name: "App",
components: {
Challenges,
},
components: {},
setup() {
const dribdatApi = ref(null);
const dribdatApi = ref("/datapackage.json");
const dribdatHome = ref("#top");
const dribdatDribs = ref("");
const voteUrl = ref("");
Expand Down Expand Up @@ -82,7 +79,7 @@ export default {
baseUrl = baseUrl.substring(0, baseUrl.indexOf("/event/"));
}
} else if (baseUrl == null) {
baseUrl = "./datapackage.json";
baseUrl = "/datapackage.json";
}
if (baseUrl.endsWith("datapackage.json")) {
apiUrl = baseUrl;
Expand Down
46 changes: 29 additions & 17 deletions src/components/Challenges.vue
Original file line number Diff line number Diff line change
Expand Up @@ -172,7 +172,8 @@
</template>

<script>
import { ref, onMounted, computed } from "vue";
import { ref, onMounted, computed, watch } from "vue";
import { useRouter } from "vue-router";
import moment from "moment";
import Header from "./Header.vue";
import Footer from "./Footer.vue";
Expand All @@ -187,6 +188,7 @@ export default {
dribs: String,
options: String,
toolbar: Boolean,
id: String,
},
components: {
Countdown,
Expand All @@ -196,6 +198,7 @@ export default {
Footer,
},
setup(props, { emit }) {
const router = useRouter();
const event = ref({});
const projects = ref(null);
const activities = ref(null);
Expand Down Expand Up @@ -246,7 +249,7 @@ export default {
};

const seeDetails = (project) => {
window.open(project.url);
router.push({ name: 'Project', params: { id: project.id } });
};

const changeDark = () => {
Expand Down Expand Up @@ -379,16 +382,17 @@ export default {
p.date = moment(p.updated_at).format("MMM Do, YYYY");
p.image_url =
typeof p.image_url === "undefined" ? null : p.image_url;
/*
// Use the event iage if no logo is available
if (!p.image_url && data.event && data.event.logo_url) {
p.image_url = data.event.logo_url;
}
*/
p.team =
typeof p.team === "string"
? p.team.replaceAll(",", " ").replaceAll(" ", " ").split(" ")
: p.team;
projects.value.push(p);
});
projects.value.forEach((p) => {
// Statistics and scores
p.statistics = "";
if (p.stats) {
p.stats["bytes pitch"] = p.stats["sizepitch"];
Expand All @@ -401,6 +405,7 @@ export default {
p.stats["score"] = p.score;
}
p.score = Math.min(p.score, 100);
projects.value.push(p);
});
if (data.title) {
document.title = data.title;
Expand All @@ -424,19 +429,15 @@ export default {
activities.value = data.activities.sort((a, b) => {
return a.time < b.time;
});
const projectMap = new Map();
projects.value.forEach((p) => projectMap.set(String(p.id), p));
activities.value.forEach((el) => {
let proj = projects.value.filter((p) => {
if (p.id == el.project_id) {
if (typeof p.activities === "undefined") {
p.activities = [];
}
p.activities.push(el);
return true;
const p = projectMap.get(String(el.project_id));
if (p) {
if (typeof p.activities === "undefined") {
p.activities = [];
}
return false;
});
if (!proj) {
return;
p.activities.push(el);
}
});
})
Expand All @@ -449,13 +450,24 @@ export default {
if (err.message.indexOf('not valid JSON')) {
errorMessage.value = 'Could not load valid hackathon metadata.'
console.warn(err.message);
console.log(datasrc);
console.debug(datasrc);
} else {
errorMessage.value = err;
}
});
});

watch(projects, (newProjects) => {
if (props.id && newProjects && newProjects.length > 0) {
const project = newProjects.find(p => p.id == props.id);
if (project) {
activePreview.value = project.id;
isPreviews.value = true;
emit("previewOn");
}
}
});

return {
event,
projects,
Expand Down
5 changes: 2 additions & 3 deletions src/components/Countdown.vue
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ export default {
setTimeout(() => {
const evt = this.event;
if (!evt) return;
console.log(evt);
//console.log(evt);
this.deadline =
(!evt.has_started &&
evt.starts_at
Expand All @@ -40,8 +40,7 @@ export default {
) || (!evt.has_finished &&
evt.ends_at
? evt.ends_at.replace("T", " ")
: null
);
: null);
this.timespan = evt.starts_at && evt.ends_at
? evt.starts_at + " → " + evt.ends_at
: "";
Expand Down
15 changes: 15 additions & 0 deletions src/components/Footer.vue
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,12 @@
:html="true"
/>
</div>
<p class="event-summary" v-if="event.summary">
{{ event.summary }}
<b class="event-hashtag" v-if="event.hashtag">
{{ event.hashtag }}
</b>
</p>
</div>
</template>

Expand Down Expand Up @@ -64,4 +70,13 @@ export default {
margin: 1em;
}
}

.event-summary {
font-size: 90%;
font-style: italic;
text-align: center;
margin-top: 3em;
display: block;
opacity: 0.7;
}
</style>
49 changes: 16 additions & 33 deletions src/components/Header.vue
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
<div class="section-header">
<div class="header-logo" v-if="event.logo_url">
<a :href="event.webpage" :title="event.name" target="_blank">
<img id="event-logo" :src="event.logo_url" />
<img alt="Logo" :src="event.logo_url" />
</a>
</div>
<div class="header-content">
Expand Down Expand Up @@ -33,9 +33,6 @@
<i class="fa fa-map">🗺️</i>
{{ event.location }}</span
>
<p class="header-summary" v-if="event.summary">
{{ event.summary }}
</p>
</div>
</div>
</template>
Expand All @@ -59,37 +56,31 @@ export default {
color: black;
}

.header-logo {
display: block;
float: none;
margin: none;
}

.header-logo img {
height: 6em;
margin-bottom: 3em;
}

.header-logo {
display: inline-block;
float: left;
max-width: 14em;
min-height: 6em;
margin-right: 2em;
margin-bottom: 1em;
}
.header-logo img {
width: 100%;
height: auto;
}

@media (max-width: 768px) {
#event-logo {
max-width: 128px;
max-height: 128px;
margin-bottom: 1em;
}
.header-logo {
display: block;
float: none;
margin: none;
margin: 0px;
max-width: none;
max-height: none;
}
.header-logo img {
width: 100%;
height: auto;
.header-logo a {
display: inline-block;
max-height: 128px;
margin-bottom: 1em;
}
.header-content {
padding-bottom: 2em;
Expand Down Expand Up @@ -122,12 +113,4 @@ export default {
box-shadow: none;
float: right;
}

.header-summary {
font-size: 90%;
font-style: italic;
text-align: left;
margin-top: 0.5em;
opacity: 0.7;
}
</style>
</style>
8 changes: 7 additions & 1 deletion src/components/ModalFrame.vue
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
frameborder="0"
marginheight="0"
marginwidth="0"
sandbox="allow-scripts allow-same-origin allow-forms"
>Loading…</iframe
>
</div>
Expand All @@ -29,6 +30,11 @@ export default {
components: {
Modal,
},
methods: {
isValidUrl(url) {
return url && (url.startsWith("http://") || url.startsWith("https://"));
},
},
data() {
return {
framesrc: null,
Expand All @@ -48,7 +54,7 @@ export default {
formref = formref + "/viewform?embedded=true";
}

this.framesrc = formref;
this.framesrc = this.isValidUrl(formref) ? formref : null;
},
};
</script>
Expand Down
24 changes: 16 additions & 8 deletions src/components/Previews.vue
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,7 @@
class="webembed"
v-if="showExcerpt && isEmbeddable(project)"
:src="getEmbed(project)"
sandbox="allow-scripts allow-same-origin allow-forms"
></iframe>

<div
Expand Down Expand Up @@ -152,6 +153,7 @@
class="webembed"
id="webembedframe"
:src="getEmbed(project)"
sandbox="allow-scripts allow-same-origin allow-forms"
></iframe>
</div>
<button
Expand Down Expand Up @@ -290,12 +292,20 @@ export default {
return project.webpage_url || isWithslides(project);
};

const isValidUrl = (url) => {
return url && (url.startsWith("http://") || url.startsWith("https://"));
};

const getEmbed = (project) => {
if (isWithslides(project)) return project.url + "/render";
if (!project.webpage_url) return "";
return project.webpage_url.endsWith(".pdf")
? project.url + "/render"
: project.webpage_url;
let url = "";
if (isWithslides(project)) {
url = project.url + "/render";
} else if (project.webpage_url) {
url = project.webpage_url.endsWith(".pdf")
? project.url + "/render"
: project.webpage_url;
}
return isValidUrl(url) ? url : "";
};

const seeEmbed = (project) => {
Expand Down Expand Up @@ -383,9 +393,7 @@ export default {
};

const countDown = () => {
const TIMER_LENGTH_MINUTES = 3;
// TODO: make configurable again
// parseInt(process.env.VUE_APP_TIMER_LENGTH) || 3;
const TIMER_LENGTH_MINUTES = parseInt(import.meta.env.VITE_TIMER_LENGTH || 3);
const pc_per_tick =
TIMER_LENGTH_MINUTES > 0 ? 100 / (60 * TIMER_LENGTH_MINUTES) : 0;
if (pc_per_tick == 0) return;
Expand Down
Loading