Commit e085b7ae authored by Gemini's avatar Gemini Committed by Jakob Moser
Browse files

feat: Implement tab navigation for song list filtering (All, Favorites, Popular)

parent b36eae4e
Loading
Loading
Loading
Loading
+54 −39
Original line number Diff line number Diff line
// SongList.mjs
import SongItem from './SongItem.mjs';
import initialSongs from './mockData.mjs'; // Deine Mock-Daten
import initialSongs from './mockData.mjs';

const SongList = {
    songs: [],
    groupedAndSortedSongs: {},
    allSongs: [], // Speichert alle initial geladenen Songs
    isLoading: true,

    loadSongs: function() {
    loadAllSongs: function() {
        this.isLoading = true;
        // Simuliere einen API-Call
        // In echt: m.request({ url: "https://example.com/api/songs" }).then(...)
        new Promise(resolve => setTimeout(() => resolve(initialSongs), 200)) // Kurze Verzögerung für Lade-Effekt
        // Simulierter API-Call
        new Promise(resolve => setTimeout(() => resolve(initialSongs), 200))
            .then(data => {
                this.songs = data.map(s => ({ ...s, isFavorite: !!s.isFavorite })); // Stelle sicher, dass isFavorite existiert
                this.processSongs();
                this.allSongs = data.map(s => ({ ...s, isFavorite: !!s.isFavorite }));
                this.isLoading = false;
                m.redraw(); // Wichtig nach asynchronen Operationen außerhalb Mithrils Lebenszyklus
                m.redraw();
            }).catch(error => {
                console.error("Fehler beim Laden der Songs:", error);
                this.isLoading = false;
                // Hier könntest du eine Fehlermeldung anzeigen
                m.redraw();
            });
    },

    oninit: function() {
        this.loadSongs();
        this.loadAllSongs();
    },

    processSongs: function() {
    getProcessedSongsForView: function(viewFilter) {
        let songsToProcess = [];
        let viewSpecificMessage = null;

        if (this.allSongs && this.allSongs.length > 0) {
            switch (viewFilter) {
                case 'favorites':
                    songsToProcess = this.allSongs.filter(song => song.isFavorite);
                    if (songsToProcess.length === 0) {
                        viewSpecificMessage = "Du hast noch keine Favoriten markiert.";
                    }
                    break;
                case 'popular':
                    // Hier könnten später Songs von 'https://example.com/api/songs/popular' geladen werden
                    songsToProcess = []; // Fürs Erste leer lassen
                    viewSpecificMessage = "Die Liste der beliebten Songs ist bald verfügbar!";
                    break;
                case 'all':
                default:
                    songsToProcess = this.allSongs;
                    if (songsToProcess.length === 0) {
                        viewSpecificMessage = "Keine Songs in der Bibliothek.";
                    }
                    break;
            }
        } else if (!this.isLoading) {
             viewSpecificMessage = "Keine Songs geladen oder Bibliothek ist leer.";
        }


        const groups = {};
        this.songs.forEach(song => {
        songsToProcess.forEach(song => {
            groups[song.artist] = groups[song.artist] || [];
            groups[song.artist].push(song);
        });
@@ -42,49 +67,39 @@ const SongList = {
        sortedArtistNames.forEach(artist => {
            finalGroupedSongs[artist] = groups[artist].sort((a, b) => a.title.localeCompare(b.title));
        });
        this.groupedAndSortedSongs = finalGroupedSongs;

        return { groupedSongs: finalGroupedSongs, message: viewSpecificMessage, hasContent: songsToProcess.length > 0 };
    },

    toggleFavorite: function(songId) {
        const song = this.songs.find(s => s.id === songId);
        const song = this.allSongs.find(s => s.id === songId);
        if (song) {
            song.isFavorite = !song.isFavorite;
            // Hier würdest du den API Call machen:
            // m.request({
            //     method: "PUT",
            //     url: `https://example.com/api/songs/${song.id}/favorite`,
            //     body: { isFavorite: song.isFavorite }
            // }).then(() => {
            //     console.log(`Favoritenstatus für ${song.title} geändert.`);
            // }).catch(e => {
            //     console.error("Fehler beim Ändern des Favoritenstatus:", e);
            //     song.isFavorite = !song.isFavorite; // Rollback bei Fehler
            //     m.redraw(); // UI aktualisieren
            // });

            // Für dieses Frontend-Beispiel reicht das direkte Ändern und Neuzeichnen (Mithril macht das bei Event-Handlern meist automatisch)
            // Da `groupedAndSortedSongs` von `this.songs` abhängt, muss es nicht neu prozessiert werden,
            // solange die Reihenfolge/Gruppierung sich nicht ändert, nur das Icon.
            // Ein m.redraw() wird implizit durch den Klick-Handler ausgelöst.
            // API Call: m.request({ method: "PUT", url: `https://example.com/api/songs/${song.id}/favorite`, body: { isFavorite: song.isFavorite }})
            // Mithril's automatisches Redraw nach dem Event-Handler aktualisiert die Ansicht.
            // Wenn die 'Favoriten'-Ansicht aktiv ist, wird die Liste korrekt neu gerendert.
        }
    },

    view: function() {
    view: function(vnode) {
        const { currentView } = vnode.attrs;

        if (this.isLoading) {
            return m("div.song-list-outer-container", m("div.loading-placeholder", "Lade Songs..."));
        }

        const artists = Object.keys(this.groupedAndSortedSongs);
        const { groupedSongs, message, hasContent } = this.getProcessedSongsForView(currentView);
        const artists = Object.keys(groupedSongs);

        if (artists.length === 0) {
            return m("div.song-list-outer-container", m("p", "Keine Songs verfügbar."));
        if (message && !hasContent) { // Zeige Nachricht nur, wenn keine Songs für die aktuelle Ansicht da sind
            return m("div.song-list-outer-container", m("p.view-message", message));
        }

        return m("div.song-list-outer-container",
            artists.map(artist =>
                m("div.artist-group", { key: artist }, [
                m("div.artist-group", { key: `${currentView}-${artist}` }, [ // Key um Eindeutigkeit bei View-Wechsel zu erhöhen
                    m("h2.artist-name", artist),
                    this.groupedAndSortedSongs[artist].map(song =>
                    groupedSongs[artist].map(song =>
                        m(SongItem, {
                            key: song.id,
                            song: song,

frontend/TabBar.mjs

0 → 100644
+25 −0
Original line number Diff line number Diff line
// TabBar.mjs
const TabBar = {
    view: function(vnode) {
        const { currentView, onViewChange } = vnode.attrs;
        const tabs = [
            { id: 'all', label: 'Alle' },
            { id: 'favorites', label: 'Favoriten' },
            { id: 'popular', label: 'Beliebt' } // 'Beliebt' als Option
        ];

        return m("div.tab-bar-container",
            tabs.map(tab => {
                return m("div.tab-item", {
                    class: currentView === tab.id ? 'active' : '',
                    onclick: () => onViewChange(tab.id),
                    role: "button", // Für Barrierefreiheit
                    tabindex: 0,    // Für Tastaturnavigation
                    onkeypress: (e) => { if (e.key === 'Enter' || e.key === ' ') onViewChange(tab.id); }
                }, tab.label);
            })
        );
    }
};

export default TabBar;
 No newline at end of file
+13 −1
Original line number Diff line number Diff line
// app.mjs
import SearchBar from './SearchBar.mjs';
import TabBar from './TabBar.mjs'; // Neu importiert
import SongList from './SongList.mjs';

const AppLayout = {
    currentView: 'all', // Mögliche Werte: 'all', 'favorites', 'popular'

    onViewChange: function(view) {
        this.currentView = view;
        // m.redraw() wird von Mithril automatisch nach Event-Handlern aufgerufen
    },

    view: function() {
        return m("div.app-layout", [
            m(SearchBar),
            m(SongList)
            m(TabBar, {
                currentView: this.currentView,
                onViewChange: this.onViewChange.bind(this) // .bind(this) ist wichtig
            }),
            m(SongList, { currentView: this.currentView }) // Übergabe des aktuellen Views
        ]);
    }
};
+78 −26
Original line number Diff line number Diff line
/* Globale Resets und Basis-Styling */
/* Globale Resets und Basis-Styling (wie vorher) */
body, html {
    margin: 0;
    padding: 0;
    font-family: 'Roboto', sans-serif;
    background-color: #f4f4f9; /* Heller Hintergrund für Kontrast */
    background-color: #f4f4f9;
    color: #333;
    line-height: 1.6;
}
@@ -13,14 +13,14 @@ body, html {
    margin: 0 auto;
}

.loading-placeholder {
.loading-placeholder, .view-message { /* Kombiniert für Konsistenz */
    text-align: center;
    padding: 50px;
    font-size: 1.2em;
    color: #777;
    padding: 40px 20px;
    font-size: 1.1em;
    color: #757575;
}

/* Suchleiste */
/* Suchleiste (Höhe ca. 60px) */
.search-bar-container {
    position: fixed;
    top: 0;
@@ -30,6 +30,8 @@ body, html {
    padding: 12px 16px;
    z-index: 1000;
    box-shadow: 0 2px 5px rgba(0,0,0,0.2);
    box-sizing: border-box; /* Stellt sicher, dass Padding die Höhe nicht sprengt */
    height: 60px; /* Feste Höhe für genaue Positionierung */
}

.search-input {
@@ -47,27 +49,69 @@ body, html {
    color: #888;
}

/* Tab Bar (Höhe ca. 48px) */
.tab-bar-container {
    display: flex;
    position: fixed;
    top: 60px; /* Direkt unter der Suchleiste */
    left: 0;
    right: 0;
    background-color: #3F51B5; /* Gleiche Farbe wie Suchleiste für einen einheitlichen Header-Block */
    z-index: 999; /* Unter der Suchleiste, falls sie überlappen könnten */
    box-shadow: 0 2px 4px rgba(0,0,0,0.15); /* Dezenter Schatten */
    height: 48px; /* Feste Höhe */
    box-sizing: border-box;
}

.tab-item {
    flex-grow: 1;
    display: flex; /* Für vertikales Zentrieren des Texts */
    align-items: center;
    justify-content: center;
    padding: 0 8px; /* Horizontal Padding */
    text-align: center;
    color: rgba(255,255,255,0.75); /* Etwas helleres, aber gedämpftes Weiß für inaktive Tabs */
    cursor: pointer;
    font-weight: 500;
    font-size: 0.9em; /* Leicht kleiner als Haupttext */
    text-transform: uppercase;
    border-bottom: 3px solid transparent; /* Platzhalter für den aktiven Indikator */
    transition: color 0.2s ease-in-out, border-bottom-color 0.2s ease-in-out;
    user-select: none;
}

.tab-item:hover {
    background-color: rgba(255,255,255,0.1); /* Leichter Hover-Effekt */
}

.tab-item.active {
    color: #FFFFFF; /* Reines Weiß für aktiven Tab */
    border-bottom-color: #FF4081; /* Material Accent Pink für den Indikator */
}

/* Song-Liste Container */
.song-list-outer-container {
    padding-top: 60px; /* Höhe der Suchleiste + etwas Puffer */
    /* padding-top: calc(60px + 48px + 10px); /* Höhe Suchleiste + Höhe TabBar + kleiner Abstand */
    padding-top: calc(60px + 48px); /* Höhe Suchleiste + Höhe TabBar */
    padding-bottom: 20px;
}

/* Restliche Styles für Artist-Group, Song-Item etc. bleiben wie zuvor */
.artist-group {
    margin-bottom: 12px;
    margin: 0 8px 12px 8px; /* Etwas seitlicher Margin */
    background-color: #ffffff;
    border-radius: 4px;
    box-shadow: 0 1px 3px rgba(0,0,0,0.1);
    overflow: hidden; /* Für abgerundete Ecken bei Kindern */
    overflow: hidden;
}

.artist-name {
    padding: 12px 16px;
    background-color: #E8EAF6; /* Indigo Lighten-5 */
    color: #303F9F; /* Indigo Darken-2 */
    background-color: #E8EAF6;
    color: #303F9F;
    font-size: 1.1em;
    font-weight: 500;
    border-bottom: 1px solid #C5CAE9; /* Indigo Lighten-4 */
    border-bottom: 1px solid #C5CAE9;
}

.song-item {
@@ -89,28 +133,37 @@ body, html {

.song-info {
    flex-grow: 1;
    margin-right: 8px; /* Abstand zum Icon */
    overflow: hidden; /* Verhindert Textüberlauf */
}

.song-title {
    font-size: 1em;
    font-weight: 400;
    color: #212121; /* Almost Black */
    color: #212121;
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis; /* Text abschneiden bei Überlauf */
}

.song-artist {
    font-size: 0.85em;
    color: #757575; /* Grey */
    color: #757575;
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}

.song-actions {
    margin-left: 16px;
    margin-left: auto; /* Pusht das Icon nach rechts, falls song-info nicht flex-grow hätte */
    flex-shrink: 0; /* Verhindert, dass das Icon schrumpft */
}

.heart-icon {
    cursor: pointer;
    color: #BDBDBD; /* Grey Lighten-1 (Outline) */
    font-size: 24px; /* Standardgröße für Material Icons */
    user-select: none; /* Verhindert Textauswahl beim Klicken */
    color: #BDBDBD;
    font-size: 24px;
    user-select: none;
    transition: color 0.2s ease-in-out, transform 0.1s ease-out;
    vertical-align: middle;
}
@@ -120,15 +173,14 @@ body, html {
}

.heart-icon.is-favorite {
    color: #FF4081; /* Pink A200 - Material Accent */
    font-variation-settings: 'FILL' 1; /* Füllt das Icon */
    color: #FF4081;
    font-variation-settings: 'FILL' 1;
}

/* Material Symbols Basis-Styling (optional, oft schon gut) */
.material-symbols-outlined {
  font-variation-settings:
  'FILL' 0,  /* 0 für Outline, 1 für Filled */
  'wght' 400, /* Schriftstärke */
  'GRAD' 0,  /* Verlauf */
  'opsz' 24  /* Optische Größe */
  'FILL' 0,
  'wght' 400,
  'GRAD' 0,
  'opsz' 24;
}
 No newline at end of file