391 lines
15 KiB
HTML
391 lines
15 KiB
HTML
<!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: <Link to="/article/123">Article 123</Link>). 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>
|