import ManaCore import SwiftUI /// Explore-Tab: Featured + Trending Sections, plus Browse-Link. struct ExploreView: View { @Binding var deepLinkSlug: String? @Environment(AuthClient.self) private var auth @State private var store: MarketplaceStore? @State private var path: [MarketplaceRoute] = [] init(deepLinkSlug: Binding = .constant(nil)) { _deepLinkSlug = deepLinkSlug } var body: some View { NavigationStack(path: $path) { ZStack { CardsTheme.background.ignoresSafeArea() content } .navigationTitle("Entdecken") .navigationDestination(for: MarketplaceRoute.self) { route in switch route { case .browse: BrowseView() case let .publicDeck(slug): PublicDeckView(slug: slug) } } .navigationDestination(for: String.self) { deckId in DeckDetailView(deckId: deckId) } .refreshable { await store?.loadExplore() } .task { if store == nil { store = MarketplaceStore(auth: auth) } await store?.loadExplore() } .onChange(of: deepLinkSlug) { _, newSlug in guard let slug = newSlug else { return } path = [.publicDeck(slug: slug)] deepLinkSlug = nil } } } @ViewBuilder private var content: some View { if let store { if store.isLoadingExplore, store.featured.isEmpty, store.trending.isEmpty { ProgressView() .tint(CardsTheme.primary) } else if let message = store.errorMessage, store.featured.isEmpty { ContentUnavailableView( "Marketplace nicht erreichbar", systemImage: "wifi.exclamationmark", description: Text(message) ) .foregroundStyle(CardsTheme.foreground) } else { ScrollView { VStack(alignment: .leading, spacing: 24) { if !store.featured.isEmpty { section(title: "Vorgestellt", items: store.featured) } if !store.trending.isEmpty { section(title: "Im Trend", items: store.trending) } NavigationLink(value: MarketplaceRoute.browse) { HStack { Label("Alle Decks durchsuchen", systemImage: "magnifyingglass") Spacer() Image(systemName: "chevron.right") .font(.footnote) } .padding() .background(CardsTheme.surface, in: RoundedRectangle(cornerRadius: 10)) .overlay( RoundedRectangle(cornerRadius: 10) .stroke(CardsTheme.border, lineWidth: 1) ) .foregroundStyle(CardsTheme.foreground) } .buttonStyle(.plain) .padding(.horizontal, 16) } .padding(.vertical, 16) } } } } private func section(title: String, items: [PublicDeckEntry]) -> some View { VStack(alignment: .leading, spacing: 12) { Text(title) .font(.title3.weight(.semibold)) .foregroundStyle(CardsTheme.foreground) .padding(.horizontal, 16) ScrollView(.horizontal, showsIndicators: false) { HStack(spacing: 12) { ForEach(items) { item in NavigationLink(value: MarketplaceRoute.publicDeck(slug: item.slug)) { PublicDeckCard(entry: item) } .buttonStyle(.plain) } } .padding(.horizontal, 16) } } } } /// Routen-Enum für die Marketplace-NavigationStack. enum MarketplaceRoute: Hashable { case browse case publicDeck(slug: String) } /// Public-Deck-Karten-Tile in Featured/Trending-Carousels und Browse-Grid. /// Selbes Tile-Layout wie DeckStackTile (5:7 Aspect-Ratio, /// CardSurface, Kategorie-Icon oben rechts), aber für PublicDeckEntry- /// Daten. Star-Count statt Edit-Button unten rechts. struct PublicDeckCard: View { let entry: PublicDeckEntry var body: some View { ZStack { CardSurface(size: .md, elevation: .standard, colorAccentHex: nil) { cardContent } } .aspectRatio(5.0 / 7.0, contentMode: .fit) .frame(maxWidth: 280) } private var cardContent: some View { VStack(alignment: .leading, spacing: 6) { HStack(alignment: .top) { if entry.isFeatured { Image(systemName: "star.fill") .font(.caption) .foregroundStyle(CardsTheme.warning) } Spacer() Image(systemName: categorySymbol) .font(.title2) .foregroundStyle(CardsTheme.primary.opacity(0.85)) } Spacer(minLength: 0) VStack(alignment: .leading, spacing: 6) { Text(entry.title) .font(.system(size: 17, weight: .semibold)) .foregroundStyle(CardsTheme.foreground) .lineLimit(3) if let description = entry.description, !description.isEmpty { Text(description) .font(.caption) .foregroundStyle(CardsTheme.mutedForeground) .lineLimit(2) } } Spacer(minLength: 0) VStack(alignment: .leading, spacing: 6) { HStack(spacing: 8) { Label("\(entry.cardCount)", systemImage: "rectangle.stack") .font(.caption2) .foregroundStyle(CardsTheme.mutedForeground) Label("\(entry.starCount)", systemImage: "star.fill") .font(.caption2) .foregroundStyle(CardsTheme.warning) if entry.isPaid { Label("\(entry.priceCredits)", systemImage: "creditcard") .font(.caption2.weight(.semibold)) .foregroundStyle(CardsTheme.primary) } Spacer() } HStack(spacing: 4) { Text(entry.owner.displayName) .font(.caption2) .foregroundStyle(CardsTheme.mutedForeground) .lineLimit(1) if entry.owner.verifiedMana { Image(systemName: "checkmark.seal.fill") .font(.caption2) .foregroundStyle(CardsTheme.primary) } } } } .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topLeading) } private var categorySymbol: String { guard let category = entry.category, let parsed = DeckCategory(rawValue: category) else { return "rectangle.stack" } return parsed.systemImageName } }