This commit is contained in:
Philippe Torrel
2026-11-03 14:52:15 +01:00
parent ed2c0b9906
commit f475f40350
104 changed files with 12459 additions and 4509 deletions
@@ -0,0 +1,18 @@
module.exports = {
root: true,
env: { browser: true, es2020: true },
extends: [
'eslint:recommended',
'plugin:@typescript-eslint/recommended',
'plugin:react-hooks/recommended',
],
ignorePatterns: ['dist', '.eslintrc.cjs'],
parser: '@typescript-eslint/parser',
plugins: ['react-refresh'],
rules: {
'react-refresh/only-export-components': [
'warn',
{ allowConstantExport: true },
],
},
}
@@ -0,0 +1,10 @@
# Frontend Class: Debugging Product Sales Table Exercise
Install the dependencies and devDependencies and start the server.
```bash
npm install
npm run dev
```
This will start the server on http://localhost:5173
@@ -0,0 +1,44 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.8/dist/css/bootstrap.min.css" />
<title>Product Sales Table</title>
</head>
<body>
<div id="root"></div>
<div class="container py-5">
<div class="alert alert-primary" role="alert">
<h2>Übung 15: Produkt-Verkaufstabelle</h2>
<p>
Es handelt sich um eine einfache React-App, die eine Liste der Produktverkäufe in einer Tabelle anzeigt, die
von einer API aufgerufen wird. Um die Übung zu vereinfachen, verwenden wir im Projekt lokale Daten, die aber
eine externe API anregen.
</p>
<p>
Du wirst jedoch feststellen, dass beim Starten der App sofort eine leere Seite mit Fehlern in der Konsole
angezeigt wird. Du kannst die Fehler überprüfen, indem du die Konsole öffnest. Die Aufgabe lautet wie folgt:
</p>
<ol class="list">
<li>
Untersuche die Fehler und debugge die Anwendung nur mit dem VSCode-Debugger. Das heißt, du darfst vorher
keine Dateien bearbeiten. Verwende das Variablenfeld im Debugger, um die Variablen zu bearbeiten.
</li>
<li>
Nachdem du die Anwendung erfolgreich debuggt hast, behebe den Fehler, indem du die Funktion fetchSalesData
änderst. Ändere nicht die Datendatei selbst. Das ist eine gute Übung, wenn du eine API aufrufst, die
manchmal fehlerhafte Daten zurückgibt.
</li>
</ol>
</div>
</div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>
@@ -0,0 +1,28 @@
{
"name": "product-sales-table",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "tsc -b && vite build",
"lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0",
"preview": "vite preview"
},
"dependencies": {
"react": "^19.2.8",
"react-dom": "^19.2.8"
},
"devDependencies": {
"@types/react": "^19.2.18",
"@types/react-dom": "^19.2.4",
"@typescript-eslint/eslint-plugin": "^8.67.0",
"@typescript-eslint/parser": "^8.67.0",
"@vitejs/plugin-react": "^6.1.0",
"eslint": "^10.8.1",
"eslint-plugin-react-hooks": "^7.1.1",
"eslint-plugin-react-refresh": "^0.5.4",
"typescript": "^6.0.3",
"vite": "^8.2.2"
}
}
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

@@ -0,0 +1,42 @@
import { useState, useEffect } from 'react';
import Table from './components/Table';
function App() {
const [salesData, setSalesData] = useState(null);
const fetchSalesData = async () => {
const response = await fetch('./src/lib/db/sales.json');
const data = await response.json();
setSalesData(data);
};
useEffect(() => {
fetchSalesData();
}, []);
return (
<main>
<div className="wrapper">
<div className="header-container">
<div className="header-title-wrapper">
<h1 className="title">Product Sales</h1>
<p className="subtitle">
A table of product sales data for the current month.
</p>
</div>
</div>
<div className="table-wrapper">
<div className="table-container">
<div className="table-box">
{salesData && <Table salesData={salesData} />}
</div>
</div>
</div>
</div>
</main>
);
}
export default App;
@@ -0,0 +1,27 @@
import TableRow from './TableRow';
import { Product } from '../lib/datatypes';
const Table = ({ salesData }: { salesData: Product[] }) => {
return (
<table className="divide-y divide-gray-300">
<thead>
<tr>
<th scope="col">ID</th>
<th scope="col">Product</th>
<th scope="col">Code</th>
<th scope="col">Quantity</th>
<th scope="col">Price</th>
<th scope="col">Profit</th>
</tr>
</thead>
<tbody className="divide-y divide-gray-200">
{salesData.map((product) => (
<TableRow key={product.id} product={product} />
))}
</tbody>
</table>
);
};
export default Table;
@@ -0,0 +1,25 @@
import { Product } from '../lib/datatypes';
const TableRow = ({ product }: { product: Product }) => {
const {
id,
product: productName,
productCode,
quantity,
price,
profit,
} = product;
return (
<tr>
<td className="id-column">{id}</td>
<td className="product-info-column">{productName}</td>
<td className="product-info-column">{productCode}</td>
<td className="product-num-column">{quantity}</td>
<td className="product-num-column">${price.toFixed(2)}</td>
<td className="product-num-column">${profit.toFixed(2)}</td>
</tr>
);
};
export default TableRow;
@@ -0,0 +1,187 @@
*,
*::before,
*::after {
box-sizing: border-box;
}
*,
html,
body {
margin: 0;
padding: 0;
color: #171717;
font-family: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto,
Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif;
}
button {
border: none;
}
.screen-reader-text {
position: absolute;
width: 1px;
height: 1px;
padding: 0;
margin: -1px;
overflow: hidden;
clip: rect(0, 0, 0, 0);
white-space: nowrap;
border-width: 0;
}
/* ===== App Component ===== */
main {
max-width: 80rem; /* 1280px */
margin: 0 auto;
padding: 3rem 0.75rem; /* 48px 12px */
}
.wrapper {
padding: 0 1rem; /* 0 16px */
}
.header-container {
display: block;
}
.header-title-wrapper {
display: block;
}
.title {
font-size: 1rem; /* 16px */
line-height: 1.5rem; /* 24px */
line-height: 1.5rem; /* 24px */
font-weight: 600;
color: #111827;
}
.subtitle {
margin-top: 0.5rem; /* 8px */
font-size: 0.875rem; /* 14px */
line-height: 1.25rem; /* 20px */
color: #374151;
}
.table-wrapper {
display: flow-root;
margin-top: 2rem; /* 32px */
}
.table-container {
margin: -0.5rem -1rem; /* -8px -16px */
overflow-x: auto;
}
.table-box {
display: inline-block;
min-width: 100%;
padding: 0.5rem 0 /* 8px 0 */;
vertical-align: middle;
}
/* ===== Table Component ===== */
table {
min-width: 100%;
border-collapse: collapse;
}
th {
white-space: nowrap;
padding-top: 0.875rem /* 14px */;
padding-bottom: 0.875rem /* 14px */;
padding-left: 1rem /* 16px */;
padding-right: 0.75rem /* 12px */;
text-align: left;
font-weight: 600;
font-size: 0.875rem /* 14px */;
line-height: 1.25rem /* 20px */;
color: #111827;
}
tbody {
background-color: white;
}
thead tr {
border-bottom: 1px solid #d1d5db;
}
tbody tr {
border-bottom: 1px solid #e5e7eb;
}
.id-column {
white-space: nowrap;
padding-top: 0.5rem /* 8px */;
padding-bottom: 0.5rem /* 8px */;
padding-left: 1rem /* 16px */;
padding-right: 0.75rem /* 12px */;
font-size: 0.875rem /* 14px */;
line-height: 1.25rem /* 20px */;
color: #6b7280;
}
.product-info-column {
white-space: nowrap;
padding: 0.5rem /* 8px */;
font-size: 0.875rem /* 14px */;
line-height: 1.25rem /* 20px */;
font-weight: 500;
color: #111827;
}
.product-num-column {
white-space: nowrap;
padding: 0.5rem /* 8px */;
font-size: 0.875rem /* 14px */;
line-height: 1.25rem /* 20px */;
color: #6b7280;
}
/* ===== Breakpoints ===== */
@media (min-width: 640px) {
.wrapper {
padding: 0 1.5rem; /* 0 24px */
}
.header-container {
display: flex;
align-items: center;
}
.header-title-wrapper {
flex: 1 1 auto;
}
.table-container {
margin: -0.5rem -1.5rem; /* -8px -24px */
}
.table-box {
padding: 0.5rem 1.5rem /* 8px 24px */;
}
th {
padding-left: 0px;
}
.id-column {
padding-left: 0px;
}
}
@media (min-width: 1024px) {
.wrapper {
padding: 0 2rem; /* 0 32px */
}
.table-container {
margin: -0.5rem -2rem; /* -8px -32px */
}
.table-box {
padding: 0.5rem 2rem /* 8px 32px */;
}
}
@@ -0,0 +1,8 @@
export interface Product {
id: number;
product: string;
productCode: string;
quantity: number;
price: number;
profit: number;
}
@@ -0,0 +1,115 @@
[
{
"id": 1,
"product": "Laptop",
"productCode": "LPT",
"quantity": 352,
"price": 1200.0,
"profit": 200.0
},
{
"id": 2,
"product": "Smartphone",
"productCode": "SPH",
"quantity": 500,
"price": 800.0,
"profit": 123.0
},
{
"id": 3,
"product": "Headphones",
"productCode": "HDP",
"quantity": 432,
"price": 150.0,
"profit": 50.0
},
{
"id": 4,
"product": "Keyboard",
"productCode": "KBD",
"quantity": 431,
"price": 75.0,
"profit": 25.0
},
null,
{
"id": 6,
"product": "Monitor",
"productCode": "MNT",
"quantity": 234,
"price": 300.0,
"profit": 50.0
},
{
"id": 7,
"product": "Tablet",
"productCode": "TBL",
"quantity": 68,
"price": 350.0,
"profit": 100.0
},
{
"id": 8,
"product": "Printer",
"productCode": "PRT",
"quantity": 642,
"price": 200.0,
"profit": 50.0
},
{
"id": 9,
"product": "Webcam",
"productCode": "WCM",
"quantity": 123,
"price": 90.0,
"profit": 20.0
},
{
"id": 10,
"product": "Desk Lamp",
"productCode": "DLP",
"quantity": 1921,
"price": 30.0,
"profit": 5.0
},
{
"id": 11,
"product": "Chair",
"productCode": "CHR",
"quantity": 4854,
"price": 120.0,
"profit": 20.0
},
{
"id": 12,
"product": "External Hard Drive",
"productCode": "EHD",
"quantity": 483,
"price": 100.0,
"profit": 20.0
},
{
"id": 13,
"product": "USB-C Adapter",
"productCode": "USC",
"quantity": 10239,
"price": 19.99,
"profit": 5.0
},
{
"id": 14,
"product": "Smart Watch",
"productCode": "SWT",
"quantity": 483,
"price": 199.99,
"profit": 50.0
},
{
"id": 15,
"product": "Speakers",
"productCode": "SPK",
"quantity": 394,
"price": 85.0,
"profit": 20.0
}
]
@@ -0,0 +1,10 @@
import React from 'react'
import ReactDOM from 'react-dom/client'
import App from './App.tsx'
import './index.css'
ReactDOM.createRoot(document.getElementById('root')!).render(
<React.StrictMode>
<App />
</React.StrictMode>,
)
@@ -0,0 +1 @@
/// <reference types="vite/client" />
@@ -0,0 +1,27 @@
{
"compilerOptions": {
"composite": true,
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
"target": "ES2020",
"useDefineForClassFields": true,
"lib": ["ES2020", "DOM", "DOM.Iterable"],
"module": "ESNext",
"skipLibCheck": true,
/* Bundler mode */
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"resolveJsonModule": true,
"isolatedModules": true,
"moduleDetection": "force",
"noEmit": true,
"jsx": "react-jsx",
/* Linting */
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true
},
"include": ["src"]
}
@@ -0,0 +1,11 @@
{
"files": [],
"references": [
{
"path": "./tsconfig.app.json"
},
{
"path": "./tsconfig.node.json"
}
]
}
@@ -0,0 +1,13 @@
{
"compilerOptions": {
"composite": true,
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
"skipLibCheck": true,
"module": "ESNext",
"moduleResolution": "bundler",
"allowSyntheticDefaultImports": true,
"strict": true,
"noEmit": true
},
"include": ["vite.config.ts"]
}
@@ -0,0 +1,7 @@
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
// https://vitejs.dev/config/
export default defineConfig({
plugins: [react()],
})
@@ -0,0 +1,18 @@
module.exports = {
root: true,
env: { browser: true, es2020: true },
extends: [
'eslint:recommended',
'plugin:@typescript-eslint/recommended',
'plugin:react-hooks/recommended',
],
ignorePatterns: ['dist', '.eslintrc.cjs'],
parser: '@typescript-eslint/parser',
plugins: ['react-refresh'],
rules: {
'react-refresh/only-export-components': [
'warn',
{ allowConstantExport: true },
],
},
}
@@ -0,0 +1,10 @@
# Frontend Class: LexiFind Exercise Starter
Install the dependencies and devDependencies and start the server.
```bash
npm install
npm run dev
```
This will start the server on http://localhost:5173
@@ -0,0 +1,56 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/LexiFind.webp" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>LexiFind - Your Personal Dictionary</title>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.8/dist/css/bootstrap.css" />
</head>
<body>
<div id="root"></div>
<div class="container py-5">
<div class="alert alert-primary" role="alert">
<h2>Übung 16: LexiFind Favorite Words</h2>
<p>
In dieser Übung musst du ein brandneues LexiFind mit einer neuen Funktion reparieren. Du kannst jetzt deine
Lieblingswörter direkt auf der Startseite speichern und alle deine Lieblingswörter auf einer separaten Seite
finden. Das ist richtig, LexiFind kommt jetzt mit Routing.
</p>
<p>
Wenn du auf das Herzsymbol auf der Startseite klickst, wird es zu deinen Favoriten hinzugefügt. Das leere
Herzsymbol wird ebenfalls ausgefüllt. Eine neue Seite namens Favoriten (/favorites) listet jetzt alle unsere
Lieblingswörter auf. Wenn du auf das Wort klickst, gelangst du zur Startseite mit dem Wort, das du angeklickt
hast.
</p>
<p>
Die App ist außerdem intelligent genug, um zu wissen, welche Lieblingswörter du ausgewählt hast. Alle
Lieblingswörter haben ein ausgefülltes Herzsymbol auf der Startseite und auf der Favoritenseite. Wenn du auf
ein gefülltes Herz klickst, wird es aus der Favoritenliste entfernt und das Herzsymbol wird leer.
</p>
<p>Aber aus irgendeinem Grund funktioniert der Code nicht so, wie er sollte...</p>
<ol class="list">
<li>
Wenn wir auf das Herzsymbol für ein Wort klicken, ist das Herz gefüllt. Aber wenn wir die Seite
aktualisieren, ist das Herz wieder leer. Es sollte ausgefüllt sein.
</li>
<li>
Das gleiche Verhalten können wir beobachten, wenn wir ein Wort mit einem Herz versehen und dann zur Seite
Favoriten gehen. Manchmal ist das Herz ausgefüllt, manchmal ist es leer. Sie sollten auf jeder Seite
ausgefüllt sein.
</li>
<l>
Wenn du auf das Wort in den Favoritenseiten klickst, z.B. "blue", kommst du auf die Startseite und es wird
"Course" statt "blue" angezeigt. Es sollte das richtige Wort angezeigt werden.
</l>
</ol>
</div>
</div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>
@@ -0,0 +1,30 @@
{
"name": "lexifind-webmaster-exercise",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "tsc -b && vite build",
"lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0",
"preview": "vite preview"
},
"dependencies": {
"@types/react-router-dom": "^5.3.3",
"react": "^19.2.8",
"react-dom": "^19.2.8",
"react-router-dom": "^7.18.2"
},
"devDependencies": {
"@types/react": "^19.2.18",
"@types/react-dom": "^19.2.4",
"@typescript-eslint/eslint-plugin": "^8.67.0",
"@typescript-eslint/parser": "^8.67.0",
"@vitejs/plugin-react": "^6.1.0",
"eslint": "^10.8.1",
"eslint-plugin-react-hooks": "^7.1.1",
"eslint-plugin-react-refresh": "^0.5.4",
"typescript": "^6.0.3",
"vite": "^8.2.2"
}
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 KiB

