Frontend Web-Interface¶
Audience: Dev
You will learn:
- Web-Interface Technologie-Stack und Architektur
- HTML-Template-Struktur und Jinja2-Integration
- JavaScript-Funktionalitäten für Suche und Filterung
- UI-Komponenten und Responsive Design
Pre-requisites: - Backend-Übersicht verstanden - HTML, CSS, JavaScript Grundkenntnisse - Flask Template-System Verständnis
Technologie-Stack¶
Frontend-Architektur¶
graph TB
A[Browser] --> B[HTML Template]
B --> C[Flask/Jinja2]
C --> D[Backend API]
B --> E[Vanilla JavaScript]
B --> F[CSS Styling]
B --> G[SVG Icons]
E --> H[Search Functionality]
E --> I[Category Filtering]
E --> J[Icon Preview]
Tech Stack: - Template Engine: Jinja2 (Flask integriert) - JavaScript: Vanilla ES6+ (keine Frameworks) - CSS: Standard CSS3 mit Flexbox/Grid - Icons: Inline SVG + Static File Serving
Evidenz: templates/index.html, app.py:45-49
Template-Struktur¶
<!-- templates/index.html -->
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Icon Browser - ak Systems</title>
<!-- CSS Styling -->
<style>
/* Responsive Grid Layout */
.icon-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(120px, 1fr));
gap: 1rem;
}
.icon-card {
border: 1px solid #ddd;
border-radius: 8px;
padding: 1rem;
text-align: center;
transition: transform 0.2s;
}
.icon-card:hover {
transform: translateY(-2px);
box-shadow: 0 4px 8px rgba(0,0,0,0.1);
}
</style>
</head>
<body>
<!-- Navigation und Header -->
<header>
<h1>ak Systems Icon Browser</h1>
<p>{{ total_icons }} verfügbare Icons</p>
</header>
<!-- Search und Filter Controls -->
<div class="controls">
<input type="search" id="search" placeholder="Icons suchen...">
<select id="category-filter">
<option value="">Alle Kategorien</option>
{% for category in categories %}
<option value="{{ category }}">{{ category }}</option>
{% endfor %}
</select>
</div>
<!-- Icon Grid -->
<div class="icon-grid" id="icon-grid">
{% for icon in icons %}
<div class="icon-card"
data-name="{{ icon.name }}"
data-category="{{ icon.category }}">
<div class="icon-preview">
<img src="{{ icon.path }}" alt="{{ icon.display_name }}">
</div>
<div class="icon-info">
<h3>{{ icon.display_name }}</h3>
<p>{{ icon.category }}</p>
<code>{{ icon.filename }}</code>
</div>
</div>
{% endfor %}
</div>
<!-- JavaScript Functionality -->
<script>
// Search and Filter Implementation
</script>
</body>
</html>
Evidenz: templates/index.html structure, app.py template rendering
Jinja2 Template-Integration¶
Template-Variablen¶
# app.py - Template Context
@app.route('/')
def index():
icons, categories = get_icon_list()
return render_template('index.html',
icons=icons, # Liste aller Icons
categories=categories, # Kategorie-Dictionary
total_icons=len(icons) # Anzahl für UI
)
Template-Rendering¶
<!-- Icon-Liste iterieren -->
{% for icon in icons %}
<div class="icon-card" data-category="{{ icon.category }}">
<img src="{{ icon.path }}" alt="{{ icon.display_name }}">
<h3>{{ icon.display_name }}</h3>
<span class="category">{{ icon.category }}</span>
<code class="filename">{{ icon.filename }}</code>
</div>
{% endfor %}
<!-- Kategorien für Filter -->
<select id="category-filter">
<option value="">Alle Kategorien ({{ total_icons }})</option>
{% for category, icon_list in categories.items() %}
<option value="{{ category }}">
{{ category }} ({{ icon_list|length }})
</option>
{% endfor %}
</select>
Template Features:
- Sicheres Escaping: Automatisch durch Jinja2
- Conditional Rendering: {% if %}
für leere States
- Loops: {% for %}
für Icon-Iteration
- Filters: |length
für Array-Größen
Evidenz: app.py:45-49 template rendering
JavaScript-Funktionalitäten¶
Icon-Suche Implementation¶
// Search Functionality
class IconSearch {
constructor() {
this.searchInput = document.getElementById('search');
this.categoryFilter = document.getElementById('category-filter');
this.iconGrid = document.getElementById('icon-grid');
this.icons = Array.from(document.querySelectorAll('.icon-card'));
this.bindEvents();
}
bindEvents() {
this.searchInput.addEventListener('input', () => this.filterIcons());
this.categoryFilter.addEventListener('change', () => this.filterIcons());
}
filterIcons() {
const searchTerm = this.searchInput.value.toLowerCase();
const selectedCategory = this.categoryFilter.value;
this.icons.forEach(icon => {
const name = icon.dataset.name.toLowerCase();
const category = icon.dataset.category;
const matchesSearch = name.includes(searchTerm);
const matchesCategory = !selectedCategory || category === selectedCategory;
if (matchesSearch && matchesCategory) {
icon.style.display = 'block';
} else {
icon.style.display = 'none';
}
});
this.updateResultCount();
}
updateResultCount() {
const visibleIcons = this.icons.filter(icon => icon.style.display !== 'none');
document.getElementById('result-count').textContent =
`${visibleIcons.length} Icons gefunden`;
}
}
// Initialize on DOM ready
document.addEventListener('DOMContentLoaded', () => {
new IconSearch();
});
Icon-Preview Modal¶
// Icon Preview Modal
class IconPreview {
constructor() {
this.modal = this.createModal();
this.bindEvents();
}
createModal() {
const modal = document.createElement('div');
modal.className = 'icon-modal';
modal.innerHTML = `
<div class="modal-content">
<span class="close">×</span>
<div class="icon-large"></div>
<div class="icon-details">
<h2 class="icon-name"></h2>
<p class="icon-category"></p>
<code class="icon-filename"></code>
<div class="usage-examples">
<h3>HTML:</h3>
<code class="html-code"></code>
<h3>CSS:</h3>
<code class="css-code"></code>
</div>
</div>
</div>
`;
document.body.appendChild(modal);
return modal;
}
showPreview(iconElement) {
const iconData = {
name: iconElement.dataset.name,
category: iconElement.dataset.category,
filename: iconElement.querySelector('code').textContent,
path: iconElement.querySelector('img').src
};
this.populateModal(iconData);
this.modal.style.display = 'block';
}
populateModal(iconData) {
this.modal.querySelector('.icon-large').innerHTML =
`<img src="${iconData.path}" alt="${iconData.name}">`;
this.modal.querySelector('.icon-name').textContent = iconData.name;
this.modal.querySelector('.icon-category').textContent = iconData.category;
this.modal.querySelector('.icon-filename').textContent = iconData.filename;
// Usage examples
this.modal.querySelector('.html-code').textContent =
`<img src="icons/${iconData.filename}" alt="${iconData.name}" class="icon">`;
this.modal.querySelector('.css-code').textContent =
`.${iconData.name}-icon { background: url('icons/${iconData.filename}'); }`;
}
}
API-Integration¶
// Dynamic Icon Loading via API
class IconAPI {
constructor() {
this.baseUrl = '/api';
}
async getAllIcons() {
try {
const response = await fetch(`${this.baseUrl}/icons`);
const data = await response.json();
return data;
} catch (error) {
console.error('Failed to load icons:', error);
return { icons: [], categories: {} };
}
}
async getIcon(name) {
try {
const response = await fetch(`${this.baseUrl}/icon/${name}`);
if (!response.ok) throw new Error('Icon not found');
return await response.json();
} catch (error) {
console.error(`Failed to load icon ${name}:`, error);
return null;
}
}
// Dynamic grid updates
async refreshIconGrid() {
const { icons, categories } = await this.getAllIcons();
this.renderIconGrid(icons);
this.updateCategoryFilter(categories);
}
}
CSS-Styling und Layout¶
Responsive Grid-System¶
/* Icon Grid Layout */
.icon-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(140px, 1fr));
gap: 1.5rem;
padding: 2rem 0;
}
/* Responsive Breakpoints */
@media (max-width: 768px) {
.icon-grid {
grid-template-columns: repeat(auto-fill, minmax(120px, 1fr));
gap: 1rem;
}
}
@media (max-width: 480px) {
.icon-grid {
grid-template-columns: repeat(auto-fill, minmax(100px, 1fr));
gap: 0.75rem;
}
}
Icon-Card Styling¶
.icon-card {
background: white;
border: 1px solid #e0e0e0;
border-radius: 12px;
padding: 1.5rem 1rem;
text-align: center;
transition: all 0.3s ease;
cursor: pointer;
}
.icon-card:hover {
transform: translateY(-4px);
box-shadow: 0 8px 25px rgba(0, 0, 0, 0.15);
border-color: #007bff;
}
.icon-preview {
width: 48px;
height: 48px;
margin: 0 auto 1rem;
display: flex;
align-items: center;
justify-content: center;
}
.icon-preview img {
max-width: 100%;
max-height: 100%;
filter: none;
transition: filter 0.2s;
}
.icon-card:hover .icon-preview img {
filter: brightness(1.1);
}
Dark Mode Support¶
/* Dark Mode Variables */
:root {
--bg-color: #ffffff;
--text-color: #333333;
--border-color: #e0e0e0;
--card-bg: #ffffff;
}
[data-theme="dark"] {
--bg-color: #1a1a1a;
--text-color: #ffffff;
--border-color: #404040;
--card-bg: #2a2a2a;
}
body {
background-color: var(--bg-color);
color: var(--text-color);
transition: background-color 0.3s, color 0.3s;
}
.icon-card {
background-color: var(--card-bg);
border-color: var(--border-color);
}
UI-Komponenten¶
Search Bar Component¶
<div class="search-container">
<div class="search-input-wrapper">
<svg class="search-icon" viewBox="0 0 24 24">
<path d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"/>
</svg>
<input type="search"
id="search"
placeholder="Icon name oder Kategorie suchen..."
autocomplete="off">
<button class="clear-search" style="display: none;">×</button>
</div>
<div class="search-results">
<span id="result-count">162 Icons</span>
</div>
</div>
Category Filter Component¶
<div class="filter-container">
<label for="category-filter">Kategorie:</label>
<select id="category-filter" class="category-select">
<option value="">Alle Kategorien</option>
{% for category, icons in categories.items() %}
<option value="{{ category }}">
{{ category }} ({{ icons|length }})
</option>
{% endfor %}
</select>
<button id="reset-filters" class="reset-btn">Filter zurücksetzen</button>
</div>
Loading States¶
/* Loading Skeleton */
.icon-card.loading {
background: linear-gradient(90deg, #f0f0f0 25%, #e0e0e0 50%, #f0f0f0 75%);
background-size: 200% 100%;
animation: loading 1.5s infinite;
}
@keyframes loading {
0% { background-position: 200% 0; }
100% { background-position: -200% 0; }
}
/* Loading Spinner */
.loading-spinner {
width: 40px;
height: 40px;
border: 4px solid #f3f3f3;
border-top: 4px solid #007bff;
border-radius: 50%;
animation: spin 1s linear infinite;
margin: 2rem auto;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
Performance-Optimierung¶
Lazy Loading für Icons¶
// Intersection Observer für Lazy Loading
class LazyIconLoader {
constructor() {
this.observer = new IntersectionObserver(
(entries) => this.handleIntersection(entries),
{ rootMargin: '50px' }
);
this.initObserver();
}
initObserver() {
document.querySelectorAll('.icon-card img[data-src]').forEach(img => {
this.observer.observe(img);
});
}
handleIntersection(entries) {
entries.forEach(entry => {
if (entry.isIntersecting) {
const img = entry.target;
img.src = img.dataset.src;
img.removeAttribute('data-src');
this.observer.unobserve(img);
}
});
}
}
Virtual Scrolling für große Icon-Mengen¶
// Virtual Scrolling Implementation
class VirtualIconGrid {
constructor(container, icons, itemHeight = 180) {
this.container = container;
this.icons = icons;
this.itemHeight = itemHeight;
this.viewportHeight = window.innerHeight;
this.renderBuffer = 5; // Extra items outside viewport
this.setupVirtualScrolling();
}
setupVirtualScrolling() {
this.container.style.height = `${this.icons.length * this.itemHeight}px`;
this.container.style.position = 'relative';
window.addEventListener('scroll', () => this.updateVisibleItems());
this.updateVisibleItems();
}
updateVisibleItems() {
const scrollTop = window.pageYOffset;
const startIndex = Math.max(0,
Math.floor(scrollTop / this.itemHeight) - this.renderBuffer);
const endIndex = Math.min(this.icons.length,
Math.ceil((scrollTop + this.viewportHeight) / this.itemHeight) + this.renderBuffer);
this.renderItems(startIndex, endIndex);
}
}
Browser-Kompatibilität¶
Supported Browsers¶
Browser | Version | Support Level |
---|---|---|
Chrome | 70+ | Full |
Firefox | 65+ | Full |
Safari | 12+ | Full |
Edge | 79+ | Full |
IE 11 | - | Partial (fallbacks) |
Progressive Enhancement¶
// Feature Detection
const supportsES6 = (() => {
try {
return new Function("(a = 0) => a");
} catch (err) {
return false;
}
})();
if (supportsES6) {
// Modern JavaScript features
import('./modern-icon-browser.js');
} else {
// ES5 fallback
loadScript('legacy-icon-browser.js');
}
Polyfills¶
<!-- Optional Polyfills für ältere Browser -->
<script src="https://polyfill.io/v3/polyfill.min.js?features=fetch,IntersectionObserver"></script>
Accessibility¶
ARIA Labels und Semantic HTML¶
<div class="icon-card"
role="button"
tabindex="0"
aria-label="Icon: {{ icon.display_name }}, Category: {{ icon.category }}">
<img src="{{ icon.path }}"
alt="{{ icon.display_name }} icon"
role="img">
<h3 id="icon-{{ icon.name }}-title">{{ icon.display_name }}</h3>
<p aria-describedby="icon-{{ icon.name }}-title">{{ icon.category }}</p>
</div>
Keyboard Navigation¶
// Keyboard Navigation Support
class KeyboardNavigation {
constructor() {
this.focusableElements = document.querySelectorAll('.icon-card[tabindex="0"]');
this.currentIndex = 0;
this.bindKeyboardEvents();
}
bindKeyboardEvents() {
document.addEventListener('keydown', (e) => {
switch(e.key) {
case 'ArrowRight':
this.moveRight();
break;
case 'ArrowLeft':
this.moveLeft();
break;
case 'Enter':
case ' ':
this.activateCurrentItem();
break;
}
});
}
moveRight() {
this.currentIndex = Math.min(this.currentIndex + 1, this.focusableElements.length - 1);
this.focusableElements[this.currentIndex].focus();
}
activateCurrentItem() {
this.focusableElements[this.currentIndex].click();
}
}
Evidenz: Web Accessibility Guidelines, semantic HTML patterns
Integration Points: - Backend API für Daten-Bereitstellung - Static File Serving für Icon-Delivery - Error Handling für User-Feedback