Initial commit
This commit is contained in:
4
.dockerignore
Normal file
4
.dockerignore
Normal file
@@ -0,0 +1,4 @@
|
||||
.react-router
|
||||
build
|
||||
node_modules
|
||||
README.md
|
||||
10
.gitignore
vendored
Normal file
10
.gitignore
vendored
Normal file
@@ -0,0 +1,10 @@
|
||||
.DS_Store
|
||||
.env
|
||||
/node_modules/
|
||||
|
||||
# React Router
|
||||
/.react-router/
|
||||
/build/
|
||||
|
||||
# Archives
|
||||
*.zip
|
||||
22
Dockerfile
Normal file
22
Dockerfile
Normal file
@@ -0,0 +1,22 @@
|
||||
FROM node:24-alpine AS development-dependencies-env
|
||||
COPY . /app
|
||||
WORKDIR /app
|
||||
RUN npm ci
|
||||
|
||||
FROM node:24-alpine AS production-dependencies-env
|
||||
COPY ./package.json package-lock.json /app/
|
||||
WORKDIR /app
|
||||
RUN npm ci --omit=dev
|
||||
|
||||
FROM node:24-alpine AS build-env
|
||||
COPY . /app/
|
||||
COPY --from=development-dependencies-env /app/node_modules /app/node_modules
|
||||
WORKDIR /app
|
||||
RUN npm run build
|
||||
|
||||
FROM node:24-alpine
|
||||
COPY ./package.json package-lock.json /app/
|
||||
COPY --from=production-dependencies-env /app/node_modules /app/node_modules
|
||||
COPY --from=build-env /app/build /app/build
|
||||
WORKDIR /app
|
||||
CMD ["npm", "run", "start"]
|
||||
32
README.md
Normal file
32
README.md
Normal file
@@ -0,0 +1,32 @@
|
||||
# Lab 3
|
||||
|
||||
This is a React project. To get started, please read the instructions below.
|
||||
|
||||
|
||||
|
||||
## Getting started
|
||||
|
||||
To start the lab, run:
|
||||
|
||||
```shell
|
||||
npm run start
|
||||
```
|
||||
|
||||
|
||||
## Development
|
||||
|
||||
To start the React development server, run:
|
||||
|
||||
```shell
|
||||
npm run dev
|
||||
```
|
||||
|
||||
|
||||
|
||||
## Finishing up
|
||||
|
||||
When you're done, run the following command to generate a zip archive.
|
||||
|
||||
```shell
|
||||
npm run zip
|
||||
```
|
||||
6
app/app.css
Normal file
6
app/app.css
Normal file
@@ -0,0 +1,6 @@
|
||||
@import "tailwindcss";
|
||||
|
||||
@theme {
|
||||
--font-sans: "Inter", ui-sans-serif, system-ui, sans-serif,
|
||||
"Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
|
||||
}
|
||||
7
app/components/error/ErrorView.tsx
Normal file
7
app/components/error/ErrorView.tsx
Normal file
@@ -0,0 +1,7 @@
|
||||
export function ErrorView() {
|
||||
return (
|
||||
<div>
|
||||
<p>Ups, an unexpected error occurred.</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
7
app/components/loading/LoadingView.tsx
Normal file
7
app/components/loading/LoadingView.tsx
Normal file
@@ -0,0 +1,7 @@
|
||||
export function LoadingView() {
|
||||
return (
|
||||
<div className="flex justify-center items-center">
|
||||
<p className="sr-only">Loading...</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
7
app/lib/fetch.ts
Normal file
7
app/lib/fetch.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
export const fetchApi = async <T = any>(url: string): Promise<T> => {
|
||||
const response = await fetch(url);
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP error! status: ${response.status}`);
|
||||
}
|
||||
return response.json();
|
||||
};
|
||||
11
app/pages/home/HomePage.tsx
Normal file
11
app/pages/home/HomePage.tsx
Normal file
@@ -0,0 +1,11 @@
|
||||
import { Link } from "react-router";
|
||||
|
||||
export function HomePage() {
|
||||
return (
|
||||
<div className="flex flex-col items-center justify-center min-h-[90vh]">
|
||||
<p>Hello React!</p>
|
||||
|
||||
<Link to="/person/John" className="text-blue-500 mt-10">Go to Person</Link>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
13
app/pages/person/PersonPage.tsx
Normal file
13
app/pages/person/PersonPage.tsx
Normal file
@@ -0,0 +1,13 @@
|
||||
import { Link, useParams } from "react-router";
|
||||
|
||||
export function PersonPage() {
|
||||
const { id } = useParams();
|
||||
|
||||
return (
|
||||
<div className="flex flex-col items-center justify-center min-h-[90vh]">
|
||||
<p>Person Page: {id}</p>
|
||||
<p>(Retrieved from the URL params)</p>
|
||||
<Link to="/" className="text-blue-500 mt-10">Go to Home</Link>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
86
app/root.tsx
Normal file
86
app/root.tsx
Normal file
@@ -0,0 +1,86 @@
|
||||
import {
|
||||
isRouteErrorResponse,
|
||||
Links,
|
||||
Meta,
|
||||
Outlet,
|
||||
Scripts,
|
||||
ScrollRestoration,
|
||||
} from "react-router";
|
||||
|
||||
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
||||
const queryClient = new QueryClient();
|
||||
|
||||
import type { Route } from "./+types/root";
|
||||
import "./app.css";
|
||||
|
||||
export const links: Route.LinksFunction = () => [
|
||||
{ rel: "preconnect", href: "https://fonts.googleapis.com" },
|
||||
{
|
||||
rel: "preconnect",
|
||||
href: "https://fonts.gstatic.com",
|
||||
crossOrigin: "anonymous",
|
||||
},
|
||||
{
|
||||
rel: "stylesheet",
|
||||
href: "https://fonts.googleapis.com/css2?family=Inter:ital,opsz,wght@0,14..32,100..900;1,14..32,100..900&display=swap",
|
||||
},
|
||||
];
|
||||
|
||||
export function Layout({ children }: { children: React.ReactNode }) {
|
||||
return (
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charSet="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<Meta />
|
||||
<Links />
|
||||
</head>
|
||||
<body>
|
||||
<div className="container mx-auto">
|
||||
<main className="pt-10 pb-4">
|
||||
{children}
|
||||
</main>
|
||||
</div>
|
||||
<ScrollRestoration />
|
||||
<Scripts />
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
}
|
||||
|
||||
export default function App() {
|
||||
return (
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<Outlet />
|
||||
</QueryClientProvider>
|
||||
);
|
||||
}
|
||||
|
||||
export function ErrorBoundary({ error }: Route.ErrorBoundaryProps) {
|
||||
let message = "Oops!";
|
||||
let details = "An unexpected error occurred.";
|
||||
let stack: string | undefined;
|
||||
|
||||
if (isRouteErrorResponse(error)) {
|
||||
message = error.status === 404 ? "404" : "Error";
|
||||
details =
|
||||
error.status === 404
|
||||
? "The requested page could not be found."
|
||||
: error.statusText || details;
|
||||
} else if (import.meta.env.DEV && error && error instanceof Error) {
|
||||
details = error.message;
|
||||
stack = error.stack;
|
||||
}
|
||||
|
||||
return (
|
||||
<main className="pt-16 p-4 container mx-auto">
|
||||
<h1>{message}</h1>
|
||||
<p>{details}</p>
|
||||
{stack && (
|
||||
<pre className="w-full p-4 overflow-x-auto">
|
||||
<code>{stack}</code>
|
||||
</pre>
|
||||
)}
|
||||
</main>
|
||||
);
|
||||
}
|
||||
6
app/routes.ts
Normal file
6
app/routes.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
import { type RouteConfig, index, route } from "@react-router/dev/routes";
|
||||
|
||||
export default [
|
||||
index("routes/home.tsx"),
|
||||
route("person/:id", "routes/person.tsx"),
|
||||
] satisfies RouteConfig;
|
||||
12
app/routes/home.tsx
Normal file
12
app/routes/home.tsx
Normal file
@@ -0,0 +1,12 @@
|
||||
import { HomePage } from "../pages/home/HomePage";
|
||||
|
||||
export function meta() {
|
||||
return [
|
||||
{ title: "React Starter" },
|
||||
{ name: "description", content: "Welcome to React!" },
|
||||
];
|
||||
}
|
||||
|
||||
export default function Home() {
|
||||
return <HomePage />;
|
||||
}
|
||||
6
app/routes/person.tsx
Normal file
6
app/routes/person.tsx
Normal file
@@ -0,0 +1,6 @@
|
||||
import { PersonPage } from "../pages/person/PersonPage";
|
||||
|
||||
|
||||
export default function Person() {
|
||||
return <PersonPage />;
|
||||
}
|
||||
48
exam/common.js
Normal file
48
exam/common.js
Normal 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
58
exam/exam.js
Normal 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
390
exam/index.html
Normal 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: <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>
|
||||
17
exam/install-deps.js
Normal file
17
exam/install-deps.js
Normal 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
2068
exam/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
18
exam/package.json
Normal file
18
exam/package.json
Normal 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
122
exam/theory.json
Normal 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
71
exam/zip.js
Normal 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();
|
||||
4479
package-lock.json
generated
Normal file
4479
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
36
package.json
Normal file
36
package.json
Normal file
@@ -0,0 +1,36 @@
|
||||
{
|
||||
"name": "dsb-lab3",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"build": "react-router build",
|
||||
"dev": "react-router dev",
|
||||
"start": "node exam/exam.js",
|
||||
"zip": "node exam/zip.js",
|
||||
"typecheck": "react-router typegen && tsc",
|
||||
"postinstall": "node exam/install-deps.js"
|
||||
},
|
||||
"dependencies": {
|
||||
"@react-router/node": "7.12.0",
|
||||
"@react-router/serve": "7.12.0",
|
||||
"@tanstack/react-query": "^5.90.21",
|
||||
"clsx": "^2.1.1",
|
||||
"isbot": "^5.1.31",
|
||||
"react": "^19.2.4",
|
||||
"react-dom": "^19.2.4",
|
||||
"react-is": "^19.2.4",
|
||||
"react-router": "7.12.0",
|
||||
"recharts": "^3.7.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@react-router/dev": "7.12.0",
|
||||
"@tailwindcss/vite": "^4.1.13",
|
||||
"@types/node": "^22",
|
||||
"@types/react": "^19.2.7",
|
||||
"@types/react-dom": "^19.2.3",
|
||||
"tailwindcss": "^4.1.13",
|
||||
"typescript": "^5.9.2",
|
||||
"vite": "^7.1.7",
|
||||
"vite-tsconfig-paths": "^5.1.4"
|
||||
}
|
||||
}
|
||||
BIN
public/favicon.ico
Normal file
BIN
public/favicon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 15 KiB |
7
react-router.config.ts
Normal file
7
react-router.config.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
import type { Config } from "@react-router/dev/config";
|
||||
|
||||
export default {
|
||||
// Config options...
|
||||
// Server-side render by default, to enable SPA mode set this to `false`
|
||||
ssr: true,
|
||||
} satisfies Config;
|
||||
28
tsconfig.json
Normal file
28
tsconfig.json
Normal file
@@ -0,0 +1,28 @@
|
||||
{
|
||||
"include": [
|
||||
"**/*",
|
||||
"**/.server/**/*",
|
||||
"**/.client/**/*",
|
||||
".react-router/types/**/*"
|
||||
],
|
||||
"compilerOptions": {
|
||||
"lib": ["DOM", "DOM.Iterable", "ES2022"],
|
||||
"types": ["node", "vite/client"],
|
||||
"target": "ES2022",
|
||||
"module": "ES2022",
|
||||
"moduleResolution": "bundler",
|
||||
"jsx": "react-jsx",
|
||||
"rootDirs": [".", "./.react-router/types"],
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"~/*": ["./app/*"]
|
||||
},
|
||||
"esModuleInterop": true,
|
||||
"verbatimModuleSyntax": true,
|
||||
"noEmit": true,
|
||||
"noImplicitAny": false,
|
||||
"resolveJsonModule": true,
|
||||
"skipLibCheck": true,
|
||||
"strict": true
|
||||
}
|
||||
}
|
||||
8
vite.config.ts
Normal file
8
vite.config.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
import { reactRouter } from "@react-router/dev/vite";
|
||||
import tailwindcss from "@tailwindcss/vite";
|
||||
import { defineConfig } from "vite";
|
||||
import tsconfigPaths from "vite-tsconfig-paths";
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [tailwindcss(), reactRouter(), tsconfigPaths()],
|
||||
});
|
||||
Reference in New Issue
Block a user