This commit is contained in:
2025-06-16 13:37:14 +02:00
parent ac273655e6
commit a8b82208f7
5100 changed files with 737524 additions and 2 deletions

29
src/assets/links.toml Normal file
View File

@@ -0,0 +1,29 @@
[[links]]
id = "jupyterhub"
icon = "https://jupyter.org/assets/homepage/main-logo.svg"
[links.config]
title = "Jupyter Hub"
link = "`${window.location.origin}/jupyter`"
description = "The main System build on top of Docker"
tags = [ "Teaching", "Docker" ]
[[links]]
id = "ifnwebsite"
icon = "https://www.tu-braunschweig.de/fileadmin/Logos_Einrichtungen/Institute_FK5/logo_IFN.svg"
[links.config]
title = "IFN Website"
link = "https://www.tu-braunschweig.de/ifn"
description = "All about the Institut"
tags = [ "Info" ]
[[links]]
id = "course-prog"
icon = "https://www.tu-braunschweig.de/fileadmin/Logos_Einrichtungen/Institute_FK5/logo_IFN.svg"
[links.config]
title = "Course Site"
link = "https://www.tu-braunschweig.de/ifn/edu/ws/einfuehrung-in-die-programmierung-fuer-nicht-informatiker"
description = "Stuff about Einführung in die Programmierung für NichtInformatiker*innen"
tags = [ "Info", "Teaching" ]

View File

@@ -0,0 +1,5 @@
import './style.css';
export default function Cell({ children }) {
return <div class="cell">{children}</div>
}

View File

@@ -0,0 +1,17 @@
import Highlight from 'react-highlight';
import './jupyter-theme.css';
export default function CellInput({ children }) {
const randomTurn = Math.floor(Math.random() * 111) + 1;
return (
<div class="cell-input">
<div class="cell-turn">In [{randomTurn}]:</div>
<div class="cell-content">
<Highlight className='python'>
{children}
</Highlight>
</div>
</div>
);
}

View File

@@ -0,0 +1,12 @@
export default function CellOutput({ children }) {
const randomTurn = Math.floor(Math.random() * 111) + 1;
return (
<div class="cell-output">
<div class="cell-turn">Out [{randomTurn}]:</div>
<div class="cell-content-2">
{children}
</div>
</div>
);
}

View File

@@ -0,0 +1,38 @@
.hljs {
display: block;
overflow-x: auto;
padding: 0.5em;
background: #F5F5F5;
}
.hljs,
.hljs-subst {
color: #434f54;
}
.hljs-number,
.hljs-keyword,
.hljs-built_in {
color: #008000;
}
.hljs-keyword {
font-weight: bold;
}
.hljs-comment {
color: #408080;
font-style: italic;
}
.hljs-string {
color: #BA2121;
}
.hljs-operator {
color: #7800C2;
}
.hljs-title {
color: red;
}

View File

@@ -0,0 +1,57 @@
.cell {
border-radius: 4px;
margin-bottom: 20px;
background: white;
}
.cell-input {
display: flex;
gap: 10px;
padding: 15px 20px;
font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
font-size: 13px;
color: #24292e;
align-items: flex-start;
}
.cell-content {
flex: 0 0 90%;
border: 1px solid #b0b0b0;
overflow: hidden;
}
.cell-content-2 {
flex: 0 0 90%;
overflow: hidden;
}
.cell-turn {
flex: 1;
color: #b0b0b0;
margin-top: 5px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
width: 100%;
}
.cell-output {
display: flex;
gap: 10px;
padding: 15px 20px;
font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
font-size: 13px;
color: #24292e;
align-items: flex-start;
}
@media (max-width: 768px) {
.cell-input {
font-size: 12px;
padding: 12px 15px;
}
.cell-output {
padding: 15px;
}
}

View File

@@ -0,0 +1,5 @@
import './style.css';
export default function KernelStatus() {
return <span class="kernel-status"></span>;
}

View File

@@ -0,0 +1,21 @@
.kernel-status {
display: inline-block;
width: 8px;
height: 8px;
border-radius: 50%;
background: #28a745;
margin-right: 8px;
animation: pulse 2s infinite;
}
@keyframes pulse {
0% {
opacity: 1;
}
50% {
opacity: 0.5;
}
100% {
opacity: 1;
}
}

