402 lines
18 KiB
HTML
402 lines
18 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 4</title>
|
|
</head>
|
|
<body>
|
|
<div class="app-root">
|
|
<header class="app-header">
|
|
<nav>
|
|
<ul>
|
|
<li>
|
|
<hgroup>
|
|
<h2>Lab 4</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>
|
|
<li><a href="https://gitea.next.code-camp.org/dsb/wagmi-lesson" target="_blank">Wagmi 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 - Wagmi</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: Payroll System</h1>
|
|
|
|
<p>The repository you downloaded contains a React+Wagmi starter project. You can start the development server by running <strong>npm run dev</strong>. Your job is to create a simple payroll web app that can connect to MetaMask. A designer on our team has already created the design for the web app, and the backend team has created an API for fetching cryptocurrency prices and employee data. Your job is to create the frontend for the web app.</p>
|
|
|
|
<h3 class="mt-3">1. The dashboard page (<a href="https://api.next.code-camp.org/cdn/payroll/landing.html" target="_blank" style="text-decoration: none;">design</a>)</h3>
|
|
<p>The dashboard page has two cards on the top, and a pricing section with the current cryptocurrency prices. The API (url below) returns those prices as a list of cryptocurrencies. In the first card, you should display the current Ether price, and in the second card, you should show the current balance of the connected wallet (in Ether, and in USD - which is just the Ether balance times the Ether price). If the wallet is not connected, you should change the message in the second card to tell the user that the wallet is not connected. Ether will always be part of the API response (slug="ethereum").</p>
|
|
<p>You should create several React components, so that the code is organized and easy to understand. Keep in mind that you shouldn't recreate the whole design, as the layout is already part of the starter template (this includes the header, the footer, and the sidebar). You should only add the missing components from the main section (in the HTML source code, this will be available under the "main-content-container" element).</p>
|
|
|
|
<p>Important links:</p>
|
|
<ul>
|
|
<li><a href="https://api.next.code-camp.org/cdn/payroll/landing.html" target="_blank">The dashboard page design</a></li>
|
|
<li><a href="https://api.next.code-camp.org/cryptocurrencies" target="_blank">The API url</a> - https://api.next.code-camp.org/cryptocurrencies</li>
|
|
<li><a href="https://api.next.code-camp.org/cdn/payroll/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 status page (<a href="https://api.next.code-camp.org/cdn/payroll/status-offline.html" target="_blank" style="text-decoration: none;">design</a>)</h3>
|
|
<p>The status page allows the user to connect their wallet and view their current status and balance. The design (and MetaMask logo) changes based on the wallet connection status, as shown below. You should use Wagmi to show a list of connectors, or (if the user has already connected their wallet) show the current wallet balance.</p>
|
|
<p>After you create this page, add a new link in the sidebar to navigate to it. You can name the link "Status" and use the icon "wallet". Additionally, change the connect button from the header to a link that navigates to the status page.</p>
|
|
|
|
<p>Important links:</p>
|
|
<ul>
|
|
<li><a href="https://api.next.code-camp.org/cdn/payroll/status-offline.html" target="_blank">The status page design (not connected)</a></li>
|
|
<li><a href="https://api.next.code-camp.org/cdn/payroll/status-online.html" target="_blank">The status page design (connected)</a></li>
|
|
</ul>
|
|
|
|
<h3 class="mt-3">3. The employees page (<a href="https://api.next.code-camp.org/cdn/payroll/people.html" target="_blank" style="text-decoration: none;">design</a>)</h3>
|
|
<p>The employees page allows the user to view a list of employees and their salary (in ETH). The API (url below) returns a list of employees. You should display the employee name, phone number, email address, position, salary, employee history (the time since the employee started working), and a button to pay their salary in Ether. Next to the button, you should have an input field to enter a bonus (as a percentage of the salary). The "Pay" button should be disabled if the bonus field is empty or invalid (for example, if the user enters a non-numeric value or a negative number). After the user enters a bonus percentage, the button should be enabled and there should be a message showing the amount of Ether to be paid (the salary plus the bonus), as well as the wallet balance after the payment. See the design link for an example.</p>
|
|
|
|
<p>After the user clicks the "Pay" button, you should check if the user has enough Ether in their wallet to pay the salary - and show an error with toast.error() if they don't. After you send the transaction, you should show a success message with toast.success(). In this task, you don't need to wait for the transaction receipt.</p>
|
|
|
|
<p>After you create this page, add a new link in the sidebar to navigate to it. You can name the link "People" and use the icon "crowd".</p>
|
|
|
|
<p>Important links:</p>
|
|
<ul>
|
|
<li><a href="https://api.next.code-camp.org/cdn/payroll/people.html" target="_blank">The employees page design</a></li>
|
|
<li><a href="https://api.next.code-camp.org/employees" target="_blank">The API url</a> - https://api.next.code-camp.org/employees</li>
|
|
</ul>
|
|
</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>
|