Compare commits

...

2 Commits

Author SHA1 Message Date
Adrian Rumpold
3498e45ab2 Migrate styles to SCSS 2025-07-10 08:33:50 +02:00
Adrian Rumpold
a690718192 Category metadata / icon support 2025-07-10 08:33:42 +02:00
16 changed files with 1280 additions and 68 deletions

View File

@@ -2,8 +2,9 @@
<html lang="en"> <html lang="en">
<head> <head>
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" /> <link rel="icon" type="image/svg+xml" href="/aai-favicon.png" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<link rel="preconnect" href="https://fonts.googleapis.com" /> <link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin /> <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link rel="preconnect" href="https://rsms.me/" /> <link rel="preconnect" href="https://rsms.me/" />
@@ -12,7 +13,12 @@
href="https://fonts.googleapis.com/css2?family=Work+Sans:ital,wght@0,100..900;1,100..900&display=swap" href="https://fonts.googleapis.com/css2?family=Work+Sans:ital,wght@0,100..900;1,100..900&display=swap"
rel="stylesheet" rel="stylesheet"
/> />
<title>Vite + React</title> <!-- Material Icons font -->
<link
rel="stylesheet"
href="https://fonts.googleapis.com/icon?family=Material+Icons"
/>
<title>AI Skills Framework</title>
</head> </head>
<body> <body>
<div id="root"></div> <div id="root"></div>