View File

@@ -0,0 +1,53 @@
import './style.css';
const Tag = ({ tag }) => {
console.log(tag)
return (
<div class="link-tag">
{tag}
</div>
);
}
const LinkItem = ({ linkData }) => {
const { config, icon } = linkData;
const fullUrl = `${window.location.origin}${config.link}`;
console.log(config.tags)
return (
<a
href={config.link}
class="link-item"
target="_blank"
rel="noopener noreferrer"
>
<img
src={icon}
class="link-icon"
onError={(e) => {
// Fallback to a default icon if the image fails to load
e.target.src = 'data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNDgiIGhlaWdodD0iNDgiIHZpZXdCb3g9IjAgMCAyNCAyNCIgZmlsbD0iIzY2NjY2NiI+PHBhdGggZD0iTTEyIDJDNi40OCAyIDIgNi40OCAyIDEyczQuNDggMTAgMTAgMTAgMTAgNDg5IDEwIDEwUzE3LjUyIDIgMTIgMnptLTIgMTVsLTUtNSAxLjQxLTEuNDFMMTAuNTcgMTVsMi4yNDktMi4yNDkgMS40MTQgMS40MTQtMy4yNDkgMy4yNDlMMTAgMTd6Ii8+PC9zdmc+';
}}
/>
<div class="link-content">
<div class="link-title">{config.title}</div>
<div class="link-description">{config.description}</div>
<div class="link-tags">
{config.tags.map(tag => (<p class="link-tag">{tag}</p>))}
</div>
</div>
<div class="link-url">
{config.link}
</div>
</a>
);
};
export default function Links({ links }) {
return (
<div class="links-container">
{links.map(link => (<LinkItem key={link.id} linkData={link} />))}
</div>
);
}

View File

@@ -0,0 +1,96 @@
.links-container {
display: flex;
flex-direction: column;
gap: 15px;
}
.link-item {
width: 95%;
display: flex;
align-items: center;
padding: 20px;
border: 1px solid #d1d5da;
border-radius: 6px;
background: white;
text-decoration: none;
color: inherit;
transition: all 0.2s ease;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.12);
}
.link-item:hover {
border-color: #0366d6;
box-shadow: 0 3px 8px rgba(3, 102, 214, 0.2);
}
.link-icon {
width: auto;
height: 48px;
margin-right: 20px;
border-radius: 8px;
object-fit: cover;
}
.link-content {
flex: 1;
}
.link-title {
font-size: 1.2rem;
font-weight: 600;
color: #24292e;
margin-bottom: 5px;
}
.link-description {
color: #586069;
font-size: 0.9rem;
margin-bottom: 8px;
}
.link-tags {
display: flex;
flex-direction: row;
gap: 10px;
}
.link-tag {
display: inline-block;
padding: 4px 6px;
color: white;
border-radius: 3px;
background-color: #586069;
white-space: nowrap;
}
.link-url {
display: inline-block;
max-width: 30%;
white-space: collapse;
color: #586069;
font-size: 0.8rem;
font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
background: #f6f8fa;
padding: 2px 6px;
border-radius: 3px;
margin-left: auto;
overflow: auto;
}
@media (max-width: 768px) {
.link-item {
flex-direction: column;
text-align: center;
padding: 15px;
}
.link-icon {
margin-right: 0;
margin-bottom: 15px;
}
.link-url {
margin-left: 0;
margin-top: 10px;
}
}

View File

@@ -0,0 +1,10 @@
import './style.css';
import KernelStatus from '../KernelStatus/KernelStatus.jsx';
export default function Loading() {
return (
<div class="loading">
<KernelStatus />Loading links...
</div>
);
}

View File

@@ -0,0 +1,9 @@
.loading {
text-align: center;
padding: 40px;
color: #586069;
display: flex;
align-items: center;
justify-content: center;
gap: 10px;
}

View File

