Skip to content

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">&times;</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