Initial commit

This commit is contained in:
2026-03-05 16:08:09 +01:00
commit e775333bfe
28 changed files with 7579 additions and 0 deletions

48
exam/common.js Normal file
View File

@@ -0,0 +1,48 @@
import fs from 'fs';
import path from 'path';
import { exec, execSync } from 'child_process';
import { promisify } from 'util';
const execPromise = promisify(exec);
export function installDeps() {
let parentDirectory = process.cwd();
if (parentDirectory.includes('exam') && !fs.existsSync(path.join(parentDirectory, 'exam'))) {
parentDirectory = path.join(parentDirectory, '..');
}
const examDirectory = path.join(parentDirectory, 'exam');
// install starter/exam dependencies
if (!fs.existsSync(path.join(examDirectory, 'node_modules'))) {
console.log('Installing starter dependencies...');
execSync('npm install', { stdio: 'inherit', cwd: examDirectory });
console.log('Starter dependencies installed');
}
// install dev dependencies
if (!fs.existsSync(path.join(parentDirectory, 'node_modules'))) {
// install dev dependencies (in the background)
console.log(); console.log(); console.log();
console.log('Installing dev dependencies...');
installDevDeps(parentDirectory);
}
}
async function installDevDeps(workingDirectory, attempt = 1) {
try {
await execPromise('npm install', { stdio: 'inherit', cwd: workingDirectory });
console.log('Dev dependencies installed');
} catch (error) {
console.error('Error installing dev dependencies:', error.message);
if (attempt < 3) {
const retryDelay = 5000 * attempt;
console.log(`Retrying in ${Math.round(retryDelay / 1000)} seconds...`);
setTimeout(() => installDevDeps(workingDirectory, attempt + 1), retryDelay);
} else {
console.error('Failed to install dev dependencies.');
console.error('Please try again manually, by running "npm install" in the project directory.');
}
}
}

58
exam/exam.js Normal file
View File

@@ -0,0 +1,58 @@
import fs from 'fs';
import path from 'path';
import { installDeps } from './common.js';
const SERVER_PORT = 3018;
async function main() {
installDeps();
let parentDirectory = process.cwd();
if (parentDirectory.includes('exam') && !fs.existsSync(path.join(parentDirectory, 'exam'))) {
parentDirectory = path.join(parentDirectory, '..');
}
const indexHtml = fs.readFileSync(path.join(parentDirectory, 'exam', 'index.html'), 'utf8');
const examTheory = fs.readFileSync(path.join(parentDirectory, 'exam', 'theory.json'), 'utf8');
let examTheoryData = JSON.parse(examTheory);
// import express and start the server
const express = await import('express');
const app = express.default();
app.use(express.json());
app.use(express.urlencoded({ extended: true }));
app.get('/', (req, res) => {
res.setHeader('Content-Type', 'text/html');
res.setHeader('Cache-Control', 'no-cache');
res.setHeader('Pragma', 'no-cache');
res.setHeader('Expires', '0');
res.send(indexHtml);
});
app.get('/theory', (req, res) => {
res.setHeader('Content-Type', 'application/json');
res.json(examTheoryData);
});
app.post('/theory-answer', (req, res) => {
const questionId = req.body.questionId;
const answer = req.body.answer;
const index = examTheoryData.findIndex(q => q.id === questionId);
if (index !== -1) {
examTheoryData[index].answer = answer;
fs.writeFileSync(path.join(parentDirectory, 'exam', 'theory.json'), JSON.stringify(examTheoryData, null, 2));
res.status(200).json({ success: true });
} else {
res.status(404).json({ success: false, error: 'Question not found' });
}
});
const open = await import('open');
app.listen(SERVER_PORT, () => {
console.log(`Server is running on port ${SERVER_PORT}`);
open.default(`http://localhost:${SERVER_PORT}/`);
});
}
main();

390
exam/index.html Normal file
View File