@@ -0,0 +1,126 @@
import './style.css';
import {
Chart as ChartJS,
CategoryScale,
LinearScale,
BarElement,
Title,
Tooltip,
Legend
} from 'chart.js';
import { Chart } from 'react-chartjs-2';
ChartJS.register(
CategoryScale,
LinearScale,
BarElement,
Title,
Tooltip,
Legend
);
function toCleanTitleCase(str) {
return str
.replace(/[_-]/g, ' ') // Replace underscores and hyphens with spaces
.replace(
/\w\S*/g,
(txt) => txt.charAt(0).toUpperCase() + txt.substr(1).toLowerCase()
);
}
const chartAreaBorder = {
id: 'chartAreaBorder',
beforeDraw(chart, args, options) {
const {ctx, chartArea: {left, top, width, height}} = chart;
ctx.save();
ctx.strokeStyle = options.borderColor;
ctx.lineWidth = options.borderWidth;
ctx.setLineDash(options.borderDash || []);
ctx.lineDashOffset = options.borderDashOffset;
ctx.strokeRect(left, top, width, height);
ctx.restore();
}
};
export default function StatsChart({ links }) {
const options = {
responsive: true,
animation: false,
interaction: {
mode: null,
intersect: false,
},
plugins: {
tooltip: {
enabled: false
},
legend: {
display: false,
position: 'top',
},
title: {
display: true,
text: 'Stats',
font: {
family: "'Monaco', 'Menlo', 'Ubuntu Mono', monospace",
weight: 'bold',
size: 17,
color: 'black',
lineHeight: 1.2,
}
},
chartAreaBorder: {
borderColor: 'black',
borderWidth: 1,
borderDash: [0, 0],
borderDashOffset: 0,
},
},
scales: {
x: {
border: { display: false },
grid: { display: false },
ticks: {
color: 'black',
font: { size: 15 }
}
},
y: {
border: { display: false },
grid: { display: false},
ticks: {
color: 'black',
font: { size: 15 }
}
}
},
maintainAspectRatio: true,
aspectRatio: 1 | 1,
}
const labels = [
"Overall",
...links.map(item => toCleanTitleCase(item.id))
];
const data = {
labels,
datasets: [
{
label: "Stats",
data: [
links.length,
...Array.from(
{ length: labels.length },
() => Math.floor(Math.random() * (links.length -1)) + 1
)],
backgroundColor: ['#1F77B4', '#FF7F0E', '#2CA02C', '#D62728', '#9467BD'],
}
],
}
return (
<div class="chart-container">
<Chart type="bar" options={options} data={data} plugins={[chartAreaBorder]} />
</div>
);
}

View File

@@ -0,0 +1,6 @@
.chart-container {
position: relative;
height: 30rem;
width: 30rem;
background-color: transparent;
}

View File

@@ -0,0 +1,10 @@
import './style.css';
import KernelStatus from '../KernelStatus/KernelStatus.jsx';
export default function StatusBar({ links }) {
return (
<div class="status-bar">
<KernelStatus />Kernel ready {links.length} links loaded Ready for interaction
</div>
);
}

View File

@@ -0,0 +1,11 @@
.status-bar {
font-size: 0.85rem;
color: #586069;
padding: 10px 0;
border-top: 1px solid #e1e4e8;
margin-top: 30px;
display: flex;
align-items: center;
}

33
src/index.jsx Normal file
View File

@@ -0,0 +1,33 @@
import { LocationProvider, Router, Route, hydrate, prerender as ssr } from 'preact-iso';
import './style.css';
import Home from './pages/Home/index.jsx';
import NotFound from './pages/_404.jsx';
export function App() {
const routes = [
{ desc: "Home", path: "/", component: Home },
]
const links = routes.map(route => ({desc: route.desc, path: route.path}))
return (
<LocationProvider>
<main>
<Router>
{routes.map(route => (
<Route path={route.path} component={route.component} />
))}
<Route default component={NotFound} />
</Router>
</main>
</LocationProvider>
);
}
if (typeof window !== 'undefined') {
hydrate(<App />, document.getElementById('app'));
}
export async function prerender(data) {
return await ssr(<App {...data} />);
}

124
src/pages/Home/index.jsx Normal file
View File

