From 82fe72bc074f9fda454e6aca5aa157d9c13cb0ab Mon Sep 17 00:00:00 2001 From: Aprameya Kannan Date: Thu, 15 Jan 2026 23:04:13 -0500 Subject: [PATCH] simplify ui for incremental features --- frontend/MusicApp/ViewModels/AppState.swift | 14 - .../ViewModels/HomeFeedViewModel.swift | 24 +- frontend/MusicApp/Views/BottomNavView.swift | 98 ------- .../MusicApp/Views/GlobalRankingsView.swift | 216 --------------- frontend/MusicApp/Views/HomeFeedView.swift | 70 +---- frontend/MusicApp/Views/MainAppView.swift | 26 +- .../MusicApp/Views/NotificationsView.swift | 177 ------------ frontend/MusicApp/Views/ProfileView.swift | 106 +++++++ frontend/MusicApp/Views/SocialView.swift | 226 --------------- .../MusicApp/Views/TasteProfileView.swift | 261 ------------------ 10 files changed, 119 insertions(+), 1099 deletions(-) delete mode 100644 frontend/MusicApp/Views/BottomNavView.swift delete mode 100644 frontend/MusicApp/Views/GlobalRankingsView.swift delete mode 100644 frontend/MusicApp/Views/NotificationsView.swift create mode 100644 frontend/MusicApp/Views/ProfileView.swift delete mode 100644 frontend/MusicApp/Views/SocialView.swift delete mode 100644 frontend/MusicApp/Views/TasteProfileView.swift diff --git a/frontend/MusicApp/ViewModels/AppState.swift b/frontend/MusicApp/ViewModels/AppState.swift index 57d6d2b..2b00f41 100644 --- a/frontend/MusicApp/ViewModels/AppState.swift +++ b/frontend/MusicApp/ViewModels/AppState.swift @@ -9,17 +9,8 @@ enum AppScreen { case main } -enum ActiveTab: String, CaseIterable { - case pulse = "pulse" - case charts = "charts" - case profile = "profile" - case social = "social" - case notifications = "notifications" -} - class AppState: ObservableObject { @Published var currentScreen: AppScreen = .splash - @Published var activeTab: ActiveTab = .pulse @Published var hasCompletedOnboarding: Bool = false @Published var isAuthenticated: Bool = false @Published var currentUser: User? @@ -91,11 +82,6 @@ class AppState: ObservableObject { KeychainHelper.clearAll() currentScreen = .authentication } - - @MainActor - func setActiveTab(_ tab: ActiveTab) { - activeTab = tab - } } extension AppState { diff --git a/frontend/MusicApp/ViewModels/HomeFeedViewModel.swift b/frontend/MusicApp/ViewModels/HomeFeedViewModel.swift index 14facc8..e4d2237 100644 --- a/frontend/MusicApp/ViewModels/HomeFeedViewModel.swift +++ b/frontend/MusicApp/ViewModels/HomeFeedViewModel.swift @@ -2,24 +2,9 @@ import Foundation import Combine import SwiftUI -enum FeedFilter: String, CaseIterable { - case trending = "trending" - case forYou = "forYou" - case following = "following" - - var displayName: String { - switch self { - case .trending: return "Trending" - case .forYou: return "For You" - case .following: return "Following" - } - } -} - @MainActor class HomeFeedViewModel: ObservableObject { @Published var feedItems: [MusicItem] = [] - @Published var activeFilter: FeedFilter = .trending @Published var isLoading: Bool = false @Published var errorMessage: String? @Published var selectedItem: MusicItem? @@ -36,7 +21,7 @@ class HomeFeedViewModel: ObservableObject { errorMessage = nil do { - feedItems = try await musicService.getFeed(filter: activeFilter.rawValue) + feedItems = try await musicService.getFeed(filter: "forYou") } catch { errorMessage = error.localizedDescription } @@ -44,13 +29,6 @@ class HomeFeedViewModel: ObservableObject { isLoading = false } - func setFilter(_ filter: FeedFilter) { - activeFilter = filter - Task { - await loadFeed() - } - } - func selectItemForRating(_ item: MusicItem) { selectedItem = item showRatingModal = true diff --git a/frontend/MusicApp/Views/BottomNavView.swift b/frontend/MusicApp/Views/BottomNavView.swift deleted file mode 100644 index 5f7da40..0000000 --- a/frontend/MusicApp/Views/BottomNavView.swift +++ /dev/null @@ -1,98 +0,0 @@ -import SwiftUI - -struct BottomNavView: View { - let activeTab: ActiveTab - let onTabChange: (ActiveTab) -> Void - - var body: some View { - HStack(spacing: 0) { - ForEach(ActiveTab.allCases, id: \.self) { tab in - Button(action: { - withAnimation(.spring(response: 0.3, dampingFraction: 0.7)) { - onTabChange(tab) - } - }) { - VStack(spacing: 4) { - Image(systemName: iconName(for: tab)) - .font(.system(size: 24)) - .foregroundColor( - activeTab == tab ? - AppColors.primary : - AppColors.textSecondary - ) - - Text(tabLabel(for: tab)) - .font(.system(size: 10)) - .foregroundColor( - activeTab == tab ? - AppColors.primary : - AppColors.textSecondary - ) - } - .frame(maxWidth: .infinity) - .padding(.vertical, 8) - .background( - Group { - if activeTab == tab { - RoundedRectangle(cornerRadius: 12) - .fill(AppColors.accentLight.opacity(0.5)) - } - } - ) - } - .buttonStyle(PlainButtonStyle()) - } - } - .padding(.horizontal, 8) - .padding(.vertical, 8) - .background(AppColors.cardBackground) - .overlay( - Rectangle() - .frame(height: 1) - .foregroundColor(AppColors.border), - alignment: .top - ) - .overlay( - Group { - if let activeIndex = ActiveTab.allCases.firstIndex(of: activeTab) { - GeometryReader { geometry in - let tabWidth = geometry.size.width / CGFloat(ActiveTab.allCases.count) - let xOffset = CGFloat(activeIndex) * tabWidth + tabWidth / 2 - 16 - - Capsule() - .fill(AppColors.primary) - .frame(width: 32, height: 2) - .offset(x: xOffset, y: -1) - .animation(.spring(response: 0.3, dampingFraction: 0.7), value: activeTab) - } - } - }, - alignment: .top - ) - } - - private func iconName(for tab: ActiveTab) -> String { - switch tab { - case .pulse: return "flame.fill" - case .charts: return "trophy.fill" - case .profile: return "person.fill" - case .social: return "person.2.fill" - case .notifications: return "bell.fill" - } - } - - private func tabLabel(for tab: ActiveTab) -> String { - switch tab { - case .pulse: return "Pulse" - case .charts: return "Charts" - case .profile: return "Profile" - case .social: return "Social" - case .notifications: return "Alerts" - } - } -} - -#Preview { - BottomNavView(activeTab: .pulse, onTabChange: { _ in }) - .background(AppColors.background) -} diff --git a/frontend/MusicApp/Views/GlobalRankingsView.swift b/frontend/MusicApp/Views/GlobalRankingsView.swift deleted file mode 100644 index 1e15c75..0000000 --- a/frontend/MusicApp/Views/GlobalRankingsView.swift +++ /dev/null @@ -1,216 +0,0 @@ -import SwiftUI - -struct GlobalRankingsView: View { - @StateObject private var viewModel = RankingViewModel() - - var body: some View { - ZStack { - AppColors.background - .ignoresSafeArea() - - VStack(spacing: 0) { - headerView - typeSelectorView - contentView - } - } - .task { - await viewModel.loadRankings() - } - } - - private var headerView: some View { - VStack(alignment: .leading, spacing: 8) { - Text("Global Charts") - .font(.system(size: 32, weight: .bold)) - .foregroundColor(AppColors.textPrimary) - - Text("Powered by your ratings") - .font(.system(size: 14)) - .foregroundColor(AppColors.textSecondary) - } - .frame(maxWidth: .infinity, alignment: .leading) - .padding(.horizontal, AppStyles.paddingMedium) - .padding(.top, AppStyles.paddingLarge) - .padding(.bottom, AppStyles.paddingMedium) - } - - private var typeSelectorView: some View { - HStack(spacing: 4) { - ForEach(RankingType.allCases, id: \.self) { type in - typeButton(for: type) - } - } - .padding(4) - .background(AppColors.cardBackground) - .cornerRadius(AppStyles.cornerRadiusMedium) - .overlay( - RoundedRectangle(cornerRadius: AppStyles.cornerRadiusMedium) - .stroke(AppColors.border, lineWidth: 1) - ) - .padding(.horizontal, AppStyles.paddingMedium) - .padding(.bottom, AppStyles.paddingMedium) - } - - private func typeButton(for type: RankingType) -> some View { - Button(action: { - viewModel.setType(type) - }) { - Text(type.displayName) - .font(.system(size: 14, weight: .medium)) - .foregroundColor( - viewModel.activeType == type ? - AppColors.textPrimary : - AppColors.textSecondary - ) - .frame(maxWidth: .infinity) - .padding(.vertical, 10) - .background( - Group { - if viewModel.activeType == type { - AppColors.primary - } else { - Color.clear - } - } - ) - .cornerRadius(AppStyles.cornerRadiusMedium) - } - } - - @ViewBuilder - private var contentView: some View { - if viewModel.isLoading { - Spacer() - ProgressView() - .tint(AppColors.primary) - Spacer() - } else if viewModel.rankings.isEmpty { - Spacer() - VStack(spacing: 16) { - Image(systemName: "trophy") - .font(.system(size: 48)) - .foregroundColor(AppColors.textSecondary) - - Text("No rankings available") - .font(.system(size: 18, weight: .medium)) - .foregroundColor(AppColors.textSecondary) - } - Spacer() - } else { - ScrollView { - LazyVStack(spacing: 12) { - ForEach(viewModel.rankings) { ranking in - RankingRowView(ranking: ranking) - .padding(.horizontal, AppStyles.paddingMedium) - } - } - .padding(.top, 8) - .padding(.bottom, 100) - } - } - } -} - -struct RankingRowView: View { - let ranking: RankingItem - - var body: some View { - HStack(spacing: 16) { - - VStack(spacing: 4) { - Text("\(ranking.rank)") - .font(.system(size: 24, weight: .bold)) - .foregroundColor( - ranking.rank <= 3 ? - AppColors.secondary : - AppColors.textPrimary - ) - - if ranking.change != 0 { - HStack(spacing: 2) { - Image(systemName: ranking.change > 0 ? "arrow.up" : "arrow.down") - .font(.system(size: 10)) - .foregroundColor( - ranking.change > 0 ? - AppColors.primary : - AppColors.secondary - ) - - Text("\(abs(ranking.change))") - .font(.system(size: 10)) - .foregroundColor( - ranking.change > 0 ? - AppColors.primary : - AppColors.secondary - ) - } - } - } - .frame(width: 40) - - ZStack(alignment: .topTrailing) { - AsyncImage(url: URL(string: ranking.imageUrl)) { image in - image - .resizable() - .aspectRatio(contentMode: .fill) - } placeholder: { - Rectangle() - .fill(AppColors.secondaryBackground) - } - .frame(width: 64, height: 64) - .cornerRadius(AppStyles.cornerRadiusMedium) - .clipped() - - if ranking.isNew { - ZStack { - Circle() - .fill(AppColors.secondary) - .frame(width: 20, height: 20) - - Image(systemName: "flame.fill") - .font(.system(size: 10)) - .foregroundColor(.white) - } - .offset(x: 4, y: -4) - } - } - - VStack(alignment: .leading, spacing: 4) { - Text(ranking.title) - .font(.system(size: 16, weight: .semibold)) - .foregroundColor(AppColors.textPrimary) - .lineLimit(1) - - Text(ranking.artist) - .font(.system(size: 14)) - .foregroundColor(AppColors.textSecondary) - .lineLimit(1) - - HStack(spacing: 8) { - HStack(spacing: 4) { - Image(systemName: "star.fill") - .font(.system(size: 12)) - .foregroundColor(AppColors.secondary) - - Text(String(format: "%.1f", ranking.rating)) - .font(.system(size: 14, weight: .medium)) - .foregroundColor(AppColors.textPrimary) - } - - Text("(\(ranking.ratingCount.formatted()))") - .font(.system(size: 12)) - .foregroundColor(AppColors.textSecondary) - } - } - - Spacer() - } - .padding(AppStyles.paddingMedium) - .cardStyle() - } -} - -#Preview { - GlobalRankingsView() -} diff --git a/frontend/MusicApp/Views/HomeFeedView.swift b/frontend/MusicApp/Views/HomeFeedView.swift index 4d8efce..f0291c4 100644 --- a/frontend/MusicApp/Views/HomeFeedView.swift +++ b/frontend/MusicApp/Views/HomeFeedView.swift @@ -3,6 +3,8 @@ import SwiftUI struct HomeFeedView: View { @StateObject private var viewModel = HomeFeedViewModel() @StateObject private var ratingViewModel = RatingViewModel() + @ObservedObject var appState = AppState.shared + @State private var showProfile = false var body: some View { ZStack { @@ -20,59 +22,14 @@ struct HomeFeedView: View { Spacer() - Button(action: {}) { - Image(systemName: "line.3.horizontal.decrease") - .font(.system(size: 20)) + Button(action: { + showProfile = true + }) { + Image(systemName: "person.circle.fill") + .font(.system(size: 24)) .foregroundColor(AppColors.textSecondary) .frame(width: 40, height: 40) - .background(AppColors.cardBackground) - .cornerRadius(AppStyles.cornerRadiusMedium) - .overlay( - RoundedRectangle(cornerRadius: AppStyles.cornerRadiusMedium) - .stroke(AppColors.border, lineWidth: 1) - ) - } - } - - ScrollView(.horizontal, showsIndicators: false) { - HStack(spacing: 8) { - ForEach(FeedFilter.allCases, id: \.self) { filter in - Button(action: { - viewModel.setFilter(filter) - }) { - HStack(spacing: 6) { - Image(systemName: iconName(for: filter)) - .font(.system(size: 14)) - - Text(filter.displayName) - .font(.system(size: 14, weight: .medium)) - } - .foregroundColor( - viewModel.activeFilter == filter ? - AppColors.textPrimary : - AppColors.textSecondary - ) - .padding(.horizontal, 16) - .padding(.vertical, 8) - .background( - viewModel.activeFilter == filter ? - AppColors.primary : - AppColors.cardBackground - ) - .cornerRadius(AppStyles.cornerRadiusMedium) - .overlay( - RoundedRectangle(cornerRadius: AppStyles.cornerRadiusMedium) - .stroke( - viewModel.activeFilter == filter ? - Color.clear : - AppColors.border, - lineWidth: 1 - ) - ) - } - } } - .padding(.horizontal, 4) } } .padding(.horizontal, AppStyles.paddingMedium) @@ -112,7 +69,7 @@ struct HomeFeedView: View { } } .padding(.top, 8) - .padding(.bottom, 100) + .padding(.bottom, 20) } } } @@ -135,18 +92,13 @@ struct HomeFeedView: View { ) } } + .sheet(isPresented: $showProfile) { + ProfileView(appState: appState) + } .task { await viewModel.loadFeed() } } - - private func iconName(for filter: FeedFilter) -> String { - switch filter { - case .trending: return "arrow.up" - case .forYou: return "sparkles" - case .following: return "person.2.fill" - } - } } #Preview { diff --git a/frontend/MusicApp/Views/MainAppView.swift b/frontend/MusicApp/Views/MainAppView.swift index f8b3d90..f516684 100644 --- a/frontend/MusicApp/Views/MainAppView.swift +++ b/frontend/MusicApp/Views/MainAppView.swift @@ -8,31 +8,7 @@ struct MainAppView: View { AppColors.background .ignoresSafeArea() - VStack(spacing: 0) { - - Group { - switch appState.activeTab { - case .pulse: - HomeFeedView() - case .charts: - GlobalRankingsView() - case .profile: - TasteProfileView() - case .social: - SocialView() - case .notifications: - NotificationsView() - } - } - .frame(maxWidth: .infinity, maxHeight: .infinity) - - BottomNavView( - activeTab: appState.activeTab, - onTabChange: { tab in - appState.setActiveTab(tab) - } - ) - } + HomeFeedView() } } } diff --git a/frontend/MusicApp/Views/NotificationsView.swift b/frontend/MusicApp/Views/NotificationsView.swift deleted file mode 100644 index a9ab99b..0000000 --- a/frontend/MusicApp/Views/NotificationsView.swift +++ /dev/null @@ -1,177 +0,0 @@ -import SwiftUI - -struct NotificationsView: View { - @StateObject private var viewModel = NotificationViewModel() - - var body: some View { - ZStack { - AppColors.background - .ignoresSafeArea() - - if viewModel.isLoading { - ProgressView() - .tint(AppColors.primary) - } else { - VStack(spacing: 0) { - - HStack { - VStack(alignment: .leading, spacing: 8) { - Text("Notifications") - .font(.system(size: 32, weight: .bold)) - .foregroundColor(AppColors.textPrimary) - - Text("See your impact on the charts") - .font(.system(size: 14)) - .foregroundColor(AppColors.textSecondary) - } - - Spacer() - - Button("Mark all read") { - Task { - await viewModel.markAllAsRead() - } - } - .font(.system(size: 14)) - .foregroundColor(AppColors.primary) - } - .padding(.horizontal, AppStyles.paddingMedium) - .padding(.top, AppStyles.paddingLarge) - .padding(.bottom, AppStyles.paddingMedium) - - if viewModel.notifications.isEmpty { - Spacer() - VStack(spacing: 16) { - Image(systemName: "bell") - .font(.system(size: 64)) - .foregroundColor(AppColors.primary.opacity(0.5)) - - Text("You're all caught up!") - .font(.system(size: 18, weight: .medium)) - .foregroundColor(AppColors.textSecondary) - - Text("Rate more music to see your impact.") - .font(.system(size: 14)) - .foregroundColor(AppColors.textSecondary) - } - Spacer() - } else { - ScrollView { - LazyVStack(spacing: 12) { - ForEach(viewModel.notifications) { notification in - NotificationCardView( - notification: notification, - color: viewModel.getNotificationColor(notification.type), - onTap: { - Task { - await viewModel.markAsRead(notification.id) - } - } - ) - .padding(.horizontal, AppStyles.paddingMedium) - } - } - .padding(.top, 8) - .padding(.bottom, 100) - } - } - } - } - } - .task { - await viewModel.loadNotifications() - } - } -} - -struct NotificationCardView: View { - let notification: AppNotification - let color: Color - let onTap: () -> Void - - var body: some View { - HStack(spacing: 0) { - - Rectangle() - .fill(color) - .frame(width: 4) - - HStack(spacing: 16) { - - ZStack { - Circle() - .fill(color.opacity(0.2)) - .frame(width: 48, height: 48) - - Image(systemName: iconName(for: notification.type)) - .font(.system(size: 24)) - .foregroundColor(color) - } - - VStack(alignment: .leading, spacing: 8) { - Text(notification.title) - .font(.system(size: 16, weight: .semibold)) - .foregroundColor(AppColors.textPrimary) - - Text(notification.message) - .font(.system(size: 14)) - .foregroundColor(AppColors.textSecondary) - .lineSpacing(4) - - Text(notification.timeAgo) - .font(.system(size: 12)) - .foregroundColor(AppColors.textSecondary) - } - - Spacer() - } - .padding(AppStyles.paddingMedium) - } - .cardStyle() - .onTapGesture { - onTap() - } - - if notification.type == .impact { - VStack(spacing: 0) { - Rectangle() - .fill( - AppColors.accentLight.opacity(0.3) - ) - .frame(height: 1) - - HStack { - Group { - if let influenceCount = notification.metadata?["influenceCount"] as? Int { - Text("You influenced \(influenceCount.formatted()) listeners") - } else { - Text("You made an impact!") - } - } - .font(.system(size: 14)) - .foregroundColor(AppColors.primary) - .padding(AppStyles.paddingSmall) - } - .frame(maxWidth: .infinity, alignment: .leading) - .background(AppColors.accentLight.opacity(0.3)) - .cornerRadius(AppStyles.cornerRadiusSmall) - .padding(.horizontal, AppStyles.paddingMedium) - .padding(.top, 8) - } - .padding(.leading, 4) - } - } - - private func iconName(for type: NotificationType) -> String { - switch type { - case .impact: return "arrow.up" - case .badge: return "trophy.fill" - case .social: return "person.2.fill" - case .trending: return "star.fill" - } - } -} - -#Preview { - NotificationsView() -} diff --git a/frontend/MusicApp/Views/ProfileView.swift b/frontend/MusicApp/Views/ProfileView.swift new file mode 100644 index 0000000..fddb494 --- /dev/null +++ b/frontend/MusicApp/Views/ProfileView.swift @@ -0,0 +1,106 @@ +import SwiftUI + +struct ProfileView: View { + @ObservedObject var appState: AppState + @Environment(\.dismiss) var dismiss + @State private var isLoggingOut = false + + var body: some View { + ZStack { + AppColors.background + .ignoresSafeArea() + + VStack(spacing: 0) { + HStack { + Button(action: { + dismiss() + }) { + Image(systemName: "xmark") + .font(.system(size: 18)) + .foregroundColor(AppColors.textSecondary) + .frame(width: 40, height: 40) + .background(AppColors.cardBackground) + .cornerRadius(AppStyles.cornerRadiusMedium) + .overlay( + RoundedRectangle(cornerRadius: AppStyles.cornerRadiusMedium) + .stroke(AppColors.border, lineWidth: 1) + ) + } + + Spacer() + + Text("Profile") + .font(.system(size: 20, weight: .semibold)) + .foregroundColor(AppColors.textPrimary) + + Spacer() + + Color.clear + .frame(width: 40, height: 40) + } + .padding(.horizontal, AppStyles.paddingMedium) + .padding(.top, AppStyles.paddingLarge) + .padding(.bottom, AppStyles.paddingLarge) + + VStack(spacing: 24) { + VStack(spacing: 16) { + ZStack { + Circle() + .fill(AppColors.primary) + .frame(width: 80, height: 80) + + Text(appState.currentUser?.username.prefix(1).uppercased() ?? "U") + .font(.system(size: 32, weight: .bold)) + .foregroundColor(.white) + } + + Text(appState.currentUser?.username ?? "User") + .font(.system(size: 24, weight: .semibold)) + .foregroundColor(AppColors.textPrimary) + } + .padding(.top, 40) + + Spacer() + + Button(action: { + Task { + isLoggingOut = true + do { + let authService = AuthService() + try await authService.logout() + } catch { + + } + appState.logout() + isLoggingOut = false + } + }) { + HStack { + if isLoggingOut { + ProgressView() + .tint(.white) + } else { + Image(systemName: "arrow.right.square") + .font(.system(size: 16)) + Text("Log Out") + .font(.system(size: 16, weight: .semibold)) + } + } + .foregroundColor(.white) + .frame(maxWidth: .infinity) + .padding(.vertical, AppStyles.paddingMedium) + .background(AppColors.accent) + .cornerRadius(AppStyles.cornerRadiusMedium) + } + .disabled(isLoggingOut) + .padding(.horizontal, AppStyles.paddingLarge) + .padding(.bottom, 40) + } + } + } + } +} + +#Preview { + ProfileView(appState: AppState.shared) +} diff --git a/frontend/MusicApp/Views/SocialView.swift b/frontend/MusicApp/Views/SocialView.swift deleted file mode 100644 index d4779cf..0000000 --- a/frontend/MusicApp/Views/SocialView.swift +++ /dev/null @@ -1,226 +0,0 @@ -import SwiftUI - -struct SocialView: View { - @StateObject private var viewModel = SocialViewModel() - - var body: some View { - ZStack { - AppColors.background - .ignoresSafeArea() - - if viewModel.isLoading { - ProgressView() - .tint(AppColors.primary) - } else { - ScrollView { - VStack(spacing: 24) { - - VStack(alignment: .leading, spacing: 8) { - Text("Social") - .font(.system(size: 32, weight: .bold)) - .foregroundColor(AppColors.textPrimary) - - Text("Compare taste with friends") - .font(.system(size: 14)) - .foregroundColor(AppColors.textSecondary) - } - .frame(maxWidth: .infinity, alignment: .leading) - .padding(.horizontal, AppStyles.paddingMedium) - .padding(.top, AppStyles.paddingLarge) - - Button(action: {}) { - HStack { - Image(systemName: "square.and.arrow.up") - .font(.system(size: 18)) - - Text("Share Your Taste Profile") - .font(.system(size: 16, weight: .semibold)) - } - .foregroundColor(.white) - .frame(maxWidth: .infinity) - .padding(.vertical, AppStyles.paddingMedium) - .background(AppColors.primary) - .cornerRadius(AppStyles.cornerRadiusMedium) - } - .padding(.horizontal, AppStyles.paddingMedium) - - VStack(alignment: .leading, spacing: 16) { - HStack(spacing: 8) { - Image(systemName: "person.2.fill") - .font(.system(size: 20)) - .foregroundColor(AppColors.primary) - - Text("Your Friends") - .font(.system(size: 18, weight: .semibold)) - .foregroundColor(AppColors.textPrimary) - } - - ForEach(viewModel.friends) { friend in - FriendCardView( - friend: friend, - compatibilityColor: viewModel.getCompatibilityColor(friend.compatibility), - compatibilityEmoji: viewModel.getCompatibilityEmoji(friend.compatibility) - ) - } - } - .padding(AppStyles.paddingMedium) - .cardStyle() - .padding(.horizontal, AppStyles.paddingMedium) - - VStack(spacing: 16) { - Image(systemName: "person.2.fill") - .font(.system(size: 48)) - .foregroundColor(AppColors.primary.opacity(0.5)) - - Text("Invite Friends") - .font(.system(size: 18, weight: .semibold)) - .foregroundColor(AppColors.textPrimary) - - Text("See how your taste compares with more friends") - .font(.system(size: 14)) - .foregroundColor(AppColors.textSecondary) - .multilineTextAlignment(.center) - - Button(action: {}) { - Text("Invite Friends") - .font(.system(size: 16, weight: .semibold)) - .foregroundColor(.white) - .frame(maxWidth: .infinity) - .padding(.vertical, 12) - .background(AppColors.primary) - .cornerRadius(AppStyles.cornerRadiusMedium) - } - } - .padding(AppStyles.paddingLarge) - .cardStyle() - .padding(.horizontal, AppStyles.paddingMedium) - .padding(.bottom, 100) - } - } - } - } - .task { - await viewModel.loadFriends() - } - } -} - -struct FriendCardView: View { - let friend: Friend - let compatibilityColor: Color - let compatibilityEmoji: String - - var body: some View { - VStack(spacing: 16) { - HStack(spacing: 16) { - - AsyncImage(url: URL(string: friend.avatar)) { image in - image - .resizable() - .aspectRatio(contentMode: .fill) - } placeholder: { - Circle() - .fill(AppColors.secondaryBackground) - } - .frame(width: 64, height: 64) - .clipShape(Circle()) - - VStack(alignment: .leading, spacing: 4) { - Text(friend.name) - .font(.system(size: 16, weight: .semibold)) - .foregroundColor(AppColors.textPrimary) - .lineLimit(1) - - Text(friend.username) - .font(.system(size: 14)) - .foregroundColor(AppColors.textSecondary) - .lineLimit(1) - - HStack(spacing: 12) { - HStack(spacing: 4) { - Image(systemName: "music.note") - .font(.system(size: 12)) - .foregroundColor(AppColors.textSecondary) - - Text(friend.topGenre) - .font(.system(size: 12)) - .foregroundColor(AppColors.textSecondary) - } - - Text("\(friend.sharedArtists) shared") - .font(.system(size: 12)) - .foregroundColor(AppColors.textSecondary) - } - } - - Spacer() - - VStack(spacing: 4) { - Text(compatibilityEmoji) - .font(.system(size: 32)) - - Text("\(friend.compatibility)%") - .font(.system(size: 14, weight: .semibold)) - .foregroundColor(compatibilityColor) - } - } - - HStack(spacing: 8) { - Button(action: {}) { - HStack { - Image(systemName: "square.and.arrow.up") - .font(.system(size: 14)) - - Text("Compare") - .font(.system(size: 14)) - } - .foregroundColor(AppColors.textSecondary) - .frame(maxWidth: .infinity) - .padding(.vertical, 10) - .background(AppColors.secondaryBackground) - .cornerRadius(AppStyles.cornerRadiusMedium) - } - - Button(action: {}) { - Image(systemName: "heart") - .font(.system(size: 16)) - .foregroundColor(AppColors.textSecondary) - .frame(width: 44, height: 44) - .background(AppColors.secondaryBackground) - .cornerRadius(AppStyles.cornerRadiusMedium) - } - } - - VStack(spacing: 4) { - GeometryReader { geometry in - ZStack(alignment: .leading) { - Rectangle() - .fill(AppColors.secondaryBackground) - .frame(height: 6) - .cornerRadius(3) - - Rectangle() - .fill( - compatibilityColor - ) - .frame(width: geometry.size.width * CGFloat(friend.compatibility) / 100, height: 6) - .cornerRadius(3) - } - } - .frame(height: 6) - - Text("Taste Match") - .font(.system(size: 10)) - .foregroundColor(AppColors.textSecondary) - .frame(maxWidth: .infinity, alignment: .center) - } - } - .padding(AppStyles.paddingMedium) - .background(AppColors.background) - .cornerRadius(AppStyles.cornerRadiusMedium) - } -} - -#Preview { - SocialView() -} diff --git a/frontend/MusicApp/Views/TasteProfileView.swift b/frontend/MusicApp/Views/TasteProfileView.swift deleted file mode 100644 index 7458c4b..0000000 --- a/frontend/MusicApp/Views/TasteProfileView.swift +++ /dev/null @@ -1,261 +0,0 @@ -import SwiftUI -import Charts - -struct TasteProfileView: View { - @StateObject private var viewModel = TasteProfileViewModel() - - var body: some View { - ZStack { - AppColors.background - .ignoresSafeArea() - - if viewModel.isLoading { - ProgressView() - .tint(AppColors.primary) - } else { - ScrollView { - VStack(spacing: 24) { - - VStack(alignment: .leading, spacing: 8) { - Text("Your Taste DNA") - .font(.system(size: 32, weight: .bold)) - .foregroundColor(AppColors.textPrimary) - - Text("Discover your unique music profile") - .font(.system(size: 14)) - .foregroundColor(AppColors.textSecondary) - } - .frame(maxWidth: .infinity, alignment: .leading) - .padding(.horizontal, AppStyles.paddingMedium) - .padding(.top, AppStyles.paddingLarge) - - HStack(spacing: 12) { - StatCard( - icon: "sparkles", - value: "\(viewModel.tasteScore)", - label: "Taste Score", - gradient: AppColors.primary - ) - - StatCard( - icon: "trophy.fill", - value: "\(viewModel.totalRatings)", - label: "Ratings", - gradient: AppColors.secondary - ) - - StatCard( - icon: "target", - value: "\(viewModel.influence.formatted())", - label: "Influence", - gradient: AppColors.accent - ) - } - .padding(.horizontal, AppStyles.paddingMedium) - - VStack(alignment: .leading, spacing: 16) { - HStack(spacing: 8) { - Image(systemName: "music.note") - .font(.system(size: 20)) - .foregroundColor(AppColors.primary) - - Text("Genre Affinity") - .font(.system(size: 18, weight: .semibold)) - .foregroundColor(AppColors.textPrimary) - } - - Chart(viewModel.genreData) { item in - BarMark( - x: .value("Value", item.value), - y: .value("Genre", item.name) - ) - .foregroundStyle( - item.name == "Hip-Hop" || item.name == "Pop" || item.name == "Electronic" ? - AppColors.primary : - AppColors.secondary - ) - .cornerRadius(4) - } - .frame(height: 200) - .chartXAxis(.hidden) - .chartYAxis { - AxisMarks { _ in - AxisValueLabel() - .foregroundStyle(AppColors.textSecondary) - .font(.system(size: 12)) - } - } - } - .padding(AppStyles.paddingMedium) - .cardStyle() - .padding(.horizontal, AppStyles.paddingMedium) - - VStack(alignment: .leading, spacing: 16) { - Text("Decade Preference") - .font(.system(size: 18, weight: .semibold)) - .foregroundColor(AppColors.textPrimary) - - ForEach(viewModel.decadeData) { decade in - VStack(alignment: .leading, spacing: 4) { - HStack { - Text(decade.decade) - .font(.system(size: 14)) - .foregroundColor(AppColors.textSecondary) - - Spacer() - - Text("\(decade.value)%") - .font(.system(size: 14, weight: .medium)) - .foregroundColor(AppColors.textPrimary) - } - - GeometryReader { geometry in - ZStack(alignment: .leading) { - Rectangle() - .fill(AppColors.secondaryBackground) - .frame(height: 8) - .cornerRadius(4) - - Rectangle() - .fill(AppColors.primary) - .frame(width: geometry.size.width * CGFloat(decade.value) / 100, height: 8) - .cornerRadius(4) - } - } - .frame(height: 8) - } - } - } - .padding(AppStyles.paddingMedium) - .cardStyle() - .padding(.horizontal, AppStyles.paddingMedium) - - VStack(alignment: .leading, spacing: 16) { - Text("Music Attributes") - .font(.system(size: 18, weight: .semibold)) - .foregroundColor(AppColors.textPrimary) - - ForEach(viewModel.radarData) { item in - VStack(alignment: .leading, spacing: 4) { - HStack { - Text(item.category) - .font(.system(size: 14)) - .foregroundColor(AppColors.textSecondary) - - Spacer() - - Text("\(item.value)") - .font(.system(size: 14, weight: .medium)) - .foregroundColor(AppColors.textPrimary) - } - - GeometryReader { geometry in - ZStack(alignment: .leading) { - Rectangle() - .fill(AppColors.secondaryBackground) - .frame(height: 6) - .cornerRadius(3) - - Rectangle() - .fill(AppColors.primary) - .frame(width: geometry.size.width * CGFloat(item.value) / 100, height: 6) - .cornerRadius(3) - } - } - .frame(height: 6) - } - } - } - .padding(AppStyles.paddingMedium) - .cardStyle() - .padding(.horizontal, AppStyles.paddingMedium) - - VStack(alignment: .leading, spacing: 16) { - Text("Controversy Affinity") - .font(.system(size: 18, weight: .semibold)) - .foregroundColor(AppColors.textPrimary) - - ZStack { - Circle() - .stroke(AppColors.secondaryBackground, lineWidth: 16) - .frame(width: 192, height: 192) - - Circle() - .trim(from: 0, to: CGFloat(viewModel.controversyAffinity) / 100) - .stroke( - AppColors.primary, - style: StrokeStyle(lineWidth: 16, lineCap: .round) - ) - .frame(width: 192, height: 192) - .rotationEffect(.degrees(-90)) - - VStack(spacing: 4) { - Text("\(viewModel.controversyAffinity)%") - .font(.system(size: 36, weight: .bold)) - .foregroundColor(AppColors.textPrimary) - - Text("Bold") - .font(.system(size: 14)) - .foregroundColor(AppColors.textSecondary) - } - } - .frame(maxWidth: .infinity) - .padding(.vertical, 16) - - HStack { - Text("Safe") - .font(.system(size: 12)) - .foregroundColor(AppColors.textSecondary) - - Spacer() - - Text("Controversial") - .font(.system(size: 12)) - .foregroundColor(AppColors.textSecondary) - } - } - .padding(AppStyles.paddingMedium) - .cardStyle() - .padding(.horizontal, AppStyles.paddingMedium) - .padding(.bottom, 100) - } - } - } - } - .task { - await viewModel.loadProfile() - } - } -} - -struct StatCard: View { - let icon: String - let value: String - let label: String - let gradient: Color - - var body: some View { - VStack(alignment: .leading, spacing: 8) { - Image(systemName: icon) - .font(.system(size: 20)) - .foregroundColor(.white.opacity(0.8)) - - Text(value) - .font(.system(size: 24, weight: .bold)) - .foregroundColor(.white) - - Text(label) - .font(.system(size: 10)) - .foregroundColor(.white.opacity(0.7)) - } - .frame(maxWidth: .infinity, alignment: .leading) - .padding(AppStyles.paddingMedium) - .background(gradient) - .foregroundColor(.white) - .cornerRadius(AppStyles.cornerRadiusMedium) - } -} - -#Preview { - TasteProfileView() -}