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 4
|
||||
|
||||
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
|
||||
```
|
||||
23
app/app.css
Normal file
23
app/app.css
Normal file
@@ -0,0 +1,23 @@
|
||||
@import "tailwindcss";
|
||||
@import "react-toastify/dist/ReactToastify.min.css";
|
||||
|
||||
|
||||
table {
|
||||
@apply w-full text-left;
|
||||
}
|
||||
|
||||
table thead {
|
||||
@apply bg-slate-100;
|
||||
}
|
||||
|
||||
table tbody {
|
||||
@apply bg-white;
|
||||
}
|
||||
|
||||
table tbody tr {
|
||||
@apply border-b border-slate-200;
|
||||
}
|
||||
|
||||
table thead th, table tbody td {
|
||||
@apply px-4 py-2;
|
||||
}
|
||||
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>
|
||||
);
|
||||
}
|
||||
13
app/components/layout/Footer.tsx
Normal file
13
app/components/layout/Footer.tsx
Normal file
@@ -0,0 +1,13 @@
|
||||
export function Footer() {
|
||||
return (
|
||||
<footer className="max-w-6xl mx-auto px-4 py-12 border-t border-slate-200 mt-12">
|
||||
<div className="text-center">
|
||||
<p className="text-sm text-slate-500">
|
||||
Copyright © {new Date().getFullYear()} Brainster Next College.
|
||||
All rights reserved.<br />
|
||||
Icons by <a href="https://icons8.com/" target="_blank" rel="noopener noreferrer">Icons8</a>
|
||||
</p>
|
||||
</div>
|
||||
</footer>
|
||||
);
|
||||
}
|
||||
31
app/components/layout/Header.tsx
Normal file
31
app/components/layout/Header.tsx
Normal file
@@ -0,0 +1,31 @@
|
||||
export function Header() {
|
||||
return (
|
||||
<header className="bg-white border-b border-slate-200 sticky top-0 z-10">
|
||||
<div className="max-w-6xl mx-auto px-4 py-3 h-auto min-h-16 flex items-center justify-between">
|
||||
<div className="flex items-center space-x-3">
|
||||
<div className="bg-blue-600 p-2 rounded-lg shrink-0">
|
||||
<img
|
||||
src="https://img.icons8.com/ios-filled/50/ffffff/flash-on.png"
|
||||
className="w-5 h-5 object-contain"
|
||||
alt="logo"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<h1 className="font-bold text-lg leading-tight">
|
||||
Distributed Systems and Blockchain
|
||||
</h1>
|
||||
<p className="text-xs text-slate-500">Brainster Next College</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center space-x-4">
|
||||
<p className="text-sm text-slate-500 hidden md:block">Chain: Next Testnet</p>
|
||||
|
||||
<button className="bg-blue-600 hover:bg-blue-700 text-white px-4 py-2 rounded-xl text-sm font-semibold transition-all shadow-sm">
|
||||
Connect Wallet
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
);
|
||||
}
|
||||
36
app/components/layout/Sidebar.tsx
Normal file
36
app/components/layout/Sidebar.tsx
Normal file
@@ -0,0 +1,36 @@
|
||||
import { Link, useLocation } from "react-router";
|
||||
|
||||
const navItems = [{ route: "/", label: "Explorer", icon: "home" }];
|
||||
|
||||
export function Sidebar() {
|
||||
const location = useLocation();
|
||||
|
||||
return (
|
||||
<aside className="lg:col-span-3">
|
||||
<nav className="space-y-1">
|
||||
{navItems.map((item) => (
|
||||
<Link
|
||||
key={item.route}
|
||||
to={item.route}
|
||||
className={`w-full flex items-center space-x-3 px-4 py-3 rounded-xl text-sm font-medium transition-colors ${
|
||||
location.pathname === item.route
|
||||
? "bg-blue-50 text-blue-700 border border-blue-100"
|
||||
: "text-slate-600 hover:bg-slate-100"
|
||||
}`}
|
||||
>
|
||||
<img
|
||||
src={
|
||||
location.pathname === item.route
|
||||
? `https://img.icons8.com/ios-filled/50/2563eb/${item.icon}.png`
|
||||
: `https://img.icons8.com/ios/50/64748b/${item.icon}.png`
|
||||
}
|
||||
className="w-5 h-5 shrink-0 object-contain"
|
||||
alt={item.label}
|
||||
/>
|
||||
<span>{item.label}</span>
|
||||
</Link>
|
||||
))}
|
||||
</nav>
|
||||
</aside>
|
||||
);
|
||||
}
|
||||
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>
|
||||
);
|
||||
}
|
||||
25
app/config/wagmi.ts
Normal file
25
app/config/wagmi.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
import { createConfig, http } from "wagmi";
|
||||
import { metaMask } from "wagmi/connectors";
|
||||
|
||||
import { type Chain } from "viem";
|
||||
|
||||
export const nextChain = {
|
||||
id: 1337,
|
||||
name: "Next",
|
||||
nativeCurrency: { name: "Ether", symbol: "ETH", decimals: 18 },
|
||||
rpcUrls: {
|
||||
default: { http: ["https://eth.code-camp.org"] }
|
||||
},
|
||||
blockExplorers: {
|
||||
default: { name: "Otterscan", url: "https://etherscan.code-camp.org" }
|
||||
},
|
||||
testnet: true
|
||||
} as const satisfies Chain;
|
||||
|
||||
export const config = createConfig({
|
||||
chains: [nextChain],
|
||||
transports: {
|
||||
[nextChain.id]: http()
|
||||
},
|
||||
connectors: [metaMask()]
|
||||
});
|
||||
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();
|
||||
};
|
||||
7
app/pages/home/HomePage.tsx
Normal file
7
app/pages/home/HomePage.tsx
Normal file
@@ -0,0 +1,7 @@
|
||||
export function HomePage() {
|
||||
return (
|
||||
<div>
|
||||
<p>Hello Wagmi!</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
103
app/root.tsx
Normal file
103
app/root.tsx
Normal file
@@ -0,0 +1,103 @@
|
||||
import {
|
||||
isRouteErrorResponse,
|
||||
Links,
|
||||
Meta,
|
||||
Outlet,
|
||||
Scripts,
|
||||
ScrollRestoration
|
||||
} from "react-router";
|
||||
|
||||
import { ToastContainer } from "react-toastify";
|
||||
|
||||
import { WagmiProvider } from "wagmi";
|
||||
import { config } from "./config/wagmi";
|
||||
|
||||
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
||||
const queryClient = new QueryClient();
|
||||
|
||||
import type { Route } from "./+types/root";
|
||||
import "./app.css";
|
||||
import { Header } from "./components/layout/Header";
|
||||
import { Footer } from "./components/layout/Footer";
|
||||
import { Sidebar } from "./components/layout/Sidebar";
|
||||
|
||||
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 className="bg-slate-50 text-slate-900 font-sans">
|
||||
<div>
|
||||
<Header />
|
||||
<main className="max-w-6xl mx-auto px-4 py-8 min-h-[calc(100vh-15rem)]">
|
||||
<div className="grid grid-cols-1 lg:grid-cols-12 gap-8">
|
||||
<Sidebar />
|
||||
|
||||
<div className="lg:col-span-9 space-y-6">{children}</div>
|
||||
</div>
|
||||
</main>
|
||||
<Footer />
|
||||
</div>
|
||||
<ToastContainer position="top-center" autoClose={5000} theme="colored" />
|
||||
<ScrollRestoration />
|
||||
<Scripts />
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
}
|
||||
|
||||
export default function App() {
|
||||
return (
|
||||
<WagmiProvider config={config}>
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<Outlet />
|
||||
</QueryClientProvider>
|
||||
</WagmiProvider>
|
||||
);
|
||||
}
|
||||
|
||||
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("connect", "routes/connect.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: "Wagmi Starter" },
|
||||
{ name: "description", content: "Welcome to Wagmi!" },
|
||||
];
|
||||
}
|
||||
|
||||
export default function Home() {
|
||||
return <HomePage />;
|
||||
}
|
||||
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();
|
||||
401
exam/index.html
Normal file
401
exam/index.html
Normal file
@@ -0,0 +1,401 @@
|
||||
<!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>
|
||||
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-lab4-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 is horizontal scaling?",
|
||||
"options": {
|
||||
"A": "Adding more CPU and RAM to one machine",
|
||||
"B": "Reducing system load",
|
||||
"C": "Upgrading the operating system",
|
||||
"D": "Adding more machines to the network"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": 2,
|
||||
"question": "In distributed systems, replication helps with:",
|
||||
"options": {
|
||||
"A": "Fault tolerance",
|
||||
"B": "Increasing latency",
|
||||
"C": "Reducing decentralization",
|
||||
"D": "Removing consensus"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": 3,
|
||||
"question": "A message broker provides:",
|
||||
"options": {
|
||||
"A": "Direct database access",
|
||||
"B": "Temporal decoupling between producer and consumer",
|
||||
"C": "Strong consistency",
|
||||
"D": "Blockchain verification"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": 4,
|
||||
"question": "Hardhat is:",
|
||||
"options": {
|
||||
"A": "An Ethereum development environment",
|
||||
"B": "A cryptocurrency",
|
||||
"C": "A blockchain",
|
||||
"D": "A consensus algorithm"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": 5,
|
||||
"question": "In Proof of Work, miners:",
|
||||
"options": {
|
||||
"A": "Stake coins",
|
||||
"B": "Solve complex mathematical puzzles",
|
||||
"C": "Vote on transactions",
|
||||
"D": "Create smart contracts"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": 6,
|
||||
"question": "In Ethereum, if two conflicting transactions attempt a double spend:",
|
||||
"options": {
|
||||
"A": "Both are accepted",
|
||||
"B": "Neither is accepted",
|
||||
"C": "A globally accepted order is selected and one is rejected",
|
||||
"D": "The sender loses all Ether"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": 7,
|
||||
"question": "A mnemonic (seed phrase) allows a user to:",
|
||||
"options": {
|
||||
"A": "Mine faster",
|
||||
"B": "Recover private keys",
|
||||
"C": "Increase gas fees",
|
||||
"D": "Modify blockchain rules"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": 8,
|
||||
"question": "In an AP (Availability + Partition tolerance) system, during a network partition the system:",
|
||||
"options": {
|
||||
"A": "Always returns consistent data",
|
||||
"B": "Stops operating",
|
||||
"C": "Removes inconsistent data",
|
||||
"D": "Returns possibly stale data"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": 9,
|
||||
"question": "Asynchronous communication primarily enables:",
|
||||
"options": {
|
||||
"A": "Tight coupling",
|
||||
"B": "Temporal coupling",
|
||||
"C": "Loose coupling",
|
||||
"D": "Direct memory access"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": 10,
|
||||
"question": "Why is Partition Tolerance (P) considered not-negotiable in real-world distributed systems?",
|
||||
"options": {
|
||||
"A": "Because users demand 100% availability",
|
||||
"B": "Because network failures that split a system are inevitable in the real world",
|
||||
"C": "Because it is the cheapest guarantee to implement",
|
||||
"D": "Because data consistency is only important in areas like banking"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": 11,
|
||||
"question": "How does Raft differ from basic Paxos in its approach to reaching consensus?",
|
||||
"options": {
|
||||
"A": "Raft was designed to work via a single elected leader",
|
||||
"B": "Raft is used for strong consistency, while Paxos is for eventual consistency",
|
||||
"C": "Raft does not require a majority of nodes to agree",
|
||||
"D": "Raft is mathematically impossible to implement"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": 12,
|
||||
"question": "Why are Paxos and Raft generally unsuitable for public blockchain networks?",
|
||||
"options": {
|
||||
"A": "They are too slow for blockchain use",
|
||||
"B": "They do not support data synchronization",
|
||||
"C": "They require specialized hardware to run",
|
||||
"D": "They assume all nodes are *honest* and operate in a trusted environment"
|
||||
}
|
||||
}
|
||||
]
|
||||
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();
|
||||
6111
package-lock.json
generated
Normal file
6111
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
40
package.json
Normal file
40
package.json
Normal file
@@ -0,0 +1,40 @@
|
||||
{
|
||||
"name": "dsb-lab4",
|
||||
"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": {
|
||||
"@metamask/sdk": "^0.33.1",
|
||||
"@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",
|
||||
"react-toastify": "^10.0.6",
|
||||
"recharts": "^3.7.0",
|
||||
"viem": "^2.47.1",
|
||||
"wagmi": "^3.5.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