@@ -0,0 +1,124 @@
import { useState, useEffect } from 'preact/hooks';
import './style.css';
// Components
import Cell from '../../components/Cell/Cell.jsx';
import CellInput from '../../components/Cell/CellInput.jsx';
import CellOutput from '../../components/Cell/CellOutput.jsx';
import StatsChart from '../../components/StatsChart/StatsChart.jsx';
import StatusBar from '../../components/StatusBar/StatusBar.jsx';
import Loading from '../../components/Loading/Loading.jsx';
import Links from '../../components/Links/Links.jsx';
const linksCode = `# Loading Dataframe
from utils import link_container
import pandas as pd
links = pd.read_csv('ifn_links.csv')
links.drop_duplicates()
for index, link in links.iterrows():
link_container.attach(index, link)
link_container.show()
`
const statsCode = `# Plotting Statistics
import matplotlib.pyplot as plt
import numpy as np
plt.title("Stats")
plt.bar("Links Loaded", len(links))
for link in links:
plt.bar(link, np.random.randint(1, len(links) - 1))
plt.show()
`
const getMockLinks = () => {
return [
{
id: 'jupyterhub',
config: {
title: 'Jupyter Hub',
link: `${window.location.origin}/jupyter`,
description: 'The main System build on top of Docker',
tags: ['Teaching', 'Docker']
},
icon: 'https://jupyter.org/assets/homepage/main-logo.svg'
},
{
id: 'ifnwebsite',
config: {
title: 'IFN Website',
link: 'https://www.tu-braunschweig.de/ifn',
description: 'All about the institut',
tags: ['Info']
},
icon: 'https://www.tu-braunschweig.de/fileadmin/Logos_Einrichtungen/Institute_FK5/logo_IFN.svg'
},
{
id: 'course-prog',
config: {
title: 'Lehrseite',
link: 'https://www.tu-braunschweig.de/ifn/edu/ws/einfuehrung-in-die-programmierung-fuer-nicht-informatiker',
description: 'Useful Stuff about the course',
tags: ['Info', 'Teaching']
},
icon: 'https://www.tu-braunschweig.de/fileadmin/Logos_Einrichtungen/Institute_FK5/logo_IFN.svg'
},
];
};
export default function Home() {
const [links, setLinks] = useState([]);
const [loading, setLoading] = useState(true);
useEffect(() => {
const delay = 500 + Math.random() * 1500;
const timer = setTimeout(() => {
setLinks(getMockLinks())
setLoading(false);
}, delay)
return () => clearTimeout(timer);
}, []);
return (
<div class="notebook-container">
<div class="notebook-header">
<div class="header-container">
<h1 class="notebook-title">Teaching System</h1>
<img class="notebook-img" src="https://www.tu-braunschweig.de/fileadmin/Logos_Einrichtungen/Institute_FK5/logo_IFN.svg" />
</div>
<div class="notebook-subtitle">
A linktree of all available systems
</div>
</div>
<Cell>
<CellInput>
{linksCode}
</CellInput>
<CellOutput>
{loading ? (<Loading />) : (<Links links={links} />)}
</CellOutput>
</Cell>
<Cell>
<CellInput>
{statsCode}
</CellInput>
<CellOutput>
<StatsChart links={links}/>
</CellOutput>
</Cell>
<StatusBar links={links}/>
</div>
);
}

54
src/pages/Home/style.css Normal file
View File

@@ -0,0 +1,54 @@
.notebook-container {
max-width: 1200px;
margin: 0 auto;
padding: 20px;
background: white;
box-shadow: 0 0 12px rgba(87, 87, 87, 0.2);
min-height: 100vh;
}
.notebook-header {
border-bottom: 1px solid #e1e4e8;
padding-bottom: 20px;
margin-bottom: 30px;
}
.header-container {
display: flex;
justify-content: space-between;
align-items: center;
width: 100%;
}
.notebook-image {
height: 1000px;
width: auto;
}
.notebook-title {
font-size: 2.5rem;
font-weight: 400;
color: #24292e;
margin-bottom: 0;
}
.notebook-subtitle {
color: #586069;
font-size: 1.1rem;
font-weight: 300;
margin-top: 0;
margin-left: 20px;
}
@media (max-width: 768px) {
.notebook-container {
padding: 15px;
margin: 0;
box-shadow: none;
}
.notebook-title {
font-size: 2rem;
}
}

5
src/pages/_404.jsx Normal file
View File

@@ -0,0 +1,5 @@
export default function NotFound() {
return (
<h1>404</h1>
);
}

15
src/style.css Normal file
View File

@@ -0,0 +1,15 @@
/* Jupyter Notebook inspired styling */
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Helvetica, Arial, sans-serif;
background: #fafafa;
color: black;
line-height: 1.6;
}