1165
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -10,9 +10,12 @@
"preview": "vite preview" "preview": "vite preview"
}, },
"dependencies": { "dependencies": {
"@emotion/styled": "^11.14.1",
"@mui/icons-material": "^7.2.0",
"d3": "^7.9.0", "d3": "^7.9.0",
"react": "^19.1.0", "react": "^19.1.0",
"react-dom": "^19.1.0" "react-dom": "^19.1.0",
"sass": "^1.89.2"
}, },
"devDependencies": { "devDependencies": {
"@eslint/js": "^9.30.1", "@eslint/js": "^9.30.1",

BIN
public/aai-favicon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 162 KiB

View File

@@ -4,14 +4,20 @@ import Legend from "./components/Legend";
import QRCode from "./components/QRCode"; import QRCode from "./components/QRCode";
import { QuestionGroupChart } from "./components/QuestionGroupChart"; import { QuestionGroupChart } from "./components/QuestionGroupChart";
import { config } from "./config"; import { config } from "./config";
import { getSampleData } from "./lib/data"; import { CategoryMetadata, fetchCategoryMetadata } from "./lib/metadata";
import { ResponseData } from "./lib/parser"; import { fetchGoogleSheet, ResponseData } from "./lib/parser";
function App() { function App() {
const [data, setData] = useState<ResponseData[]>([]); const [data, setData] = useState<ResponseData[]>([]);
const [categoryMetadata, setCategoryMetadata] = useState<CategoryMetadata[]>(
[]
);
useEffect(() => { useEffect(() => {
// fetchGoogleSheet().then(setData); fetchGoogleSheet().then(setData);
setData(getSampleData()); // setData(getSampleData());
fetchCategoryMetadata().then(setCategoryMetadata);
}, []); }, []);
if (!data.length) return null; if (!data.length) return null;
@@ -38,6 +44,7 @@ function App() {
<QuestionGroupChart <QuestionGroupChart
key={question} key={question}
question={question} question={question}
metadata={categoryMetadata.find((m) => m.category === question)}
groupData={groupData} groupData={groupData}
responses={sortedResponses} responses={sortedResponses}
xScale={xScale} xScale={xScale}

View File

@@ -1,5 +1,5 @@
import { colorScheme } from "../config"; import { colorScheme } from "../config";
import "./Legend.css"; import "../styles/Legend.scss";
const labels = { const labels = {
0: "Keine Erfahrung", 0: "Keine Erfahrung",

View File

@@ -1,4 +1,4 @@
import "./QRCode.css"; import "../styles/QRCode.scss";
export default function QRCode() { export default function QRCode() {
return ( return (
@@ -9,7 +9,7 @@ export default function QRCode() {
egal, auf welchem Level du bist. egal, auf welchem Level du bist.
</p> </p>
<div className="qr-code bracket-frame"> <div className="qr-code bracket-frame">
<img src="https://upload.wikimedia.org/wikipedia/commons/4/41/QR_Code_Example.svg" /> <img src="/qr_code_ki-skills-umfrage.png" />
</div> </div>
</div> </div>
); );

View File

@@ -1,11 +1,15 @@
import MaterialIcon from "@mui/material/Icon";
import * as d3 from "d3"; import * as d3 from "d3";
import { useEffect, useRef } from "react"; import { useEffect, useRef } from "react";
import { colorScheme, config } from "../config"; import { colorScheme, config } from "../config";
import { CategoryMetadata } from "../lib/metadata";
import "./Chart.css"; import "./Chart.css";
interface QuestionGroupChartProps { interface QuestionGroupChartProps {
question: string; question: string;
metadata?: CategoryMetadata;
groupData: { response: string }[]; groupData: { response: string }[];
responses: string[]; responses: string[];
xScale: d3.ScaleBand<string>; xScale: d3.ScaleBand<string>;
@@ -38,6 +42,7 @@ function makeDot(
export function QuestionGroupChart({ export function QuestionGroupChart({
question, question,
metadata,
groupData, groupData,
responses, responses,
xScale, xScale,
@@ -81,13 +86,11 @@ export function QuestionGroupChart({
return ( return (
<div className="question-group"> <div className="question-group">
<svg ref={svgRef}></svg> <svg ref={svgRef}></svg>
<div className="question-title">{question}</div> <div className="question-title">
<p> {metadata?.icon && <MaterialIcon>{metadata.icon}</MaterialIcon>}
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod {question}
tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim </div>
veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea <p>{metadata?.description || question}</p>
commodo.
</p>
</div> </div>
); );
} }

View File

@@ -23,3 +23,6 @@ const aaiColors = [
export const colorScheme = Object.fromEntries( export const colorScheme = Object.fromEntries(
aaiColors.map((color, index) => [index, color]) aaiColors.map((color, index) => [index, color])
); );
export const categoryMetadataUrl =
"https://docs.google.com/spreadsheets/d/e/2PACX-1vT6FQoV_8ET_pmEB5LGlI_ST9AAhsfiZrWydFwIB80G0Lr_kGwVJUzjM6fRPP9Yrx6iVZYMVAPTnLKq/pub?gid=0&single=true&output=csv";

37
src/lib/metadata.ts Normal file
View File

@@ -0,0 +1,37 @@
import * as d3 from "d3";
export interface CategoryMetadata {
category: string;
description: string;
icon: string;
}
const sampleMetadataCsv = `title,text,icon
Allgemeines KI-Wissen,Grundlegendes Wissen über Künstliche Intelligenz und deren Anwendung in Organisationen,school
KI-Innovation,"Fähigkeiten zur Entwicklung, Bewertung und Förderung von KI-Innovationen im Unternehmen",science
KI-Geschäftsstrategie,"Verstehen, wie KI strategisch in Geschäftsmodelle integriert und eingesetzt werden kann",business
Stakeholder-Landschaft,"Fähigkeit, relevante Stakeholder für KI-Initiativen zu identifizieren, einzubinden und zu koordinieren",people_alt
KI-Ethik,Kenntnisse über ethische Fragestellungen und verantwortungsvollen KI-Einsatz,local_police
KI-Regulation,Verständnis rechtlicher Rahmenbedingungen und Regulierungen rund um KI,gavel
Datenkompetenz,"Fähigkeit, Daten kritisch zu beurteilen, aufzubereiten und für KI nutzbar zu machen",equalizer
Python-Programmierung,Grundlegende Programmier-kenntnisse zur Umsetzung und Anpassung von KI-Lösungen,data_object
Software Design,"Gestaltung robuster, skalierbarer und wartbarer Softwarelösungen mit KI-Komponenten",code
Maschinelles Lernen,Kenntnisse in maschinellem Lernen zur Entwicklung datengetriebener Modelle,model_training
MLOps / Infrastruktur,Fähigkeiten zum produktiven Einsatz und Betrieb von KI-Systemen in Unternehmen,all_inclusive
GenAI-Kenntnisse,Verständnis generativer KI-Modelle (z.B. Large Language Models) und ihrer praktischen Nutzung,auto_awesome`;
export function fetchCategoryMetadata(): Promise<CategoryMetadata[]> {
const parseCsv = (csv: string): CategoryMetadata[] => {
const parsed = d3.csvParse(csv);
return parsed.map((row) => ({
category: row.title,
description: row.text,
icon: row.icon,
}));
};
/*return fetch(categoryMetadataUrl)
.then((response) => response.text())
.then(parseCsv);*/
return Promise.resolve(parseCsv(sampleMetadataCsv));
}

View File

@@ -1,7 +1,7 @@
import { StrictMode } from "react"; import { StrictMode } from "react";
import { createRoot } from "react-dom/client"; import { createRoot } from "react-dom/client";
import App from "./App"; import App from "./App";
import "./index.css"; import "./styles/index.scss";
createRoot(document.getElementById("root")!).render( createRoot(document.getElementById("root")!).render(
<StrictMode> <StrictMode>

View File

@@ -1,3 +1,5 @@
@use "shared" as *;
.legend { .legend {
.box { .box {
display: inline-block; display: inline-block;
@@ -6,7 +8,9 @@
border-radius: 4px; border-radius: 4px;
margin-right: 8px; margin-right: 8px;
vertical-align: middle; vertical-align: middle;
@include shadow;
} }
ul { ul {
list-style: none; list-style: none;
margin: 16px; margin: 16px;

View File

@@ -1,11 +1,12 @@
@use "shared" as *;
.qr-code-container { .qr-code-container {
background-color: hsl(18.6deg 100% 55.1%); @include rounded;
background-color: $aai-orange;
flex-basis: 20%; flex-basis: 20%;
flex-shrink: 0; flex-shrink: 0;
padding: 16px; padding: 16px;
border-radius: 16px;
filter: drop-shadow(0 8px 8px hsl(202.5deg 20% 76.5%));
color: #fff; color: #fff;

50
src/styles/_shared.scss Normal file
View File

@@ -0,0 +1,50 @@
$aai-blue-dark: hsl(198.5deg 83.5% 19%);
$aai-blue-light: hsl(196.8deg 63% 55.5%);
$aai-green-dark: hsl(181deg 75% 37%);
$aai-green-light: hsl(127.5deg 100% 87.5%);
$aai-grey: hsl(202.5deg 20% 76.5%);
$aai-orange: hsl(18.6deg 100% 55.1%);
@mixin rounded {
border-radius: 16px;
}
@mixin shadow {
filter: drop-shadow(0 16px 16px rgb($aai-grey, 80%));
}
@mixin no-shadow {
filter: none;
}
$border-radius: 16px;
@mixin button {
@include shadow;
background-color: $aai-action-button;
color: #ffffff;
border-radius: $border-radius;
cursor: pointer;
display: flex;
align-items: center;
overflow: hidden;
&:active {
@include no-shadow;
}
span {
flex: 1 auto;
white-space: nowrap;
text-align: center;
font-weight: bold;
text-transform: uppercase;
font-size: 16px;
}
}

View File

@@ -1,3 +1,5 @@
@use "shared" as *;
:root { :root {
font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif; font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif;
line-height: 1.5; line-height: 1.5;
@@ -32,11 +34,11 @@ h6 {
a { a {
font-weight: 500; font-weight: 500;
color: #646cff; color: $aai-blue-dark;
text-decoration: inherit; text-decoration: inherit;
} }
a:hover { a:hover {
color: #747bff; color: $aai-blue-light;
} }
body { body {
@@ -56,13 +58,13 @@ body {
} }
.chart-container { .chart-container {
@include rounded;
@include shadow;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
background-color: #ffffff; background-color: #ffffff;
border-radius: 16px;
filter: drop-shadow(0 8px 8px hsl(202.5deg 20% 76.5%));
} }
.charts { .charts {
@@ -73,12 +75,14 @@ body {
} }
.question-group { .question-group {
@include rounded;
@include shadow;
background-color: #ffffff; background-color: #ffffff;
padding: 16px; padding: 16px;
border: 1px solid #ddd; border: 1px solid #ddd;
border-radius: 8px;
filter: drop-shadow(0 2px 2px hsl(202.5deg 20% 76.5%)); flex: 1 1 300px;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
@@ -95,7 +99,6 @@ body {
font-size: 14px; font-size: 14px;
color: #666; color: #666;
line-height: 1.4; line-height: 1.4;
max-width: 40ch;
} }
.question-title { .question-title {
@@ -103,6 +106,8 @@ body {
font-weight: bold; font-weight: bold;
color: #333; color: #333;
} }
.question-title::before {
content: "💻 "; .question-title .material-icons {
vertical-align: top;
margin-right: 1ex;
} }