@@ -0,0 +1,390 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="color-scheme" content="light dark">
<link
rel="stylesheet"
href="https://cdn.jsdelivr.net/npm/@picocss/pico@2/css/pico.min.css"
>
<link
rel="stylesheet"
href="https://cdn.jsdelivr.net/npm/@picocss/pico@2/css/pico.colors.min.css"
>
<style>
[data-theme="dark"] {
--pico-background-color: #282a36; /* Custom color */
}
.app-header {
border-bottom: 1px solid var(--pico-muted-border-color);
box-shadow: 0 1px 0 var(--pico-muted-border-color);
}
[data-theme="light"] .app-header {
background-color: #fcfcfc;
}
[data-theme="dark"] .app-header {
background-color: rgba(9, 21, 21, 0.6);
}
[data-theme="dark"] .sidebar {
border-right: 3px solid rgba(9, 21, 21, 0.6);
}
.app-header nav {
display: flex;
flex-direction: row;
align-items: center;
justify-content: space-between;
padding: 0 1.5rem;
}
.app-header hgroup {
margin: 0;
}
.app-header #theme-icon {
cursor: pointer;
user-select: none;
margin-left: 25px;
}
.app-root {
display: flex;
flex-direction: column;
min-height: 100vh;
}
.app-body {
display: flex;
flex: 1;
overflow: hidden;
}
.sidebar {
width: 315px;
flex-shrink: 0;
padding: 1.5rem;
border-right: 1px solid var(--pico-muted-border-color);
overflow-y: auto;
}
.sidebar nav {
display: flex;
flex-direction: column;
align-items: stretch;
justify-content: stretch;
gap: 0;
}
.sidebar .sidebar-button {
text-align: left;
width: 100%;
padding: 0.75rem 1rem 0.75rem 0;
border-bottom: 1px solid var(--pico-muted-border-color);
text-decoration: none !important;
}
[data-theme="dark"] hgroup>:not(:first-child):last-child {
--pico-color: revert;
}
.main-content {
flex: 1;
padding: 1.5rem;
}
.main-content section.hidden {
display: none !important;
}
.main-content section p.mt-3 {
margin-top: 3rem;
}
.main-content section h3.mt-3 {
margin-top: 3rem;
}
.main-content section .theory-question:first-of-type {
margin-top: 2.5rem;
}
.main-content section .theory-question {
border-top: 1px solid var(--pico-muted-border-color);
padding-top: 1.5rem;
padding-bottom: 1rem;
margin-bottom: 0;
margin-top: 0;
}
.main-content section .theory-question p.question {
font-weight: 500;
margin-bottom: 0.5rem;
}
.main-content section hgroup p {
padding-top: 5px;
}
/* Mobile: hide sidebar, show desktop-only message */
@media (max-width: 768px) {
.sidebar {
display: none;
}
.main-content {
display: none;
}
.mobile-message {
display: flex !important;
}
}
.mobile-message {
display: none;
flex: 1;
align-items: center;
justify-content: center;
padding: 2rem;
text-align: center;
}
.mobile-message article {
max-width: 28rem;
}
</style>
<title>Lab 3</title>
</head>
<body>
<div class="app-root">
<header class="app-header">
<nav>
<ul>
<li>
<hgroup>
<h2>Lab 3</h2>
<p>Distributed Systems and Blockchain</p>
</hgroup>
</li>
</ul>
<ul>
<li>
<details class="dropdown">
<summary>
Documentation
</summary>
<ul>
<li><a href="https://api.next.code-camp.org/cdn/viem-docs.pdf" target="_blank">Viem</a></li>
<li><a href="https://api.next.code-camp.org/cdn/solidity-docs.pdf" target="_blank">Solidity</a></li>
<li><a href="https://api.next.code-camp.org/cdn/crowdfunding-contract.html" target="_blank">Contract example</a></li>
<li><a href="https://api.next.code-camp.org/cdn/crowdfunding-tests.html" target="_blank">Tests example</a></li>
<li><a href="https://gitea.next.code-camp.org/dsb/next-coins" target="_blank">React example app</a></li>
</ul>
</details>
</li>
<li>
<span id="theme-icon" aria-hidden="true" onclick="cycleTheme()">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 32 32" fill="currentColor" class="icon-theme-toggle "><clipPath id="theme-toggle-cutout"><path d="M0-11h25a1 1 0 0017 13v30H0Z"></path></clipPath><g clip-path="url(#theme-toggle-cutout)"><circle cx="16" cy="16" r="8.4"></circle><path d="M18.3 3.2c0 1.3-1 2.3-2.3 2.3s-2.3-1-2.3-2.3S14.7.9 16 .9s2.3 1 2.3 2.3zm-4.6 25.6c0-1.3 1-2.3 2.3-2.3s2.3 1 2.3 2.3-1 2.3-2.3 2.3-2.3-1-2.3-2.3zm15.1-10.5c-1.3 0-2.3-1-2.3-2.3s1-2.3 2.3-2.3 2.3 1 2.3 2.3-1 2.3-2.3 2.3zM3.2 13.7c1.3 0 2.3 1 2.3 2.3s-1 2.3-2.3 2.3S.9 17.3.9 16s1-2.3 2.3-2.3zm5.8-7C9 7.9 7.9 9 6.7 9S4.4 8 4.4 6.7s1-2.3 2.3-2.3S9 5.4 9 6.7zm16.3 21c-1.3 0-2.3-1-2.3-2.3s1-2.3 2.3-2.3 2.3 1 2.3 2.3-1 2.3-2.3 2.3zm2.4-21c0 1.3-1 2.3-2.3 2.3S23 7.9 23 6.7s1-2.3 2.3-2.3 2.4 1 2.4 2.3zM6.7 23C8 23 9 24 9 25.3s-1 2.3-2.3 2.3-2.3-1-2.3-2.3 1-2.3 2.3-2.3z"></path></g></svg>
</span>
</li>
</ul>
</nav>
</header>
<div class="app-body">
<aside class="sidebar">
<nav>
<a href="#theory" class="sidebar-button">Theory</a>
<a href="#react" class="sidebar-button">Coding - React</a>
</nav>
</aside>
<main class="main-content container" id="main-content">
<section id="theory">
<hgroup>
<h2>Theory</h1>
<p>Answer the following questions to the best of your ability. Each question has exactly one correct answer. There are no negative marks for incorrect answers.</p>
</hgroup>
<div id="theory-questions"></div>
</section>
<section id="react" class="hidden">
<h2>Coding - React: Next News</h1>
<p>The repository you downloaded contains a React starter project. You can start the development server by running <strong>npm run dev</strong>. Your job is to create a simple news website. A designer on our team has already created the design for the website, and the backend team has created an API for fetching news articles. Your job is to create the frontend for the website.</p>
<h3 class="mt-3">1. The landing page (<a href="https://api.next.code-camp.org/cdn/news/index.html" target="_blank" style="text-decoration: none;">design</a>)</h3>
<p>The landing page has a header, a featured news article on top, a list of other news articles, and a footer. The API (url below) returns a list of articles. You can show the first one as the featured article, and the rest as standard news articles below it.</p>
<p>You should create several React components, so that the code is organized and easy to understand. At the very least, you should create a component for the header, a component for the featured news article, a component for a standard news article, and a component for the footer (which is the same on all pages, so you can render it in the root.tsx file - where you can also define the classes on the body element).</p>
<p>Important links:</p>
<ul>
<li><a href="https://api.next.code-camp.org/cdn/news/index.html" target="_blank">The landing page design</a></li>
<li><a href="https://api.next.code-camp.org/news-posts" target="_blank">The API url</a> - https://api.next.code-camp.org/news-posts</li>
<li><a href="https://api.next.code-camp.org/cdn/news/types.html" target="_blank">The types</a> (created by our backend team)</li>
</ul>
<p>Remember: Just open the design link in your browser and view the source code (or use the browser's Inspect tool) to copy the necessary HTML structure. You don't need to design anything!</p>
<h3 class="mt-3">2. The article page (<a href="https://api.next.code-camp.org/cdn/news/article.html" target="_blank" style="text-decoration: none;">design</a>)</h3>
<p>The article page is used to display a single news article. The API (url below) returns a single article (in addition to the basic information, each article has content, which is a list of strings defining the paragraphs of text; and a list of similar articles which are displayed on the right side of the page).</p>
<p>Important links:</p>
<ul>
<li><a href="https://api.next.code-camp.org/cdn/news/article.html" target="_blank">The article page design</a></li>
<li>The API url - https://api.next.code-camp.org/news-posts/:slug (see <a href="https://api.next.code-camp.org/news-posts/bitcoin-surges-institutional-investors-increase-exposure" target="_blank">example</a>)</li>
</ul>
<h3 class="mt-3">Linking the pages</h3>
<p>Instead of using the "a" tag for links, you should use Link from react-router (example: &lt;Link to="/article/123"&gt;Article 123&lt;/Link&gt;). This allows the application to update the URL and change the content without a full page reload, making the navigation feel more seamless.</p>
</section>
</main>
<div class="mobile-message">
<article>
<header>
<strong>Desktop required</strong>
</header>
<p>This exam layout needs a larger screen. Please open it on a desktop computer.</p>
</article>
</div>
</div>
</div>
<script>
(function () {
function setupTheme() {
var THEME_KEY = 'dsb-exam-theme';
var html = document.documentElement;
var icon = document.getElementById('theme-icon');
function applyTheme(theme) {
html.setAttribute('data-theme', theme);
try { localStorage.setItem(THEME_KEY, theme); } catch (e) {}
}
function cycleTheme(ev) {
ev.preventDefault();
var next = html.getAttribute('data-theme') === 'dark' ? 'light' : 'dark';
applyTheme(next);
}
try {
var saved = localStorage.getItem(THEME_KEY);
if (saved === 'light' || saved === 'dark') applyTheme(saved);
} catch (e) {}
if (icon) icon.addEventListener('click', cycleTheme);
}
function setupSidebar() {
var sidebar = document.querySelector('.sidebar');
var sidebarButtons = sidebar.querySelectorAll('.sidebar-button');
sidebarButtons.forEach(function(button) {
button.addEventListener('click', function() {
var target = button.getAttribute('href');
var targetSection = document.querySelector(target);
var sections = document.querySelectorAll('.main-content section');
if (targetSection) {
document.body.style.opacity = 0;
sections.forEach(function(section) {
section.classList.add('hidden');
});
targetSection.classList.remove('hidden');
setTimeout(function() {
window.scrollTo(0, 0);
document.body.style.opacity = 1;
}, 50);
}
});
});
}
function setupTheory() {
function setupTheoryCheckboxes() {
var theoryCheckboxes = document.querySelectorAll('.theory-question input[type="checkbox"]');
theoryCheckboxes.forEach(function(checkbox) {
checkbox.addEventListener('change', function() {
var question = checkbox.closest('.theory-question');
var questionId = question.getAttribute('id').split('-')[1];
var answer = checkbox.getAttribute('name').split('-')[2];
var otherCheckboxes = question.querySelectorAll('input[type="checkbox"]');
otherCheckboxes.forEach(function(otherCheckbox) {
const otherAnswer = otherCheckbox.getAttribute('name').split('-')[2];
if (otherAnswer !== answer) {
otherCheckbox.checked = false;
}
});
checkbox.checked = true;
fetch('/theory-answer', {
method: 'POST',
body: JSON.stringify({
questionId: Number(questionId),
answer: answer
}),
headers: {
'Content-Type': 'application/json'
}
})
.then(function(response) {
if (!response.ok) {
alert('Failed to submit answer.');
}
})
.catch(function(error) {
alert('Failed to submit answer.');
});
});
});
}
fetch('/theory')
.then(response => response.json())
.then(function(data) {
var theory = document.querySelector('#theory');
var html = '';
for (var i = 0; i < data.length; i++) {
var questionId = data[i].id;
var question = data[i].question;
var options = data[i].options;
var answer = data[i].answer;
html += '<div class="theory-question" id="question-' + questionId + '">';
html += '<p class="question">' + (i + 1) + '. ' + question + "</p>";
const sortedOptionKeys = Object.keys(options).sort();
html += '<fieldset>';
sortedOptionKeys.forEach(function(optionKey) {
html += '<label>';
html += '<input type="checkbox" name="question-' + questionId + '-' + optionKey + '"' + (answer === optionKey ? ' checked' : '') +'>';
html += ' ' + options[optionKey];
html += '</label>';
});
html += '</fieldset>';
html += '</div>';
}
document.querySelector('#theory-questions').innerHTML = html;
setupTheoryCheckboxes();
});
}
setupTheme();
setupSidebar();
setupTheory();
})();
</script>
</body>
</html>

17
exam/install-deps.js Normal file
View File

@@ -0,0 +1,17 @@
import fs from 'fs';
import { execSync } from 'child_process';
import path from 'path';
const parentDirectory = process.cwd();
if (parentDirectory.includes('exam') && !fs.existsSync(path.join(parentDirectory, 'exam'))) {
parentDirectory = path.join(parentDirectory, '..');
}
const examDirectory = path.join(parentDirectory, 'exam');
if (!fs.existsSync(path.join(examDirectory, 'node_modules'))) {
console.log('Exam directory node_modules not found. Installing...');
execSync('npm install', { stdio: 'inherit', cwd: examDirectory });
} else {
console.log('Exam directory node_modules already exists. Skipping.');
}

2068
exam/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

18
exam/package.json Normal file
View File

@@ -0,0 +1,18 @@
{
"name": "dsb-lab3-exam",
"version": "1.0.0",
"type": "module",
"scripts": {
"start": "echo 'Run this command in the parent directory...'"
},
"devDependencies": {
"@types/node": "^22.19.11",
"@types/prompts": "^2.4.9"
},
"dependencies": {
"archiver": "^7.0.1",
"express": "^5.2.1",
"open": "^11.0.0",
"prompts": "^2.4.2"
}
}

122
exam/theory.json Normal file
View File

@@ -0,0 +1,122 @@
[
{
"id": 1,
"question": "What best describes a distributed system?",
"options": {
"A": "A system running on a single powerful computer",
"B": "A collection of independent computers appearing as a single coherent system",
"C": "A database replicated in multiple files",
"D": "A centralized system with backup servers"
}
},
{
"id": 2,
"question": "In distributed systems, asynchronous communication primarily enables:",
"options": {
"A": "Tight coupling",
"B": "Immediate request-response",
"C": "Direct memory access",
"D": "Loose coupling"
}
},
{
"id": 3,
"question": "A standard message queue is ideal for:",
"options": {
"A": "Broadcasting events to all services",
"B": "Encrypting REST calls",
"C": "Storing smart contracts",
"D": "Work distribution (e.g., resizing images)"
}
},
{
"id": 4,
"question": "In distributed systems, retries are generally considered:",
"options": {
"A": "Immediate",
"B": "Impossible",
"C": "Inevitable",
"D": "Unnecessary"
}
},
{
"id": 5,
"question": "A cryptocurrency wallet (running on our phone or computer) primarily stores:",
"options": {
"A": "Coins owned by the user",
"B": "Blocks from the blockchain",
"C": "Gas to sign transactions",
"D": "Private (and public) keys"
}
},
{
"id": 6,
"question": "The Bitcoin Lightning Network is designed to:",
"options": {
"A": "Replace Ethereum smart contracts",
"B": "Equalize PoW energy consumption",
"C": "Enable faster off-chain Bitcoin transactions",
"D": "Change Bitcoin's supply cap"
}
},
{
"id": 7,
"question": "RPC can cause problems if developers forget that:",
"options": {
"A": "It uses HTTP for communication",
"B": "It requires JSON encoding",
"C": "It needs a message broker to work",
"D": "It is still a network call and can fail"
}
},
{
"id": 8,
"question": "In Solidity, a mapping is most similar to:",
"options": {
"A": "An array with elements",
"B": "A dictionary (key-value store)",
"C": "A loop with fixed iterations",
"D": "A function implementing a feature"
}
},
{
"id": 9,
"question": "What is React?",
"options": {
"A": "A database management system",
"B": "A free and open-source front-end JavaScript library",
"C": "A backend framework for Node.js",
"D": "A CSS styling library"
}
},
{
"id": 10,
"question": "What is used to pass data from a parent component to a child component in React?",
"options": {
"A": "Props",
"B": "State",
"C": "Events",
"D": "Hooks"
}
},
{
"id": 11,
"question": "What is the main purpose of the Virtual DOM in React?",
"options": {
"A": "To store sensitive application data",
"B": "To create a CRUD design for objects",
"C": "To efficiently update the browser DOM by comparing changes",
"D": "To replace JavaScript in modern browsers"
}
},
{
"id": 12,
"question": "Why does React require a key attribute when rendering lists?",
"options": {
"A": "To uniquely identify items for efficient updates",
"B": "To apply CSS styling for each element",
"C": "To be able to present escaped HTML data",
"D": "To enable linking to the list items"
}
}
]

71
exam/zip.js Normal file
View File

@@ -0,0 +1,71 @@
import fs from 'fs';
import path from 'path';
import { installDeps } from './common.js';
import packageJson from '../package.json' with { type: 'json' };
async function main() {
installDeps();
const prompts = await import('prompts');
console.log(`Lets create a zip archive for your submission.`);
const { studentId } = await prompts.default({
type: 'text',
name: 'studentId',
message: 'What is your student ID?',
format: (value) => value.trim().replace(/[^A-Za-z0-9]/g, ''),
});
if (!studentId) {
console.error('Student ID is required');
process.exit(1);
}
let parentDirectory = process.cwd();
if (parentDirectory.includes('exam') && !fs.existsSync(path.join(parentDirectory, 'exam'))) {
parentDirectory = path.join(parentDirectory, '..');
}
const archiveName = `${packageJson.name}-${studentId}.zip`;
const zipPath = path.join(parentDirectory, archiveName);
if (fs.existsSync(zipPath)) {
console.log(`Removing existing archive at ${zipPath}`);
fs.unlinkSync(zipPath);
}
const archiver = await import('archiver');
const archive = archiver.default('zip');
archive.on('error', (err) => {
console.error(err);
throw err;
});
archive.glob('**/*', {
cwd: parentDirectory,
ignore: [
'node_modules/**',
'exam/node_modules/**',
'.git/**',
'.react-router/**',
'artifacts/**',
'build/**',
'cache/**',
'dist/**',
'.env',
'.DS_Store',
'*.zip',
'exam/common.js',
'exam/exam.js',
'exam/install-deps.js',
'exam/index.html',
'exam/zip.js'
]
});
archive.pipe(fs.createWriteStream(zipPath));
await archive.finalize();
console.log();
console.log(`Archive created: ${zipPath}`);
}
main();