@@ -0,0 +1,57 @@
import { useState, useEffect } from 'react';
import { Link } from 'react-router-dom';
import { HeartFilled, HeartIcon } from './Icons';
interface FavoriteWordProps {
word: string;
}
const FavoriteWord: React.FC<FavoriteWordProps> = ({ word }) => {
const [isFavorite, setIsFavorite] = useState(false);
const toggleFavorite = () => {
const favorites = JSON.parse(localStorage.getItem('favoriteWords') || '[]');
if (favorites.includes(word)) {
const filteredFavorites = favorites.filter(
(item: string) => item !== word
);
localStorage.setItem('favoriteWords', JSON.stringify(filteredFavorites));
} else {
favorites.push(word);
localStorage.setItem('favoriteWords', JSON.stringify(favorites));
}
setIsFavorite(!isFavorite);
};
// BUG: Missing useEffect
/* useEffect(() => {
const favoriteWords = JSON.parse(
localStorage.getItem('favoriteWords') || '[]'
);
setIsFavorite(favoriteWords.includes(word));
}, [word]); */
return (
<div className="fave-word-wrapper">
<Link
// to={`/?word=${word}`}
to="/" // BUG
className="fave-word-link"
>
<p>{word}</p>
</Link>
<button className="fave-icon-btn" onClick={toggleFavorite}>
{isFavorite ? (
<HeartFilled className="heart-icon-filled" aria-hidden="true" />
) : (
<HeartIcon className="heart-icon-unfilled" aria-hidden="true" />
)}
</button>
</div>
);
};
export default FavoriteWord;
@@ -0,0 +1,42 @@
import React from 'react';
import { Link } from 'react-router-dom';
import { SERIF_FONTS, SANS_SERIF_FONTS } from '../lib/constants';
interface HeaderProps {
font: string;
setFont: React.Dispatch<React.SetStateAction<string>>;
}
const Header: React.FC<HeaderProps> = ({ font, setFont }) => {
return (
<header className="header-container">
<Link to="/" className="header-logo-wrapper">
<img
src="/LexiFind.webp"
alt="LexiFind Logo"
className="header-logo-img"
/>
<span className="header-logo-text">LexiFind</span>
</Link>
<div>
<Link to="/favorites" className="header-link-text">
Favorites
</Link>
</div>
<select
value={font}
onChange={(e) => setFont(e.target.value)}
className="header-select"
style={{ fontFamily: font }}
>
<option value={SERIF_FONTS}>Serif</option>
<option value={SANS_SERIF_FONTS}>Sans Serif</option>
</select>
</header>
);
};
export default Header;
@@ -0,0 +1,46 @@
export function HeartIcon(props: React.SVGProps<SVGSVGElement>) {
return (
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
strokeWidth="1.5"
stroke="currentColor"
{...props}
>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M21 8.25c0-2.485-2.099-4.5-4.688-4.5-1.935 0-3.597 1.126-4.312 2.733-.715-1.607-2.377-2.733-4.313-2.733C5.1 3.75 3 5.765 3 8.25c0 7.22 9 12 9 12s9-4.78 9-12Z"
/>
</svg>
);
}
export function HeartFilled(props: React.SVGProps<SVGSVGElement>) {
return (
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
fill="currentColor"
{...props}
>
<path d="m11.645 20.91-.007-.003-.022-.012a15.247 15.247 0 0 1-.383-.218 25.18 25.18 0 0 1-4.244-3.17C4.688 15.36 2.25 12.174 2.25 8.25 2.25 5.322 4.714 3 7.688 3A5.5 5.5 0 0 1 12 5.052 5.5 5.5 0 0 1 16.313 3c2.973 0 5.437 2.322 5.437 5.25 0 3.925-2.438 7.111-4.739 9.256a25.175 25.175 0 0 1-4.244 3.17 15.247 15.247 0 0 1-.383.219l-.022.012-.007.004-.003.001a.752.752 0 0 1-.704 0l-.003-.001Z" />
</svg>
);
}
export function PlayIcon(props: React.SVGProps<SVGSVGElement>) {
return (
<svg
xmlns="http://www.w3.org/2000/svg"
width="16"
height="16"
fill="currentColor"
viewBox="0 0 16 16"
{...props}
>
<path d="m11.596 8.697-6.363 3.692c-.54.313-1.233-.066-1.233-.697V4.308c0-.63.692-1.01 1.233-.696l6.363 3.692a.802.802 0 0 1 0 1.393" />
</svg>
);
}
@@ -0,0 +1,59 @@
import React from 'react';
interface SearchbarProps {
word: string;
setWord: React.Dispatch<React.SetStateAction<string>>;
handleSubmit: (e: React.FormEvent<HTMLFormElement>) => void;
}
const Searchbar: React.FC<SearchbarProps> = ({
word,
setWord,
handleSubmit,
}) => {
return (
<div className="searchbar-wrapper">
<form onSubmit={handleSubmit}>
<label htmlFor="searchWord" className="screen-reader-text">
Search word
</label>
<div className="searchbar-container">
<input
type="text"
name="searchWord"
id="searchWord"
className="searchbar-input"
placeholder="Type a word to search"
value={word}
onChange={(e) => setWord(e.target.value)}
/>
<button
type="submit"
className="searchbar-magnifying-glass"
aria-label="Search"
>
<SearchIcon className="searchbar-icon" aria-hidden="true" />
</button>
</div>
</form>
</div>
);
};
function SearchIcon(props: React.SVGProps<SVGSVGElement>) {
return (
<svg
xmlns="http://www.w3.org/2000/svg"
width="16"
height="16"
fill="currentColor"
viewBox="0 0 16 16"
{...props}
>
<path d="M11.742 10.344a6.5 6.5 0 1 0-1.397 1.398h-.001q.044.06.098.115l3.85 3.85a1 1 0 0 0 1.415-1.414l-3.85-3.85a1 1 0 0 0-.115-.1zM12 6.5a5.5 5.5 0 1 1-11 0 5.5 5.5 0 0 1 11 0" />
</svg>
);
}
export default Searchbar;
@@ -0,0 +1,51 @@
import React from 'react';
interface SourceFooterProps {
source: string;
}
const SourceFooter: React.FC<SourceFooterProps> = ({ source }) => {
return (
<>
<div className="footer-divider"></div>
<div className="footer-wrapper">
<h4 className="footer-title">Source</h4>
<div>
<a href={source} className="footer-link-wrapper">
{source}
<BoxArrowUpRightIcon
className="footer-link-icon"
aria-hidden="true"
/>
</a>
</div>
</div>
</>
);
};
function BoxArrowUpRightIcon(props: React.SVGProps<SVGSVGElement>) {
return (
<svg
xmlns="http://www.w3.org/2000/svg"
width="16"
height="16"
fill="currentColor"
viewBox="0 0 16 16"
{...props}
>
<path
fillRule="evenodd"
d="M8.636 3.5a.5.5 0 0 0-.5-.5H1.5A1.5 1.5 0 0 0 0 4.5v10A1.5 1.5 0 0 0 1.5 16h10a1.5 1.5 0 0 0 1.5-1.5V7.864a.5.5 0 0 0-1 0V14.5a.5.5 0 0 1-.5.5h-10a.5.5 0 0 1-.5-.5v-10a.5.5 0 0 1 .5-.5h6.636a.5.5 0 0 0 .5-.5"
/>
<path
fillRule="evenodd"
d="M16 .5a.5.5 0 0 0-.5-.5h-5a.5.5 0 0 0 0 1h3.793L6.146 9.146a.5.5 0 1 0 .708.708L15 1.707V5.5a.5.5 0 0 0 1 0z"
/>
</svg>
);
}
export default SourceFooter;
@@ -0,0 +1,66 @@
import React from 'react';
import { Meaning, WordData } from '../lib/datatypes';
interface WordDefinitionsProps {
wordData: WordData;
}
const WordDefinitions: React.FC<WordDefinitionsProps> = ({ wordData }) => {
return (
<div className="word-definitions-wrapper">
{wordData.meanings.map((meaning: any, index: number) => (
<WordDefinitionBlock key={index} data={meaning} />
))}
</div>
);
};
interface WordDefinitionBlockProps {
data: Meaning;
}
const WordDefinitionBlock: React.FC<WordDefinitionBlockProps> = ({ data }) => {
const { partOfSpeech, definitions, synonyms } = data;
return (
<div className="word-block-wrapper">
<div className="word-block-category-container">
<h2 className="word-block-category-name">{partOfSpeech}</h2>
<div className="word-block-category-divider"></div>
</div>
<div className="word-block-meaning-container">
<h3 className="word-block-meaning-title">Meaning</h3>
<ul className="word-block-meaning-list">
{definitions.map((definition: any, index: number) => (
<li key={index} className="word-block-meaning-list-item">
{definition.definition}{' '}
{definition.example && (
<span className="word-block-meaning-list-item-example">
"{definition.example}"
</span>
)}
</li>
))}
</ul>
</div>
{synonyms.length > 0 && (
<div className="word-block-synonyms-container">
<h3 className="word-block-synonyms-title">Synonyms</h3>
<ul className="word-block-synonyms-list">
{synonyms.map((synonym: string, index: number) => (
<li key={index} className="word-block-synonyms-list-item">
{synonym}
</li>
))}
</ul>
</div>
)}
</div>
);
};
export default WordDefinitions;
@@ -0,0 +1,82 @@
import React, { useState, useEffect, useRef } from 'react';
import { PlayIcon, HeartIcon, HeartFilled } from './Icons';
import { Phonetic } from '../lib/datatypes';
interface WordHeroProps {
data: {
word: string;
phonetics: Phonetic[];
};
}
const WordHero: React.FC<WordHeroProps> = ({ data }) => {
const { word, phonetics } = data;
const [isFavorite, setIsFavorite] = useState(false);
const americanEnglishPhonetic = phonetics.find((phonetic) =>
phonetic.audio?.includes('-us')
);
const finalPhonetic = americanEnglishPhonetic || phonetics[0];
const audioRef = useRef<HTMLAudioElement | null>(null);
const playAudio = () => {
if (audioRef.current) {
audioRef.current.play();
}
};
const toggleFavorite = () => {
const favorites = JSON.parse(localStorage.getItem('favoriteWords') || '[]');
if (favorites.includes(word)) {
const filteredFavorites = favorites.filter(
(item: string) => item !== word
);
localStorage.setItem('favoriteWords', JSON.stringify(filteredFavorites));
} else {
favorites.push(word);
localStorage.setItem('favoriteWords', JSON.stringify(favorites));
}
setIsFavorite(!isFavorite);
};
useEffect(() => {
const favoriteWords = JSON.parse(
localStorage.getItem('favortieWords') || '[]' // BUG favortieWords instead of (favoriteWords)
);
// setIsFavorite(favoriteWords.includes(word)); // BUG
}, [word]);
return (
<div className="wordhero-wrapper">
<div className="wordhero-word-container">
<button className="heart-icon-btn" onClick={toggleFavorite}>
{isFavorite ? (
<HeartFilled className="heart-icon-filled" aria-hidden="true" />
) : (
<HeartIcon className="heart-icon-unfilled" aria-hidden="true" />
)}
</button>
<h1 className="wordhero-word">{word}</h1>
{finalPhonetic?.text && (
<p className="wordhero-word-phonetic">{finalPhonetic.text}</p>
)}
</div>
{finalPhonetic?.audio && (
<div>
<button onClick={playAudio} className="wordhero-play-btn">
<PlayIcon className="wordhero-play-icon" aria-hidden="true" />
</button>
<audio ref={audioRef} src={finalPhonetic.audio}></audio>
</div>
)}
</div>
);
};
export default WordHero;
@@ -0,0 +1,430 @@
*,
*::before,
*::after {
box-sizing: border-box;
}
*,
html,
body {
margin: 0;
padding: 0;
color: #171717;
}
button {
border: none;
}
a {
text-decoration: none;
}
.screen-reader-text {
position: absolute;
width: 1px;
height: 1px;
padding: 0;
margin: -1px;
overflow: hidden;
clip: rect(0, 0, 0, 0);
white-space: nowrap;
border-width: 0;
}
.heart-icon-btn {
background-color: transparent;
margin-right: auto;
cursor: pointer;
}
.heart-icon-unfilled {
width: 2rem; /* 32px */
height: 2rem; /* 32px */
stroke: #9333ea;
background-color: transparent;
}
.heart-icon-filled {
width: 2rem; /* 32px */
height: 2rem; /* 32px */
fill: #9333ea;
background-color: transparent;
}
/* ===== App Component ===== */
.app-wrapper {
margin: 0 auto;
max-width: 42rem; /* 672px */
padding: 3.5rem 1.5rem; /* 56px 24px */
}
/* ===== Header Component ===== */
.header-container {
display: flex;
align-items: center;
justify-content: space-between;
}
.header-logo-wrapper {
display: flex;
align-items: center;
}
.header-logo-img {
height: 2.5rem; /* 40px */
}
.header-logo-text {
font-family: ui-serif, Georgia, Cambria, 'Times New Roman', Times, serif;
font-size: 1.875rem; /* 30px */
line-height: 2.25rem; /* 36px */
font-weight: 600;
margin-left: 1rem; /* 16px */
color: #262626;
}
.header-select {
border-radius: 0.375rem; /* 6px */
border-width: 0px;
padding: 0.625rem 0.5rem; /* 10px 8px */
font-size: 1.125rem; /* 18px */
line-height: 1.75rem; /* 28px */
outline: 2px solid transparent;
outline-offset: 2px;
color: #262626;
}
.header-select:focus {
outline-color: #9333ea;
}
.header-link-text {
font-size: 1.125rem; /* 18px */
line-height: 1.75rem; /* 28px */
text-decoration: none;
color: #262626;
}
.header-link-text:hover {
color: #9333ea;
}
/* ===== Searchbar Component ===== */
.searchbar-wrapper {
margin-top: 4rem; /* 64px */
}
.searchbar-container {
position: relative;
margin-top: 0.5rem; /* 8px */
border-radius: 0.75rem; /* 12px */
box-shadow: 0 1px 2px 0 rgb(0 0 0 / 0.05);
}
.searchbar-input {
display: block;
width: 100%;
border-radius: 0.75rem; /* 12px */
border-width: 0px;
padding: 1.25rem 0 1.25rem 1.5rem; /* 20px 0 20px 24px */
font-size: 1.125rem; /* 18px */
line-height: 1.75rem; /* 28px */
font-weight: 600;
outline-style: solid;
outline: 2px solid transparent;
outline-color: #f5f5f5;
color: #171717;
background-color: #f5f5f5;
}
.searchbar-input:focus {
outline-color: #9333ea;
}
.searchbar-input::placeholder {
color: #737373;
}
.searchbar-magnifying-glass {
position: absolute;
top: 0px;
bottom: 0px;
right: 0px;
display: flex;
align-items: center;
border-top-right-radius: 0.75rem; /* 12px */
border-bottom-right-radius: 0.75rem; /* 12px */
padding-left: 1rem; /* 16px */
padding-right: 1rem; /* 16px */
outline: 2px solid transparent;
outline-offset: 2px;
background-color: transparent;
}
.searchbar-icon {
width: 1.5rem; /* 24px */
height: 1.5rem; /* 24px */
fill: #6b21a8;
}
/* ===== WordHero Component ===== */
.wordhero-wrapper {
display: flex;
align-items: center;
justify-content: space-between;
margin-top: 4rem; /* 64px */
}
.wordhero-word-container {
display: flex;
flex-direction: column;
}
.wordhero-word {
font-size: 3.75rem; /* 60px */
line-height: 1;
font-weight: 600;
color: #262626;
}
.wordhero-word-phonetic {
font-size: 1.5rem; /* 24px */
line-height: 2rem; /* 32px */
margin-top: 0.75rem; /* 12px */
margin-bottom: 0.75rem; /* 12px */
color: #9333ea;
}
.wordhero-play-btn {
display: flex;
align-items: center;
border-radius: 9999px;
padding: 1.25rem; /* 20px */
box-shadow: 0 1px 2px 0 rgb(0 0 0 / 0.05);
color: white;
background-color: #e9d5ff;
}
.wordhero-play-btn:hover {
background-color: #d8b4fe;
}
.wordhero-play-btn:focus {
outline: 2px solid transparent;
outline-color: #9333ea;
}
.wordhero-play-icon {
height: 2.5rem; /* 40px */
width: 2.5rem; /* 40px */
fill: #9333ea;
}
/* ===== WordDefinitions Component ===== */
.word-definitions-wrapper {
display: flex;
flex-direction: column;
gap: 2rem; /* 32px */
margin-top: 4rem; /* 64px */
}
.word-block-wrapper {
display: flex;
flex-direction: column;
gap: 3.5rem; /* 56px */
}
.word-block-category-container {
display: flex;
align-items: center;
gap: 0.5rem; /* 8px */
}
.word-block-category-name {
padding-right: 0.75rem; /* 12px */
font-size: 1.5rem; /* 24px */
line-height: 2rem; /* 32px */
font-weight: 600;
color: #262626;
}
.word-block-category-divider {
height: 1px;
width: 100%;
background-color: #d4d4d4;
}
.word-block-meaning-container {
display: flex;
flex-direction: column;
row-gap: 1rem; /* 16px */
}
.word-block-meaning-title {
font-size: 1.25rem; /* 20px */
line-height: 1.75rem; /* 28px */
font-weight: 400;
color: #737373;
}
.word-block-meaning-list {
display: flex;
flex-direction: column;
padding-left: 1rem; /* 16px */
font-size: 1.125rem; /* 18px */
line-height: 1.75rem; /* 28px */
row-gap: 2rem; /* 32px */
color: #262626;
}
.word-block-meaning-list-item::marker {
padding-left: 2.5rem; /* 40px */
color: #9333ea;
}
.word-block-meaning-list-item-example {
display: block;
margin-top: 0.5rem; /* 8px */
color: #9333ea;
}
.word-block-synonyms-container {
display: flex;
align-items: baseline;
column-gap: 2rem; /* 32px */
}
.word-block-synonyms-title {
font-size: 1.25rem; /* 20px */
line-height: 1.75rem; /* 28px */
font-weight: 400;
color: #737373;
}
.word-block-synonyms-list {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 0.5rem; /* 8px */
}
.word-block-synonyms-list-item {
list-style: none;
font-size: 1.125rem; /* 18px */
line-height: 1.75rem; /* 28px */
font-weight: 600;
color: #9333ea;
}
/* ===== SourceFooter Component ===== */
.footer-divider {
margin-top: 2.5rem; /* 40px */
margin-bottom: 2.5rem; /* 40px */
height: 1px;
background-color: #e5e5e5;
}
.footer-wrapper {
display: flex;
align-items: baseline;
gap: 1rem; /* 16px */
font-size: 0.875rem; /* 14px */
line-height: 1.25rem; /* 20px */
}
.footer-title {
font-weight: 400;
color: #737373;
}
.footer-link-wrapper {
display: flex;
text-decoration: underline;
color: #262626;
}
.footer-link-icon {
margin-left: 0.5rem; /* 8px */
height: 1rem; /* 16px */
width: 1rem; /* 16px */
fill: #262626;
}
/* ===== Favorites Page ===== */
.favorites-list {
margin-top: 1rem; /* 16px */
border-top-width: 2px;
border-bottom-width: 0px;
border-color: rgb(229 229 229);
}
.favorites-text {
margin-top: 4rem; /* 64px */
font-size: 1.125rem; /* 18px */
line-height: 1.75rem; /* 28px */
color: #525252;
}
.favorites-item {
list-style: none;
padding: 0.5rem 0; /* Add some padding to each item */
}
.favorites-item.with-divider {
border-top: 1px solid #e5e5e5; /* Add divider */
}
/* ===== Favorite Word ===== */
.fave-word-wrapper {
display: flex;
align-items: center;
justify-content: space-between;
padding: 2.5rem 0; /* 40px 0 */
}
.fave-word-link {
background-color: #e9d5ff;
padding: 0.5rem 0.875rem; /* 8px 14px */
border-radius: 0.375rem; /* 6px */
box-shadow: 0 1px 2px 0 rgb(0 0 0 / 0.05);
}
.fave-word-link:hover {
background-color: #d8b4fe;
}
.fave-word-link:focus {
outline: 2px solid #9333ea;
}
.fave-word-link p {
font-size: 1.25rem; /* 20px */
line-height: 1.75rem; /* 28px */
font-weight: 500;
color: #262626;
}
.fave-icon-btn {
background-color: transparent;
cursor: pointer;
}
/* ===== Breakpoints ===== */
@media (min-width: 640px) {
.searchbar-input {
line-height: 1.5rem; /* 24px */
}
}
@media (min-width: 768px) {
.word-block-meaning-list {
padding-left: 2.5rem; /* 40px */
}
}
@media (min-width: 1024px) {
.app-wrapper {
padding: 3.5rem 0.5rem; /* 56px 8px */
}
}
@@ -0,0 +1,5 @@
export const SERIF_FONTS =
'ui-serif, Georgia, Cambria, "Times New Roman", Times, serif';
export const SANS_SERIF_FONTS =
'ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"';
@@ -0,0 +1,22 @@
export interface Phonetic {
text?: string;
audio?: string;
}
export interface Definition {
definition: string;
example?: string;
}
export interface Meaning {
partOfSpeech: string;
definitions: Definition[];
synonyms: string[];
}
export interface WordData {
word: string;
phonetics: Phonetic[];
meanings: Meaning[];
sourceUrls: string[];
}
@@ -0,0 +1,24 @@
import React from 'react';
import ReactDOM from 'react-dom/client';
import './index.css';
import Root from './pages/root.tsx';
import Favorites from './pages/favorites.tsx';
import { createBrowserRouter, RouterProvider } from 'react-router-dom';
const router = createBrowserRouter([
{
path: '/',
element: <Root />,
},
{
path: '/favorites',
element: <Favorites />,
},
]);
ReactDOM.createRoot(document.getElementById('root')!).render(
<React.StrictMode>
<RouterProvider router={router} />
</React.StrictMode>
);
@@ -0,0 +1,46 @@
import { useState, useEffect } from 'react';
import Header from '../components/Header';
import FavoriteWord from '../components/FavoriteWord';
import { SERIF_FONTS } from '../lib/constants';
function Favorites() {
const [font, setFont] = useState<string>(SERIF_FONTS);
const [favoriteWords, setFavoriteWords] = useState<string[]>([]);
useEffect(() => {
const loadedFavorites = JSON.parse(
localStorage.getItem('favoriteWords') || '[]'
);
setFavoriteWords(loadedFavorites);
}, []);
return (
<div
className="app-wrapper"
style={{
fontFamily: font,
}}
>
<Header font={font} setFont={setFont} />
{favoriteWords.length > 0 ? (
<ul className="favorites-list">
{favoriteWords.map((word, index) => (
<li
className={`favorites-item ${index > 0 ? 'with-divider' : ''}`}
key={index}
>
<FavoriteWord word={word} />
</li>
))}
</ul>
) : (
<p className="favorites-text">No favorite words added yet.</p>
)}
</div>
);
}
export default Favorites;
@@ -0,0 +1,60 @@
import React, { useState, useEffect } from 'react';
import { WordData } from '../lib/datatypes';
import Header from '../components/Header';
import Searchbar from '../components/Searchbar';
import WordHero from '../components/WordHero';
import WordDefinitions from '../components/WordDefinitions';
import SourceFooter from '../components/SourceFooter';
import { SERIF_FONTS } from '../lib/constants';
function Root() {
const [font, setFont] = useState<string>(SERIF_FONTS);
const [word, setWord] = useState<string>('course');
const [wordData, setWordData] = useState<WordData | null>(null);
const fetchData = async () => {
try {
const url = `https://api.dictionaryapi.dev/api/v2/entries/en/${word}`;
const response = await fetch(url);
const data = await response.json();
setWordData(data[0]);
} catch (error) {
console.error(error);
}
};
const handleSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
fetchData();
};
useEffect(() => {
fetchData();
}, []);
return (
<div
className="app-wrapper"
style={{
fontFamily: font,
}}
>
<Header font={font} setFont={setFont} />
<Searchbar word={word} setWord={setWord} handleSubmit={handleSubmit} />
{wordData && (
<div>
<WordHero data={wordData} />
<WordDefinitions wordData={wordData} />
<SourceFooter source={wordData.sourceUrls[0]} />
</div>
)}
</div>
);
}
export default Root;
@@ -0,0 +1 @@
/// <reference types="vite/client" />
@@ -0,0 +1,27 @@
{
"compilerOptions": {
"composite": true,
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
"target": "ES2020",
"useDefineForClassFields": true,
"lib": ["ES2020", "DOM", "DOM.Iterable"],
"module": "ESNext",
"skipLibCheck": true,
/* Bundler mode */
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"resolveJsonModule": true,
"isolatedModules": true,
"moduleDetection": "force",
"noEmit": true,
"jsx": "react-jsx",
/* Linting */
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true
},
"include": ["src"]
}
@@ -0,0 +1,11 @@
{
"files": [],
"references": [
{
"path": "./tsconfig.app.json"
},
{
"path": "./tsconfig.node.json"
}
]
}
@@ -0,0 +1,13 @@
{
"compilerOptions": {
"composite": true,
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
"skipLibCheck": true,
"module": "ESNext",
"moduleResolution": "bundler",
"allowSyntheticDefaultImports": true,
"strict": true,
"noEmit": true
},
"include": ["vite.config.ts"]
}
@@ -0,0 +1,7 @@
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
// https://vitejs.dev/config/
export default defineConfig({
plugins: [react()],
})
Binary file not shown.
File diff suppressed because it is too large Load Diff
@@ -4,255 +4,209 @@
Free for personal and commercial use under the CCA 3.0 license (html5up.net/license)
*/
(function($) {
var $window = $(window),
$body = $('body'),
$wrapper = $('#wrapper'),
$header = $('#header'),
$nav = $('#nav'),
$main = $('#main'),
$navPanelToggle, $navPanel, $navPanelInner;
// Breakpoints.
breakpoints({
default: ['1681px', null ],
xlarge: ['1281px', '1680px' ],
large: ['981px', '1280px' ],
medium: ['737px', '980px' ],
small: ['481px', '736px' ],
xsmall: ['361px', '480px' ],
xxsmall: [null, '360px' ]
});
/**
* Applies parallax scrolling to an element's background image.
* @return {jQuery} jQuery object.
*/
$.fn._parallax = function(intensity) {
var $window = $(window),
$this = $(this);
if (this.length == 0 || intensity === 0)
return $this;
if (this.length > 1) {
for (var i=0; i < this.length; i++)
$(this[i])._parallax(intensity);
return $this;
}
if (!intensity)
intensity = 0.25;
$this.each(function() {
var $t = $(this),
$bg = $('<div class="bg"></div>').appendTo($t),
on, off;
on = function() {
$bg
.removeClass('fixed')
.css('transform', 'matrix(1,0,0,1,0,0)');
$window
.on('scroll._parallax', function() {
var pos = parseInt($window.scrollTop()) - parseInt($t.position().top);
$bg.css('transform', 'matrix(1,0,0,1,0,' + (pos * intensity) + ')');
});
};
off = function() {
$bg
.addClass('fixed')
.css('transform', 'none');
$window
.off('scroll._parallax');
};
// Disable parallax on ..
if (browser.name == 'ie' // IE
|| browser.name == 'edge' // Edge
|| window.devicePixelRatio > 1 // Retina/HiDPI (= poor performance)
|| browser.mobile) // Mobile devices
off();
// Enable everywhere else.
else {
breakpoints.on('>large', on);
breakpoints.on('<=large', off);
}
});
$window
.off('load._parallax resize._parallax')
.on('load._parallax resize._parallax', function() {
$window.trigger('scroll');
});
return $(this);
};
// Play initial animations on page load.
$window.on('load', function() {
window.setTimeout(function() {
$body.removeClass('is-preload');
}, 100);
});
// Scrolly.
$('.scrolly').scrolly();
// Background.
$wrapper._parallax(0.925);
// Nav Panel.
// Toggle.
$navPanelToggle = $(
'<a href="#navPanel" id="navPanelToggle">Menu</a>'
)
.appendTo($wrapper);
// Change toggle styling once we've scrolled past the header.
$header.scrollex({
bottom: '5vh',
enter: function() {
$navPanelToggle.removeClass('alt');
},
leave: function() {
$navPanelToggle.addClass('alt');
}
});
// Panel.
$navPanel = $(
'<div id="navPanel">' +
'<nav>' +
'</nav>' +
'<a href="#navPanel" class="close"></a>' +
'</div>'
)
.appendTo($body)
.panel({
delay: 500,
hideOnClick: true,
hideOnSwipe: true,
resetScroll: true,
resetForms: true,
side: 'right',
target: $body,
visibleClass: 'is-navPanel-visible'
});
// Get inner.
$navPanelInner = $navPanel.children('nav');
// Move nav content on breakpoint change.
var $navContent = $nav.children();
breakpoints.on('>medium', function() {
// NavPanel -> Nav.
$navContent.appendTo($nav);
// Flip icon classes.
$nav.find('.icons, .icon')
.removeClass('alt');
});
breakpoints.on('<=medium', function() {
// Nav -> NavPanel.
$navContent.appendTo($navPanelInner);
// Flip icon classes.
$navPanelInner.find('.icons, .icon')
.addClass('alt');
});
// Hack: Disable transitions on WP.
if (browser.os == 'wp'
&& browser.osVersion < 10)
$navPanel
.css('transition', 'none');
// Intro.
var $intro = $('#intro');
if ($intro.length > 0) {
// Hack: Fix flex min-height on IE.
if (browser.name == 'ie') {
$window.on('resize.ie-intro-fix', function() {
var h = $intro.height();
if (h > $window.height())
$intro.css('height', 'auto');
else
$intro.css('height', h);
}).trigger('resize.ie-intro-fix');
}
// Hide intro on scroll (> small).
breakpoints.on('>small', function() {
$main.unscrollex();
$main.scrollex({
mode: 'bottom',
top: '25vh',
bottom: '-50vh',
enter: function() {
$intro.addClass('hidden');
},
leave: function() {
$intro.removeClass('hidden');
}
});
});
// Hide intro on scroll (<= small).
breakpoints.on('<=small', function() {
$main.unscrollex();
$main.scrollex({
mode: 'middle',
top: '15vh',
bottom: '-15vh',
enter: function() {
$intro.addClass('hidden');
},
leave: function() {
$intro.removeClass('hidden');
}
});
});
}
})(jQuery);
(function (jq) {
var $window = $(window),
$body = $('body'),
$wrapper = $('#wrapper'),
$header = $('#header'),
$nav = $('#nav'),
$main = $('#main'),
$navPanelToggle,
$navPanel,
$navPanelInner;
// Breakpoints.
breakpoints({
default: ['1681px', null],
xlarge: ['1281px', '1680px'],
large: ['981px', '1280px'],
medium: ['737px', '980px'],
small: ['481px', '736px'],
xsmall: ['361px', '480px'],
xxsmall: [null, '360px'],
});
/**
* Applies parallax scrolling to an element's background image.
* @return {jQuery} jQuery object.
*/
$.fn._parallax = function (intensity) {
var $window = $(window),
$this = $(this);
if (this.length == 0 || intensity === 0) return $this;
if (this.length > 1) {
for (var i = 0; i < this.length; i++) $(this[i])._parallax(intensity);
return $this;
}
if (!intensity) {
intensity = 0.25;
}
$this.each(function () {
var $t = $(this),
$bg = $('<div class="bg"></div>').appendTo($t),
on,
off;
on = function () {
$bg.removeClass('fixed').css('transform', 'matrix(1,0,0,1,0,0)');
$window.on('scroll._parallax', function () {
var pos = parseInt($window.scrollTop()) - parseInt($t.position().top);
$bg.css('transform', 'matrix(1,0,0,1,0,' + pos * intensity + ')');
});
};
off = function () {
$bg.addClass('fixed').css('transform', 'none');
$window.off('scroll._parallax');
};
// Disable parallax on ..
if (
browser.name == 'ie' || // IE
browser.name == 'edge' || // Edge
window.devicePixelRatio > 1 || // Retina/HiDPI (= poor performance)
browser.mobile
)
// Mobile devices
off();
// Enable everywhere else.
else {
breakpoints.on('>large', on);
breakpoints.on('<=large', off);
}
});
$window.off('load._parallax resize._parallax').on('load._parallax resize._parallax', function () {
$window.trigger('scroll');
});
return $(this);
};
// Play initial animations on page load.
$window.on('load', function () {
window.setTimeout(function () {
$body.removeClass('is-preload');
}, 100);
});
// Scrolly.
$('.scrolly').scrolly();
// Background.
$wrapper._parallax(0.925);
// Nav Panel.
// Toggle.
$navPanelToggle = $('<a href="#navPanel" id="navPanelToggle">Menu</a>').appendTo($wrapper);
// Change toggle styling once we've scrolled past the header.
$header.scrollex({
bottom: '5vh',
enter: function () {
$navPanelToggle.removeClass('alt');
},
leave: function () {
$navPanelToggle.addClass('alt');
},
});
// Panel.
$navPanel = $('<div id="navPanel">' + '<nav>' + '</nav>' + '<a href="#navPanel" class="close"></a>' + '</div>')
.appendTo($body)
.panel({
delay: 500,
hideOnClick: true,
hideOnSwipe: true,
resetScroll: true,
resetForms: true,
side: 'right',
target: $body,
visibleClass: 'is-navPanel-visible',
});
// Get inner.
$navPanelInner = $navPanel.children('nav');
// Move nav content on breakpoint change.
var $navContent = $nav.children();
breakpoints.on('>medium', function () {
// NavPanel -> Nav.
$navContent.appendTo($nav);
// Flip icon classes.
$nav.find('.icons, .icon').removeClass('alt');
});
breakpoints.on('<=medium', function () {
// Nav -> NavPanel.
$navContent.appendTo($navPanelInner);
// Flip icon classes.
$navPanelInner.find('.icons, .icon').addClass('alt');
});
// Hack: Disable transitions on WP.
if (browser.os == 'wp' && browser.osVersion < 10) $navPanel.css('transition', 'none');
// Intro.
var $intro = $('#intro');
if ($intro.length > 0) {
// Hack: Fix flex min-height on IE.
if (browser.name == 'ie') {
$window
.on('resize.ie-intro-fix', function () {
var h = $intro.height();
if (h > $window.height()) $intro.css('height', 'auto');
else $intro.css('height', h);
})
.trigger('resize.ie-intro-fix');
}
// Hide intro on scroll (> small).
breakpoints.on('>small', function () {
$main.unscrollex();
$main.scrollex({
mode: 'bottom',
top: '25vh',
bottom: '-50vh',
enter: function () {
$intro.addClass('hidden');
},
leave: function () {
$intro.removeClass('hidden');
},
});
});
// Hide intro on scroll (<= small).
breakpoints.on('<=small', function () {
$main.unscrollex();
$main.scrollex({
mode: 'middle',
top: '15vh',
bottom: '-15vh',
enter: function () {
$intro.addClass('hidden');
},
leave: function () {
$intro.removeClass('hidden');
},
});
});
}
})(jQuery);
@@ -0,0 +1,2 @@
# eslint-config-airbnb-base@15 deklariert nur ESLint 7/8, das Setup läuft auf ESLint 9.
legacy-peer-deps=true
@@ -0,0 +1,5 @@
node_modules
assets
dev/js/libs
package-lock.json
dev/js/modules/ternaryLintExample.js
@@ -0,0 +1,6 @@
{
"singleQuote": true,
"trailingComma": "es5",
"arrowParens": "always",
"printWidth": 100
}
@@ -0,0 +1,3 @@
{
"recommendations": ["dbaeumer.vscode-eslint"]
}
@@ -0,0 +1,6 @@
{
"eslint.enable": true,
"eslint.useFlatConfig": true,
"eslint.validate": ["javascript"],
"eslint.workingDirectories": [{ "mode": "auto" }]
}
@@ -0,0 +1,47 @@
# ESLint und Prettier
ESLint prüft den JavaScript-Code. Die Konfiguration steht in `eslint.config.mjs`. Basis ist **Airbnb Base**. Darüber liegen zwei Regeln für Klammern bei ternären Operatoren.
## Befehle
```bash
npm run lint
npm run lint:fix
npm run format
npm run dev
```
`lint` meldet Verstöße. `lint:fix` korrigiert, was automatisch behebbar ist. `format` lässt zuerst Prettier laufen und setzt danach die Ternary-Klammern mit ESLint wieder. `dev` startet die Seite und schreibt die aktuellen Lint-Fehler in die geladene Seite.
Im Editor zeigt die Erweiterung **ESLint** (`dbaeumer.vscode-eslint`) dieselben Fehler als Wellenlinie und im Panel „Probleme“, sobald die Datei offen ist. Die Erweiterung ist für dieses Projekt empfohlen. Wenn nach der Installation noch nichts markiert ist, das Fenster neu laden.
Geprüft werden die eigenen Dateien. Ignoriert werden `node_modules`, das Bundle unter `assets/` und die mitgelieferte UUID-Bibliothek in `dev/js/libs`.
## Klammern bei ternären Operatoren
Die Bedingung eines ternären Operators soll in Klammern stehen:
```javascript
const status = (isActive) ? 'online' : 'offline';
```
- `local/ternary-condition-parens` verlangt die Klammern und setzt sie beim Fix.
- `@stylistic/no-extra-parens` verbietet überflüssige Klammern an anderen Stellen und lässt die Klammern um eine Ternary-Bedingung stehen.
Verschachtelte Ternaries sind erlaubt (`no-nested-ternary` ist aus).
## Prettier
Die Prettier-Konfiguration liegt in `.prettierrc.json` (einfache Anführungszeichen, Zeilenbreite 100, Klammern um Arrow-Parameter).
Prettier hat keine Einstellung, die `(isActive)` behält. Ein einzelnes `prettier --write` entfernt diese Klammern. `npm run format` führt deshalb danach `eslint --fix` aus. Der zweite Schritt setzt die Klammern zurück. Die Beispieldatei ist von beiden Schritten ausgenommen, damit die absichtlichen Fehler dort stehen bleiben.
## Meldungen in der laufenden Seite
ESLint liest die Dateien, bevor sie ausgeführt werden. `(isActive) ? 'online' : 'offline'` und `isActive ? 'online' : 'offline'` sind beide gültiges JavaScript. Der Browser wirft dafür keine Exception.
`npm run dev` prüft die Dateien bei jedem Build erneut. Die Fehler erscheinen oben auf der Seite und als `console.error` in der Browser-Konsole. Dieselben Texte liefert `npm run lint` im Terminal.
## Beispiel
`dev/js/modules/ternaryLintExample.js` enthält gültige und ungültige Zeilen. Die Datei hängt nicht am Programmcode, ihre Lint-Fehler stehen aber mit auf der Seite. `npm run lint:fix` schreibt die Beispielzeilen um.
@@ -0,0 +1,78 @@
(() => {
// node_modules/uuid/dist/stringify.js
var byteToHex = [];
for (let i = 0; i < 256; ++i) {
byteToHex.push((i + 256).toString(16).slice(1));
}
function unsafeStringify(arr, offset = 0) {
return (byteToHex[arr[offset + 0]] + byteToHex[arr[offset + 1]] + byteToHex[arr[offset + 2]] + byteToHex[arr[offset + 3]] + "-" + byteToHex[arr[offset + 4]] + byteToHex[arr[offset + 5]] + "-" + byteToHex[arr[offset + 6]] + byteToHex[arr[offset + 7]] + "-" + byteToHex[arr[offset + 8]] + byteToHex[arr[offset + 9]] + "-" + byteToHex[arr[offset + 10]] + byteToHex[arr[offset + 11]] + byteToHex[arr[offset + 12]] + byteToHex[arr[offset + 13]] + byteToHex[arr[offset + 14]] + byteToHex[arr[offset + 15]]).toLowerCase();
}
// node_modules/uuid/dist/rng.js
var rnds8 = new Uint8Array(16);
function rng() {
return crypto.getRandomValues(rnds8);
}
// node_modules/uuid/dist/v4.js
function v4(options, buf, offset) {
if (!buf && !options && crypto.randomUUID) {
return crypto.randomUUID();
}
return _v4(options, buf, offset);
}
function _v4(options, buf, offset) {
options = options || {};
const rnds = options.random ?? options.rng?.() ?? rng();
if (rnds.length < 16) {
throw new Error("Random bytes length must be >= 16");
}
rnds[6] = rnds[6] & 15 | 64;
rnds[8] = rnds[8] & 63 | 128;
if (buf) {
offset = offset || 0;
if (offset < 0 || offset + 16 > buf.length) {
throw new RangeError(`UUID byte range ${offset}:${offset + 15} is out of buffer bounds`);
}
for (let i = 0; i < 16; ++i) {
buf[offset + i] = rnds[i];
}
return buf;
}
return unsafeStringify(rnds);
}
var v4_default = v4;
// dev/js/modules/Counter.js
var Counter = (elOrSelector = null) => {
const module = elOrSelector || document.querySelector(elOrSelector);
const countEl = module.querySelector('input[id^="count"]');
const btnEl = module.querySelector('button[id^="incrementButton"]');
const id = v4_default();
countEl.id = `count-${id}`;
btnEl.id = `incrementButton-${id}`;
const counter = { count: 0 };
const init = () => {
btnEl.addEventListener("click", onClickIncrement);
};
const onClickIncrement = () => {
const count = counter.count + 1;
countEl.value = count;
};
init();
};
var Counter_default = Counter;
// dev/js/main.js
(() => {
const DOM = {
counters: Array.from(document.querySelectorAll(".counter"))
};
const init = () => {
DOM.counters.forEach((el) => {
Counter_default(el);
});
};
init();
})();
})();
@@ -0,0 +1,28 @@
(() => {
const findings = [{"file":"dev/js/modules/Counter.js","line":22,"column":37,"rule":"no-use-before-define","message":"'onClickIncrement' was used before it was defined."},{"file":"dev/js/modules/ternaryLintExample.js","line":39,"column":12,"rule":"@stylistic/no-extra-parens","message":"Unnecessary parentheses around expression."}];
if (findings.length === 0) {
return;
}
const box = document.createElement('div');
box.className = 'alert alert-danger m-3';
box.setAttribute('role', 'alert');
const title = document.createElement('strong');
title.textContent = 'ESLint: ' + findings.length + ' Fehler';
box.append(title);
const list = document.createElement('ul');
list.className = 'mb-0 mt-2';
for (const finding of findings) {
const text = finding.file + ':' + finding.line + ':' + finding.column + ' ' + finding.message + ' (' + finding.rule + ')';
console.error('[ESLint] ' + text);
const item = document.createElement('li');
item.textContent = text;
list.append(item);
}
box.append(list);
document.body.prepend(box);
})();
@@ -0,0 +1,15 @@
export { default as MAX } from './max.js';
export { default as NIL } from './nil.js';
export { default as parse } from './parse.js';
export { default as stringify } from './stringify.js';
export type * from './types.js';
export { default as v1 } from './v1.js';
export { default as v1ToV6 } from './v1ToV6.js';
export { default as v3 } from './v3.js';
export { default as v4 } from './v4.js';
export { default as v5 } from './v5.js';
export { default as v6 } from './v6.js';
export { default as v6ToV1 } from './v6ToV1.js';
export { default as v7 } from './v7.js';
export { default as validate } from './validate.js';
export { default as version } from './version.js';
@@ -0,0 +1,14 @@
export { default as MAX } from './max.js';
export { default as NIL } from './nil.js';
export { default as parse } from './parse.js';
export { default as stringify } from './stringify.js';
export { default as v1 } from './v1.js';
export { default as v1ToV6 } from './v1ToV6.js';
export { default as v3 } from './v3.js';
export { default as v4 } from './v4.js';
export { default as v5 } from './v5.js';
export { default as v6 } from './v6.js';
export { default as v6ToV1 } from './v6ToV1.js';
export { default as v7 } from './v7.js';
export { default as validate } from './validate.js';
export { default as version } from './version.js';
@@ -0,0 +1,2 @@
declare const _default: "ffffffff-ffff-ffff-ffff-ffffffffffff";
export default _default;
@@ -0,0 +1 @@
export default 'ffffffff-ffff-ffff-ffff-ffffffffffff';
@@ -0,0 +1,2 @@
declare function md5(bytes: Uint8Array): Uint8Array;
export default md5;
@@ -0,0 +1,135 @@
function md5(bytes) {
const words = uint8ToUint32(bytes);
const md5Bytes = wordsToMd5(words, bytes.length * 8);
return uint32ToUint8(md5Bytes);
}
function uint32ToUint8(input) {
const bytes = new Uint8Array(input.length * 4);
for (let i = 0; i < input.length * 4; i++) {
bytes[i] = (input[i >> 2] >>> ((i % 4) * 8)) & 0xff;
}
return bytes;
}
function getOutputLength(inputLength8) {
return (((inputLength8 + 64) >>> 9) << 4) + 14 + 1;
}
function wordsToMd5(x, len) {
const xpad = new Uint32Array(getOutputLength(len)).fill(0);
xpad.set(x);
xpad[len >> 5] |= 0x80 << (len % 32);
xpad[xpad.length - 1] = len;
x = xpad;
let a = 1732584193;
let b = -271733879;
let c = -1732584194;
let d = 271733878;
for (let i = 0; i < x.length; i += 16) {
const olda = a;
const oldb = b;
const oldc = c;
const oldd = d;
a = md5ff(a, b, c, d, x[i], 7, -680876936);
d = md5ff(d, a, b, c, x[i + 1], 12, -389564586);
c = md5ff(c, d, a, b, x[i + 2], 17, 606105819);
b = md5ff(b, c, d, a, x[i + 3], 22, -1044525330);
a = md5ff(a, b, c, d, x[i + 4], 7, -176418897);
d = md5ff(d, a, b, c, x[i + 5], 12, 1200080426);
c = md5ff(c, d, a, b, x[i + 6], 17, -1473231341);
b = md5ff(b, c, d, a, x[i + 7], 22, -45705983);
a = md5ff(a, b, c, d, x[i + 8], 7, 1770035416);
d = md5ff(d, a, b, c, x[i + 9], 12, -1958414417);
c = md5ff(c, d, a, b, x[i + 10], 17, -42063);
b = md5ff(b, c, d, a, x[i + 11], 22, -1990404162);
a = md5ff(a, b, c, d, x[i + 12], 7, 1804603682);
d = md5ff(d, a, b, c, x[i + 13], 12, -40341101);
c = md5ff(c, d, a, b, x[i + 14], 17, -1502002290);
b = md5ff(b, c, d, a, x[i + 15], 22, 1236535329);
a = md5gg(a, b, c, d, x[i + 1], 5, -165796510);
d = md5gg(d, a, b, c, x[i + 6], 9, -1069501632);
c = md5gg(c, d, a, b, x[i + 11], 14, 643717713);
b = md5gg(b, c, d, a, x[i], 20, -373897302);
a = md5gg(a, b, c, d, x[i + 5], 5, -701558691);
d = md5gg(d, a, b, c, x[i + 10], 9, 38016083);
c = md5gg(c, d, a, b, x[i + 15], 14, -660478335);
b = md5gg(b, c, d, a, x[i + 4], 20, -405537848);
a = md5gg(a, b, c, d, x[i + 9], 5, 568446438);
d = md5gg(d, a, b, c, x[i + 14], 9, -1019803690);
c = md5gg(c, d, a, b, x[i + 3], 14, -187363961);
b = md5gg(b, c, d, a, x[i + 8], 20, 1163531501);
a = md5gg(a, b, c, d, x[i + 13], 5, -1444681467);
d = md5gg(d, a, b, c, x[i + 2], 9, -51403784);
c = md5gg(c, d, a, b, x[i + 7], 14, 1735328473);
b = md5gg(b, c, d, a, x[i + 12], 20, -1926607734);
a = md5hh(a, b, c, d, x[i + 5], 4, -378558);
d = md5hh(d, a, b, c, x[i + 8], 11, -2022574463);
c = md5hh(c, d, a, b, x[i + 11], 16, 1839030562);
b = md5hh(b, c, d, a, x[i + 14], 23, -35309556);
a = md5hh(a, b, c, d, x[i + 1], 4, -1530992060);
d = md5hh(d, a, b, c, x[i + 4], 11, 1272893353);
c = md5hh(c, d, a, b, x[i + 7], 16, -155497632);
b = md5hh(b, c, d, a, x[i + 10], 23, -1094730640);
a = md5hh(a, b, c, d, x[i + 13], 4, 681279174);
d = md5hh(d, a, b, c, x[i], 11, -358537222);
c = md5hh(c, d, a, b, x[i + 3], 16, -722521979);
b = md5hh(b, c, d, a, x[i + 6], 23, 76029189);
a = md5hh(a, b, c, d, x[i + 9], 4, -640364487);
d = md5hh(d, a, b, c, x[i + 12], 11, -421815835);
c = md5hh(c, d, a, b, x[i + 15], 16, 530742520);
b = md5hh(b, c, d, a, x[i + 2], 23, -995338651);
a = md5ii(a, b, c, d, x[i], 6, -198630844);
d = md5ii(d, a, b, c, x[i + 7], 10, 1126891415);
c = md5ii(c, d, a, b, x[i + 14], 15, -1416354905);
b = md5ii(b, c, d, a, x[i + 5], 21, -57434055);
a = md5ii(a, b, c, d, x[i + 12], 6, 1700485571);
d = md5ii(d, a, b, c, x[i + 3], 10, -1894986606);
c = md5ii(c, d, a, b, x[i + 10], 15, -1051523);
b = md5ii(b, c, d, a, x[i + 1], 21, -2054922799);
a = md5ii(a, b, c, d, x[i + 8], 6, 1873313359);
d = md5ii(d, a, b, c, x[i + 15], 10, -30611744);
c = md5ii(c, d, a, b, x[i + 6], 15, -1560198380);
b = md5ii(b, c, d, a, x[i + 13], 21, 1309151649);
a = md5ii(a, b, c, d, x[i + 4], 6, -145523070);
d = md5ii(d, a, b, c, x[i + 11], 10, -1120210379);
c = md5ii(c, d, a, b, x[i + 2], 15, 718787259);
b = md5ii(b, c, d, a, x[i + 9], 21, -343485551);
a = safeAdd(a, olda);
b = safeAdd(b, oldb);
c = safeAdd(c, oldc);
d = safeAdd(d, oldd);
}
return Uint32Array.of(a, b, c, d);
}
function uint8ToUint32(input) {
if (input.length === 0) {
return new Uint32Array();
}
const output = new Uint32Array(getOutputLength(input.length * 8)).fill(0);
for (let i = 0; i < input.length; i++) {
output[i >> 2] |= (input[i] & 0xff) << ((i % 4) * 8);
}
return output;
}
function safeAdd(x, y) {
const lsw = (x & 0xffff) + (y & 0xffff);
const msw = (x >> 16) + (y >> 16) + (lsw >> 16);
return (msw << 16) | (lsw & 0xffff);
}
function bitRotateLeft(num, cnt) {
return (num << cnt) | (num >>> (32 - cnt));
}
function md5cmn(q, a, b, x, s, t) {
return safeAdd(bitRotateLeft(safeAdd(safeAdd(a, q), safeAdd(x, t)), s), b);
}
function md5ff(a, b, c, d, x, s, t) {
return md5cmn((b & c) | (~b & d), a, b, x, s, t);
}
function md5gg(a, b, c, d, x, s, t) {
return md5cmn((b & d) | (c & ~d), a, b, x, s, t);
}
function md5hh(a, b, c, d, x, s, t) {
return md5cmn(b ^ c ^ d, a, b, x, s, t);
}
function md5ii(a, b, c, d, x, s, t) {
return md5cmn(c ^ (b | ~d), a, b, x, s, t);
}
export default md5;
@@ -0,0 +1,2 @@
declare const _default: "00000000-0000-0000-0000-000000000000";
export default _default;
@@ -0,0 +1 @@
export default '00000000-0000-0000-0000-000000000000';
@@ -0,0 +1,3 @@
import type { NonSharedArrayBuffer } from './types.js';
declare function parse(uuid: string): NonSharedArrayBuffer;
export default parse;
@@ -0,0 +1,9 @@
import validate from './validate.js';
function parse(uuid) {
if (!validate(uuid)) {
throw TypeError('Invalid UUID');
}
let v;
return Uint8Array.of((v = parseInt(uuid.slice(0, 8), 16)) >>> 24, (v >>> 16) & 0xff, (v >>> 8) & 0xff, v & 0xff, (v = parseInt(uuid.slice(9, 13), 16)) >>> 8, v & 0xff, (v = parseInt(uuid.slice(14, 18), 16)) >>> 8, v & 0xff, (v = parseInt(uuid.slice(19, 23), 16)) >>> 8, v & 0xff, ((v = parseInt(uuid.slice(24, 36), 16)) / 0x10000000000) & 0xff, (v / 0x100000000) & 0xff, (v >>> 24) & 0xff, (v >>> 16) & 0xff, (v >>> 8) & 0xff, v & 0xff);
}
export default parse;
@@ -0,0 +1,2 @@
declare const _default: RegExp;
export default _default;
@@ -0,0 +1 @@
export default /^(?:[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$/i;
@@ -0,0 +1 @@
export default function rng(): Uint8Array;
@@ -0,0 +1,4 @@
const rnds8 = new Uint8Array(16);
export default function rng() {
return crypto.getRandomValues(rnds8);
}
@@ -0,0 +1,2 @@
declare function sha1(bytes: Uint8Array): Uint8Array;
export default sha1;
@@ -0,0 +1,70 @@
function f(s, x, y, z) {
switch (s) {
case 0:
return (x & y) ^ (~x & z);
case 1:
return x ^ y ^ z;
case 2:
return (x & y) ^ (x & z) ^ (y & z);
case 3:
return x ^ y ^ z;
}
}
function ROTL(x, n) {
return (x << n) | (x >>> (32 - n));
}
function sha1(bytes) {
const K = [0x5a827999, 0x6ed9eba1, 0x8f1bbcdc, 0xca62c1d6];
const H = [0x67452301, 0xefcdab89, 0x98badcfe, 0x10325476, 0xc3d2e1f0];
const newBytes = new Uint8Array(bytes.length + 1);
newBytes.set(bytes);
newBytes[bytes.length] = 0x80;
bytes = newBytes;
const l = bytes.length / 4 + 2;
const N = Math.ceil(l / 16);
const M = new Array(N);
for (let i = 0; i < N; ++i) {
const arr = new Uint32Array(16);
for (let j = 0; j < 16; ++j) {
arr[j] =
(bytes[i * 64 + j * 4] << 24) |
(bytes[i * 64 + j * 4 + 1] << 16) |
(bytes[i * 64 + j * 4 + 2] << 8) |
bytes[i * 64 + j * 4 + 3];
}
M[i] = arr;
}
M[N - 1][14] = ((bytes.length - 1) * 8) / 2 ** 32;
M[N - 1][14] = Math.floor(M[N - 1][14]);
M[N - 1][15] = ((bytes.length - 1) * 8) & 0xffffffff;
for (let i = 0; i < N; ++i) {
const W = new Uint32Array(80);
for (let t = 0; t < 16; ++t) {
W[t] = M[i][t];
}
for (let t = 16; t < 80; ++t) {
W[t] = ROTL(W[t - 3] ^ W[t - 8] ^ W[t - 14] ^ W[t - 16], 1);
}
let a = H[0];
let b = H[1];
let c = H[2];
let d = H[3];
let e = H[4];
for (let t = 0; t < 80; ++t) {
const s = Math.floor(t / 20);
const T = (ROTL(a, 5) + f(s, b, c, d) + e + K[s] + W[t]) >>> 0;
e = d;
d = c;
c = ROTL(b, 30) >>> 0;
b = a;
a = T;
}
H[0] = (H[0] + a) >>> 0;
H[1] = (H[1] + b) >>> 0;
H[2] = (H[2] + c) >>> 0;
H[3] = (H[3] + d) >>> 0;
H[4] = (H[4] + e) >>> 0;
}
return Uint8Array.of(H[0] >> 24, H[0] >> 16, H[0] >> 8, H[0], H[1] >> 24, H[1] >> 16, H[1] >> 8, H[1], H[2] >> 24, H[2] >> 16, H[2] >> 8, H[2], H[3] >> 24, H[3] >> 16, H[3] >> 8, H[3], H[4] >> 24, H[4] >> 16, H[4] >> 8, H[4]);
}
export default sha1;
@@ -0,0 +1,3 @@
export declare function unsafeStringify(arr: Uint8Array, offset?: number): string;
declare function stringify(arr: Uint8Array, offset?: number): string;
export default stringify;
@@ -0,0 +1,35 @@
import validate from './validate.js';
const byteToHex = [];
for (let i = 0; i < 256; ++i) {
byteToHex.push((i + 0x100).toString(16).slice(1));
}
export function unsafeStringify(arr, offset = 0) {
return (byteToHex[arr[offset + 0]] +
byteToHex[arr[offset + 1]] +
byteToHex[arr[offset + 2]] +
byteToHex[arr[offset + 3]] +
'-' +
byteToHex[arr[offset + 4]] +
byteToHex[arr[offset + 5]] +
'-' +
byteToHex[arr[offset + 6]] +
byteToHex[arr[offset + 7]] +
'-' +
byteToHex[arr[offset + 8]] +
byteToHex[arr[offset + 9]] +
'-' +
byteToHex[arr[offset + 10]] +
byteToHex[arr[offset + 11]] +
byteToHex[arr[offset + 12]] +
byteToHex[arr[offset + 13]] +
byteToHex[arr[offset + 14]] +
byteToHex[arr[offset + 15]]).toLowerCase();
}
function stringify(arr, offset = 0) {
const uuid = unsafeStringify(arr, offset);
if (!validate(uuid)) {
throw TypeError('Stringified UUID is invalid');
}
return uuid;
}
export default stringify;
@@ -0,0 +1,22 @@
export type UUIDTypes<TBuf extends Uint8Array = Uint8Array> = string | TBuf;
export type Version1Options = {
node?: Uint8Array;
clockseq?: number;
random?: Uint8Array;
rng?: () => Uint8Array;
msecs?: number;
nsecs?: number;
_v6?: boolean;
};
export type Version4Options = {
random?: Uint8Array;
rng?: () => Uint8Array;
};
export type Version6Options = Version1Options;
export type Version7Options = {
random?: Uint8Array;
msecs?: number;
seq?: number;
rng?: () => Uint8Array;
};
export type NonSharedArrayBuffer = ReturnType<typeof Uint8Array.of>;
@@ -0,0 +1 @@
export {};
@@ -0,0 +1,70 @@
import * as assert from 'node:assert/strict';
import v1 from './v1.js';
import v3 from './v3.js';
import v4 from './v4.js';
import v5 from './v5.js';
import v6 from './v6.js';
import v7 from './v7.js';
function usage() {
console.log('Usage:');
console.log(' uuid');
console.log(' uuid v1');
console.log(' uuid v3 <name> <namespace uuid>');
console.log(' uuid v4');
console.log(' uuid v5 <name> <namespace uuid>');
console.log(' uuid v6');
console.log(' uuid v7');
console.log(' uuid --help');
console.log('\nNote: <namespace uuid> may be "URL" or "DNS" to use the corresponding UUIDs defined by RFC9562');
}
const args = process.argv.slice(2);
if (args.indexOf('--help') >= 0) {
usage();
process.exit(0);
}
const version = args.shift() || 'v4';
switch (version) {
case 'v1':
console.log(v1());
break;
case 'v3': {
const name = args.shift();
let namespace = args.shift();
assert.ok(name != null, 'v3 name not specified');
assert.ok(namespace != null, 'v3 namespace not specified');
if (namespace === 'URL') {
namespace = v3.URL;
}
if (namespace === 'DNS') {
namespace = v3.DNS;
}
console.log(v3(name, namespace));
break;
}
case 'v4':
console.log(v4());
break;
case 'v5': {
const name = args.shift();
let namespace = args.shift();
assert.ok(name != null, 'v5 name not specified');
assert.ok(namespace != null, 'v5 namespace not specified');
if (namespace === 'URL') {
namespace = v5.URL;
}
if (namespace === 'DNS') {
namespace = v5.DNS;
}
console.log(v5(name, namespace));
break;
}
case 'v6':
console.log(v6());
break;
case 'v7':
console.log(v7());
break;
default:
usage();
process.exit(1);
}
@@ -0,0 +1,11 @@
import type { Version1Options } from './types.js';
type V1State = {
node?: Uint8Array;
clockseq?: number;
msecs?: number;
nsecs?: number;
};
declare function v1(options?: Version1Options, buf?: undefined, offset?: number): string;
declare function v1<Buf extends Uint8Array = Uint8Array>(options: Version1Options | undefined, buf: Buf, offset?: number): Buf;
export declare function updateV1State(state: V1State, now: number, rnds: Uint8Array): V1State;
export default v1;
@@ -0,0 +1,87 @@
import rng from './rng.js';
import { unsafeStringify } from './stringify.js';
const _state = {};
function v1(options, buf, offset) {
let bytes;
const isV6 = options?._v6 ?? false;
if (options) {
const optionsKeys = Object.keys(options);
if (optionsKeys.length === 1 && optionsKeys[0] === '_v6') {
options = undefined;
}
}
if (options) {
bytes = v1Bytes(options.random ?? options.rng?.() ?? rng(), options.msecs, options.nsecs, options.clockseq, options.node, buf, offset);
}
else {
const now = Date.now();
const rnds = rng();
updateV1State(_state, now, rnds);
bytes = v1Bytes(rnds, _state.msecs, _state.nsecs, isV6 ? undefined : _state.clockseq, isV6 ? undefined : _state.node, buf, offset);
}
return buf ?? unsafeStringify(bytes);
}
export function updateV1State(state, now, rnds) {
state.msecs ??= -Infinity;
state.nsecs ??= 0;
if (now === state.msecs) {
state.nsecs++;
if (state.nsecs >= 10000) {
state.node = undefined;
state.nsecs = 0;
}
}
else if (now > state.msecs) {
state.nsecs = 0;
}
else if (now < state.msecs) {
state.node = undefined;
}
if (!state.node) {
state.node = rnds.slice(10, 16);
state.node[0] |= 0x01;
state.clockseq = ((rnds[8] << 8) | rnds[9]) & 0x3fff;
}
state.msecs = now;
return state;
}
function v1Bytes(rnds, msecs, nsecs, clockseq, node, buf, offset = 0) {
if (rnds.length < 16) {
throw new Error('Random bytes length must be >= 16');
}
if (!buf) {
buf = new Uint8Array(16);
offset = 0;
}
else {
if (offset < 0 || offset + 16 > buf.length) {
throw new RangeError(`UUID byte range ${offset}:${offset + 15} is out of buffer bounds`);
}
}
msecs ??= Date.now();
nsecs ??= 0;
clockseq ??= ((rnds[8] << 8) | rnds[9]) & 0x3fff;
if (node == null) {
node = rnds.slice(10, 16);
node[0] |= 0x01;
}
msecs += 12219292800000;
const t = (msecs & 0xfffffff) * 10000 + nsecs;
const tl = t >>> 0;
buf[offset++] = (tl >>> 24) & 0xff;
buf[offset++] = (tl >>> 16) & 0xff;
buf[offset++] = (tl >>> 8) & 0xff;
buf[offset++] = tl & 0xff;
const tmh = (((msecs / 0x10000000) | 0) * 625 + ((t / 0x100000000) | 0)) & 0xfffffff;
buf[offset++] = (tmh >>> 8) & 0xff;
buf[offset++] = tmh & 0xff;
buf[offset++] = ((tmh >>> 24) & 0xf) | 0x10;
buf[offset++] = (tmh >>> 16) & 0xff;
buf[offset++] = (clockseq >>> 8) | 0x80;
buf[offset++] = clockseq & 0xff;
for (let n = 0; n < 6; ++n) {
buf[offset++] = node[n];
}
return buf;
}
export default v1;
@@ -0,0 +1,3 @@
import type { NonSharedArrayBuffer } from './types.js';
export default function v1ToV6(uuid: string): string;
export default function v1ToV6(uuid: Uint8Array): NonSharedArrayBuffer;
@@ -0,0 +1,10 @@
import parse from './parse.js';
import { unsafeStringify } from './stringify.js';
export default function v1ToV6(uuid) {
const v1Bytes = typeof uuid === 'string' ? parse(uuid) : uuid;
const v6Bytes = _v1ToV6(v1Bytes);
return typeof uuid === 'string' ? unsafeStringify(v6Bytes) : v6Bytes;
}
function _v1ToV6(v1Bytes) {
return Uint8Array.of(((v1Bytes[6] & 0x0f) << 4) | ((v1Bytes[7] >> 4) & 0x0f), ((v1Bytes[7] & 0x0f) << 4) | ((v1Bytes[4] & 0xf0) >> 4), ((v1Bytes[4] & 0x0f) << 4) | ((v1Bytes[5] & 0xf0) >> 4), ((v1Bytes[5] & 0x0f) << 4) | ((v1Bytes[0] & 0xf0) >> 4), ((v1Bytes[0] & 0x0f) << 4) | ((v1Bytes[1] & 0xf0) >> 4), ((v1Bytes[1] & 0x0f) << 4) | ((v1Bytes[2] & 0xf0) >> 4), 0x60 | (v1Bytes[2] & 0x0f), v1Bytes[3], v1Bytes[8], v1Bytes[9], v1Bytes[10], v1Bytes[11], v1Bytes[12], v1Bytes[13], v1Bytes[14], v1Bytes[15]);
}
@@ -0,0 +1,9 @@
import type { UUIDTypes } from './types.js';
export { DNS, URL } from './v35.js';
declare function v3(value: string | Uint8Array, namespace: UUIDTypes, buf?: undefined, offset?: number): string;
declare function v3<TBuf extends Uint8Array = Uint8Array>(value: string | Uint8Array, namespace: UUIDTypes, buf: TBuf, offset?: number): TBuf;
declare namespace v3 {
var DNS: string;
var URL: string;
}
export default v3;
@@ -0,0 +1,9 @@
import md5 from './md5.js';
import v35, { DNS, URL } from './v35.js';
export { DNS, URL } from './v35.js';
function v3(value, namespace, buf, offset) {
return v35(0x30, md5, value, namespace, buf, offset);
}
v3.DNS = DNS;
v3.URL = URL;
export default v3;
@@ -0,0 +1,7 @@
import type { UUIDTypes } from './types.js';
export declare function stringToBytes(str: string): Uint8Array;
export declare const DNS = "6ba7b810-9dad-11d1-80b4-00c04fd430c8";
export declare const URL = "6ba7b811-9dad-11d1-80b4-00c04fd430c8";
type HashFunction = (bytes: Uint8Array) => Uint8Array;
export default function v35<TBuf extends Uint8Array = Uint8Array>(version: 0x30 | 0x50, hash: HashFunction, value: string | Uint8Array, namespace: UUIDTypes, buf?: TBuf, offset?: number): UUIDTypes<TBuf>;
export {};
@@ -0,0 +1,39 @@
import parse from './parse.js';
import { unsafeStringify } from './stringify.js';
export function stringToBytes(str) {
str = unescape(encodeURIComponent(str));
const bytes = new Uint8Array(str.length);
for (let i = 0; i < str.length; ++i) {
bytes[i] = str.charCodeAt(i);
}
return bytes;
}
export const DNS = '6ba7b810-9dad-11d1-80b4-00c04fd430c8';
export const URL = '6ba7b811-9dad-11d1-80b4-00c04fd430c8';
export default function v35(version, hash, value, namespace, buf, offset) {
const valueBytes = typeof value === 'string' ? stringToBytes(value) : value;
const namespaceBytes = typeof namespace === 'string' ? parse(namespace) : namespace;
if (typeof namespace === 'string') {
namespace = parse(namespace);
}
if (namespace?.length !== 16) {
throw TypeError('Namespace must be array-like (16 iterable integer values, 0-255)');
}
let bytes = new Uint8Array(16 + valueBytes.length);
bytes.set(namespaceBytes);
bytes.set(valueBytes, namespaceBytes.length);
bytes = hash(bytes);
bytes[6] = (bytes[6] & 0x0f) | version;
bytes[8] = (bytes[8] & 0x3f) | 0x80;
if (buf) {
offset ??= 0;
if (offset < 0 || offset + 16 > buf.length) {
throw new RangeError(`UUID byte range ${offset}:${offset + 15} is out of buffer bounds`);
}
for (let i = 0; i < 16; ++i) {
buf[offset + i] = bytes[i];
}
return buf;
}
return unsafeStringify(bytes);
}
@@ -0,0 +1,4 @@
import type { Version4Options } from './types.js';
declare function v4(options?: Version4Options, buf?: undefined, offset?: number): string;
declare function v4<TBuf extends Uint8Array = Uint8Array>(options: Version4Options | undefined, buf: TBuf, offset?: number): TBuf;
export default v4;
@@ -0,0 +1,29 @@
import rng from './rng.js';
import { unsafeStringify } from './stringify.js';
function v4(options, buf, offset) {
if (!buf && !options && crypto.randomUUID) {
return crypto.randomUUID();
}
return _v4(options, buf, offset);
}
function _v4(options, buf, offset) {
options = options || {};
const rnds = options.random ?? options.rng?.() ?? rng();
if (rnds.length < 16) {
throw new Error('Random bytes length must be >= 16');
}
rnds[6] = (rnds[6] & 0x0f) | 0x40;
rnds[8] = (rnds[8] & 0x3f) | 0x80;
if (buf) {
offset = offset || 0;
if (offset < 0 || offset + 16 > buf.length) {
throw new RangeError(`UUID byte range ${offset}:${offset + 15} is out of buffer bounds`);
}
for (let i = 0; i < 16; ++i) {
buf[offset + i] = rnds[i];
}
return buf;
}
return unsafeStringify(rnds);
}
export default v4;
@@ -0,0 +1,9 @@
import type { UUIDTypes } from './types.js';
export { DNS, URL } from './v35.js';
declare function v5(value: string | Uint8Array, namespace: UUIDTypes, buf?: undefined, offset?: number): string;
declare function v5<TBuf extends Uint8Array = Uint8Array>(value: string | Uint8Array, namespace: UUIDTypes, buf: TBuf, offset?: number): TBuf;
declare namespace v5 {
var DNS: string;
var URL: string;
}
export default v5;
@@ -0,0 +1,9 @@
import sha1 from './sha1.js';
import v35, { DNS, URL } from './v35.js';
export { DNS, URL } from './v35.js';
function v5(value, namespace, buf, offset) {
return v35(0x50, sha1, value, namespace, buf, offset);
}
v5.DNS = DNS;
v5.URL = URL;
export default v5;
@@ -0,0 +1,4 @@
import type { Version6Options } from './types.js';
declare function v6(options?: Version6Options, buf?: undefined, offset?: number): string;
declare function v6<TBuf extends Uint8Array = Uint8Array>(options: Version6Options | undefined, buf: TBuf, offset?: number): TBuf;
export default v6;
@@ -0,0 +1,20 @@
import { unsafeStringify } from './stringify.js';
import v1 from './v1.js';
import v1ToV6 from './v1ToV6.js';
function v6(options, buf, offset) {
options ??= {};
offset ??= 0;
let bytes = v1({ ...options, _v6: true }, new Uint8Array(16));
bytes = v1ToV6(bytes);
if (buf) {
if (offset < 0 || offset + 16 > buf.length) {
throw new RangeError(`UUID byte range ${offset}:${offset + 15} is out of buffer bounds`);
}
for (let i = 0; i < 16; i++) {
buf[offset + i] = bytes[i];
}
return buf;
}
return unsafeStringify(bytes);
}
export default v6;
@@ -0,0 +1,2 @@
export default function v6ToV1(uuid: string): string;
export default function v6ToV1(uuid: Uint8Array): Uint8Array;
@@ -0,0 +1,10 @@
import parse from './parse.js';
import { unsafeStringify } from './stringify.js';
export default function v6ToV1(uuid) {
const v6Bytes = typeof uuid === 'string' ? parse(uuid) : uuid;
const v1Bytes = _v6ToV1(v6Bytes);
return typeof uuid === 'string' ? unsafeStringify(v1Bytes) : v1Bytes;
}
function _v6ToV1(v6Bytes) {
return Uint8Array.of(((v6Bytes[3] & 0x0f) << 4) | ((v6Bytes[4] >> 4) & 0x0f), ((v6Bytes[4] & 0x0f) << 4) | ((v6Bytes[5] & 0xf0) >> 4), ((v6Bytes[5] & 0x0f) << 4) | (v6Bytes[6] & 0x0f), v6Bytes[7], ((v6Bytes[1] & 0x0f) << 4) | ((v6Bytes[2] & 0xf0) >> 4), ((v6Bytes[2] & 0x0f) << 4) | ((v6Bytes[3] & 0xf0) >> 4), 0x10 | ((v6Bytes[0] & 0xf0) >> 4), ((v6Bytes[0] & 0x0f) << 4) | ((v6Bytes[1] & 0xf0) >> 4), v6Bytes[8], v6Bytes[9], v6Bytes[10], v6Bytes[11], v6Bytes[12], v6Bytes[13], v6Bytes[14], v6Bytes[15]);
}
@@ -0,0 +1,9 @@
import type { Version7Options } from './types.js';
type V7State = {
msecs?: number;
seq?: number;
};
declare function v7(options?: Version7Options, buf?: undefined, offset?: number): string;
declare function v7<TBuf extends Uint8Array = Uint8Array>(options: Version7Options | undefined, buf: TBuf, offset?: number): TBuf;
export declare function updateV7State(state: V7State, now: number, rnds: Uint8Array): V7State;
export default v7;
@@ -0,0 +1,68 @@
import rng from './rng.js';
import { unsafeStringify } from './stringify.js';
const _state = {};
function v7(options, buf, offset) {
let bytes;
if (options) {
bytes = v7Bytes(options.random ?? options.rng?.() ?? rng(), options.msecs, options.seq, buf, offset);
}
else {
const now = Date.now();
const rnds = rng();
updateV7State(_state, now, rnds);
bytes = v7Bytes(rnds, _state.msecs, _state.seq, buf, offset);
}
return buf ?? unsafeStringify(bytes);
}
export function updateV7State(state, now, rnds) {
state.msecs ??= -Infinity;
state.seq ??= 0;
if (now > state.msecs) {
state.seq = v7Sequence(rnds);
state.msecs = now;
}
else {
state.seq = (state.seq + 1) | 0;
if (state.seq === 0) {
state.msecs++;
}
}
return state;
}
function v7Bytes(rnds, msecs, seq, buf, offset = 0) {
if (rnds.length < 16) {
throw new Error('Random bytes length must be >= 16');
}
if (!buf) {
buf = new Uint8Array(16);
offset = 0;
}
else {
if (offset < 0 || offset + 16 > buf.length) {
throw new RangeError(`UUID byte range ${offset}:${offset + 15} is out of buffer bounds`);
}
}
msecs ??= Date.now();
seq ??= v7Sequence(rnds);
buf[offset++] = (msecs / 0x10000000000) & 0xff;
buf[offset++] = (msecs / 0x100000000) & 0xff;
buf[offset++] = (msecs / 0x1000000) & 0xff;
buf[offset++] = (msecs / 0x10000) & 0xff;
buf[offset++] = (msecs / 0x100) & 0xff;
buf[offset++] = msecs & 0xff;
buf[offset++] = 0x70 | ((seq >>> 28) & 0x0f);
buf[offset++] = (seq >>> 20) & 0xff;
buf[offset++] = 0x80 | ((seq >>> 14) & 0x3f);
buf[offset++] = (seq >>> 6) & 0xff;
buf[offset++] = ((seq << 2) & 0xff) | (rnds[10] & 0x03);
buf[offset++] = rnds[11];
buf[offset++] = rnds[12];
buf[offset++] = rnds[13];
buf[offset++] = rnds[14];
buf[offset++] = rnds[15];
return buf;
}
function v7Sequence(rnds) {
return ((rnds[6] & 0x7f) << 24) | (rnds[7] << 16) | (rnds[8] << 8) | rnds[9];
}
export default v7;
@@ -0,0 +1,2 @@
declare function validate(uuid: unknown): boolean;
export default validate;
@@ -0,0 +1,5 @@
import REGEX from './regex.js';
function validate(uuid) {
return typeof uuid === 'string' && REGEX.test(uuid);
}
export default validate;
@@ -0,0 +1,2 @@
declare function version(uuid: string): number;
export default version;
@@ -0,0 +1,8 @@
import validate from './validate.js';
function version(uuid) {
if (!validate(uuid)) {
throw TypeError('Invalid UUID');
}
return parseInt(uuid.slice(14, 15), 16);
}
export default version;
@@ -0,0 +1,23 @@
import Counter from './modules/Counter.js'; // wichtig mit Dateiendung, da kein Bundler
(() => {
// === DOM & VARS =======
const DOM = {
counters: Array.from(document.querySelectorAll('.counter')),
};
// === INIT =============
const init = () => {
DOM.counters.forEach((el) => {
Counter(el);
});
};
// === EVENTHANDLER =====
// === XHR/FETCH ========
// === FUNCTIONS ========
init();
})();
@@ -0,0 +1,36 @@
import { v4 } from 'uuid';
const Counter = (elOrSelector = null) => {
// ===== DOM & VARS =====
// TODO: forEach for string selector
const module = elOrSelector || document.querySelector(elOrSelector);
const countEl = module.querySelector('input[id^="count"]');
const btnEl = module.querySelector('button[id^="incrementButton"]');
const id = v4();
countEl.id = `count-${id}`;
btnEl.id = `incrementButton-${id}`;
// console.log('id:', id);
const counter = { count: 0 };
// ===== INIT =====
const init = () => {
btnEl.addEventListener('click', onClickIncrement);
};
// ===== EVENT HANDLER =====
const onClickIncrement = () => {
const count = counter.count + 1;
// count++;
countEl.value = count;
};
// ===== CALL INIT =====
init();
};
export default Counter;
@@ -0,0 +1,42 @@
// Demo für die Ternary-Klammerregeln. Diese Datei wird nicht gebündelt.
// Die ESLint-Erweiterung markiert die fehlerhaften Zeilen direkt im Editor.
// npm run lint zeigt dieselben Meldungen im Terminal.
// npm run dev zeigt sie in der Seite und in der Browser-Konsole.
// npm run lint:fix ergänzt fehlende Klammern und entfernt überflüssige.
const ternaryLintExample = {
// gültig
status(isActive) {
// prettier-ignore
return (isActive) ? 'online' : 'offline';
},
// Fehler: local/ternary-condition-parens
statusOhneKlammern(isActive) {
return (isActive) ? 'online' : 'offline';
},
// gültig: zusammengesetzte Bedingung bleibt in Klammern
label(count, hasError) {
// prettier-ignore
return (count > 0 && !hasError) ? 'Valid' : 'Invalid';
},
// Fehler: local/ternary-condition-parens
labelOhneKlammern(count, hasError) {
return (count > 0 && !hasError) ? 'Valid' : 'Invalid';
},
// gültig: verschachtelte Ternaries sind erlaubt, jede Bedingung in Klammern
nested(isAdmin, isActive) {
// prettier-ignore
return (isAdmin) ? ((isActive) ? 'Admin online' : 'Admin offline') : 'Gast';
},
// Fehler: @stylistic/no-extra-parens
summeMitExtraKlammern(a, b) {
return a + b;
},
};
export default ternaryLintExample;
@@ -0,0 +1,110 @@
import * as esbuild from 'esbuild';
import { ESLint } from 'eslint';
import fs from 'node:fs';
import path from 'node:path';
const outfile = path.resolve('assets/js/bundle.js');
const reportFile = path.resolve('assets/js/lint-report.js');
const watch = process.argv.includes('--watch');
function runtimeReport(results) {
const findings = [];
results.forEach((result) => {
const file = path.relative(process.cwd(), result.filePath);
result.messages.forEach((message) => {
if (message.severity !== 2) {
return;
}
findings.push({
file,
line: message.line ?? 0,
column: message.column ?? 0,
rule: message.ruleId ?? '',
message: message.message,
});
});
});
const script = `(() => {
const findings = ${JSON.stringify(findings)};
if (findings.length === 0) {
return;
}
const box = document.createElement('div');
box.className = 'alert alert-danger m-3';
box.setAttribute('role', 'alert');
const title = document.createElement('strong');
title.textContent = 'ESLint: ' + findings.length + ' Fehler';
box.append(title);
const list = document.createElement('ul');
list.className = 'mb-0 mt-2';
for (const finding of findings) {
const text = finding.file + ':' + finding.line + ':' + finding.column + ' ' + finding.message + ' (' + finding.rule + ')';
console.error('[ESLint] ' + text);
const item = document.createElement('li');
item.textContent = text;
list.append(item);
}
box.append(list);
document.body.prepend(box);
})();
`;
return { findings, script };
}
function printFindings(findings) {
if (findings.length === 0) {
console.log('ESLint: keine Fehler');
return;
}
console.error(`ESLint: ${findings.length} Fehler (auch in der geladenen Seite)`);
findings.forEach((finding) => {
console.error(`${finding.file}:${finding.line}:${finding.column} ${finding.message} (${finding.rule})`);
});
}
const eslintRuntime = {
name: 'eslint-runtime',
setup(build) {
build.onEnd(async (result) => {
if (result.errors.length > 0) {
return;
}
try {
const eslint = new ESLint({ cwd: process.cwd() });
const results = await eslint.lintFiles(['dev/js/**/*.js']);
const report = runtimeReport(results);
fs.writeFileSync(reportFile, report.script);
printFindings(report.findings);
} catch (error) {
console.error('ESLint-Laufzeitbericht fehlgeschlagen:', error);
}
});
},
};
const context = await esbuild.context({
entryPoints: ['dev/js/main.js'],
bundle: true,
outfile,
plugins: [eslintRuntime],
});
if (watch) {
await context.watch();
} else {
await context.rebuild();
await context.dispose();
}
@@ -0,0 +1,95 @@
import { FlatCompat } from '@eslint/eslintrc';
import js from '@eslint/js';
import stylistic from '@stylistic/eslint-plugin';
import globals from 'globals';
import ternaryConditionParens from './eslint/ternary-condition-parens.mjs';
const compat = new FlatCompat({
baseDirectory: import.meta.dirname,
recommendedConfig: js.configs.recommended,
allConfig: js.configs.all,
});
export default [
{
ignores: ['node_modules/**', 'assets/**', 'dev/js/libs/**'],
},
...compat.extends('airbnb-base'),
{
files: ['**/*.{js,mjs}'],
languageOptions: {
ecmaVersion: 'latest',
sourceType: 'module',
globals: {
...globals.browser,
},
},
plugins: {
'@stylistic': stylistic,
local: {
rules: {
'ternary-condition-parens': ternaryConditionParens,
},
},
},
settings: {
'import/resolver': {
node: {
extensions: ['.mjs', '.js', '.json'],
// uuid@14 hat nur ein exports-Feld. Der Resolver folgt exports erst mit Kategorie.
exportsCategory: 'pattern-trailers-no-dir-slash+json-imports',
moduleSystem: 'import',
},
},
},
rules: {
// Airbnb schaltet die Core-Regel aus. Stylistic übernimmt sie,
// damit Prettier diese Stellen nicht mehr formatieren muss.
'no-extra-parens': 'off',
'no-nested-ternary': 'off',
'no-use-before-define': 'off',
// Erzwingt Klammern um die Ternary-Bedingung. Stylistic kann das nur erlauben.
'local/ternary-condition-parens': 'error',
'@stylistic/no-extra-parens': [
'error',
'all',
{
conditionalAssign: false,
nestedBinaryExpressions: false,
ternaryOperandBinaryExpressions: false,
ignoreJSX: 'all',
nestedConditionalExpressions: false,
// Überspringt die Ternary-Prüfung, sonst entfernt --fix (isActive).
// Arrow-Body ersetzt die veraltete Option enforceForArrowConditionals.
ignoredNodes: [
'ConditionalExpression',
'ArrowFunctionExpression[body.type=ConditionalExpression]',
],
},
],
// Dieses Projekt importiert mit Dateiendung, Airbnb verlangt sonst „never“.
'import/extensions': [
'error',
'ignorePackages',
{
js: 'always',
mjs: 'always',
},
],
'import/no-extraneous-dependencies': [
'error',
{
devDependencies: ['eslint.config.mjs', 'esbuild.config.mjs', 'eslint/**'],
},
],
},
},
{
files: ['esbuild.config.mjs'],
rules: {
'no-console': 'off',
},
},
];

Some files were not shown because too many files have changed in this diff Show More