import SwiftUI import Charts struct AnalyticsView: View { @EnvironmentObject var linkViewModel: LinkViewModel @State private var selectedTimeRange: TimeRange = .oneMonth @State private var selectedMetric: Metric = .clicks @State private var showingExport = false enum TimeRange: String, CaseIterable { case oneWeek = "1w" case oneMonth = "1m" case threeMonths = "3m" case sixMonths = "6m" case oneYear = "1y" case allTime = "all" var displayName: String { switch self { case .oneWeek: return NSLocalizedString("1_week", comment: "1 week") case .oneMonth: return NSLocalizedString("1_month", comment: "1 month") case .threeMonths: return NSLocalizedString("3_months", comment: "3 months") case .sixMonths: return NSLocalizedString("6_months", comment: "6 months") case .oneYear: return NSLocalizedString("1_year", comment: "1 year") case .allTime: return NSLocalizedString("all_time", comment: "All time") } } var dateRange: DateInterval? { let calendar = Calendar.current let endDate = Date() switch self { case .oneWeek: let startDate = calendar.date(byAdding: .weekOfYear, value: -1, to: endDate)! return DateInterval(start: startDate, end: endDate) case .oneMonth: let startDate = calendar.date(byAdding: .month, value: -1, to: endDate)! return DateInterval(start: startDate, end: endDate) case .threeMonths: let startDate = calendar.date(byAdding: .month, value: -3, to: endDate)! return DateInterval(start: startDate, end: endDate) case .sixMonths: let startDate = calendar.date(byAdding: .month, value: -6, to: endDate)! return DateInterval(start: startDate, end: endDate) case .oneYear: let startDate = calendar.date(byAdding: .year, value: -1, to: endDate)! return DateInterval(start: startDate, end: endDate) case .allTime: return nil } } } enum Metric: String, CaseIterable { case clicks = "clicks" case links = "links" case topLinks = "top_links" var displayName: String { switch self { case .clicks: return NSLocalizedString("clicks_over_time", comment: "Clicks over time") case .links: return NSLocalizedString("links_created", comment: "Links created") case .topLinks: return NSLocalizedString("top_links", comment: "Top links") } } } var analyticsData: AnalyticsData { return AnalyticsCalculator.calculateAnalytics( for: linkViewModel.links, timeRange: selectedTimeRange, metric: selectedMetric ) } var body: some View { NavigationView { ScrollView { VStack(alignment: .leading, spacing: 20) { timeRangePicker overviewCards metricSelector mainChart topLinksSection linkTypeDistributionSection recentActivitySection } .padding() } .navigationTitle("analytics") .navigationBarTitleDisplayMode(.large) .toolbar { ToolbarItem(placement: .navigationBarTrailing) { analyticsToolbar } } } .sheet(isPresented: $showingExport) { ExportView(analyticsData: analyticsData) } } private var timeRangePicker: some View { VStack(alignment: .leading) { Text("time_range") .font(.headline) Picker("time_range", selection: $selectedTimeRange) { ForEach(TimeRange.allCases, id: \.self) { range in Text(range.displayName).tag(range) } } .pickerStyle(SegmentedPickerStyle()) } } private var overviewCards: some View { LazyVGrid(columns: [ GridItem(.flexible()), GridItem(.flexible()) ], spacing: 16) { OverviewCard( title: "total_links", value: "\(analyticsData.totalLinks)", change: analyticsData.linksChange, icon: "link" ) OverviewCard( title: "total_clicks", value: "\(analyticsData.totalClicks)", change: analyticsData.clicksChange, icon: "hand.tap" ) OverviewCard( title: "avg_clicks_per_link", value: String(format: "%.1f", analyticsData.averageClicksPerLink), change: analyticsData.avgClicksChange, icon: "chart.bar" ) OverviewCard( title: "active_links", value: "\(analyticsData.activeLinks)", change: analyticsData.activeLinksChange, icon: "bolt" ) } } private var metricSelector: some View { VStack(alignment: .leading) { Text("metric") .font(.headline) Picker("metric", selection: $selectedMetric) { ForEach(Metric.allCases, id: \.self) { metric in Text(metric.displayName).tag(metric) } } .pickerStyle(SegmentedPickerStyle()) } } private var mainChart: some View { VStack(alignment: .leading) { Text(selectedMetric.displayName) .font(.headline) if analyticsData.chartData.isEmpty { Text("no_data_available") .font(.body) .foregroundColor(.secondary) .frame(height: 200) .frame(maxWidth: .infinity) .background(Color(.systemGray6)) .cornerRadius(12) } else { Chart(analyticsData.chartData) { dataPoint in switch selectedMetric { case .clicks: LineMark( x: .value("Date", dataPoint.date), y: .value("Clicks", dataPoint.count) ) .foregroundStyle(.blue) case .links: BarMark( x: .value("Date", dataPoint.date), y: .value("Links", dataPoint.count) ) .foregroundStyle(.green) case .topLinks: BarMark( x: .value("Link", dataPoint.label), y: .value("Clicks", dataPoint.count) ) .foregroundStyle(.orange) } } .frame(height: 200) .chartXAxisLabel(selectedMetric == .topLinks ? "Links" : "Date") .chartYAxisLabel(selectedMetric == .links ? "Links Created" : "Clicks") } } } private var topLinksSection: some View { VStack(alignment: .leading) { Text("top_performing_links") .font(.headline) ForEach(analyticsData.topLinks, id: \.link.id) { linkData in TopLinkRow(linkData: linkData) } } } private var linkTypeDistributionSection: some View { VStack(alignment: .leading) { Text("link_type_distribution") .font(.headline) if !analyticsData.linkTypeDistribution.isEmpty { Chart(analyticsData.linkTypeDistribution, id: \.type) { data in SectorMark( angle: .value("Count", data.count), innerRadius: .ratio(0.5), angularInset: 2 ) .foregroundStyle(linkTypeColor(data.type)) } .frame(height: 200) } LazyVGrid(columns: [ GridItem(.flexible()), GridItem(.flexible()), GridItem(.flexible()) ], spacing: 12) { ForEach(analyticsData.linkTypeDistribution, id: \.type) { data in VStack { Text(data.type.displayName) .font(.caption) .foregroundColor(.secondary) Text("\(data.count)") .font(.headline) .foregroundColor(linkTypeColor(data.type)) } .frame(maxWidth: .infinity) .padding() .background(Color(.systemGray6)) .cornerRadius(8) } } } } private var recentActivitySection: some View { VStack(alignment: .leading) { Text("recent_activity") .font(.headline) ForEach(analyticsData.recentActivity.prefix(10), id: \.id) { activity in RecentActivityRow(activity: activity) } } } private var analyticsToolbar: some View { Menu { Button("export_data") { showingExport = true } Button("refresh_data") { linkViewModel.fetchLinks() } } label: { Image(systemName: "ellipsis.circle") } } private func linkTypeColor(_ linkType: LinkType) -> Color { switch linkType { case .link: return .blue case .custom: return .green case .template: return .orange } } } struct OverviewCard: View { let title: String let value: String let change: Double? let icon: String var body: some View { VStack(alignment: .leading, spacing: 8) { HStack { Image(systemName: icon) .foregroundColor(.blue) Text(title) .font(.caption) .foregroundColor(.secondary) Spacer() } Text(value) .font(.title2) .fontWeight(.semibold) if let change = change { HStack { Image(systemName: change >= 0 ? "arrow.up.right" : "arrow.down.right") .font(.caption2) Text(String(format: "%.1f%%", abs(change))) .font(.caption2) } .foregroundColor(change >= 0 ? .green : .red) } } .padding() .frame(maxWidth: .infinity, alignment: .leading) .background(Color(.systemGray6)) .cornerRadius(12) } } struct TopLinkRow: View { let linkData: TopLinkData var body: some View { HStack { VStack(alignment: .leading) { Text(linkData.link.alias) .font(.body) .fontWeight(.medium) if let url = linkData.link.originalURL { Text(url) .font(.caption) .foregroundColor(.secondary) .lineLimit(1) } } Spacer() VStack(alignment: .trailing) { Text("\(linkData.clicks)") .font(.headline) .foregroundColor(.blue) Text("clicks") .font(.caption) .foregroundColor(.secondary) } } .padding() .background(Color(.systemGray6)) .cornerRadius(8) } } struct RecentActivityRow: View { let activity: RecentActivity var body: some View { HStack { Image(systemName: activity.type.icon) .foregroundColor(activity.type.color) .frame(width: 24) VStack(alignment: .leading) { Text(activity.description) .font(.body) Text(activity.timestamp, format: .relative(presentation: .named)) .font(.caption) .foregroundColor(.secondary) } Spacer() } .padding(.vertical, 4) } } struct ExportView: View { let analyticsData: AnalyticsData @Environment(\.presentationMode) var presentationMode @State private var exportFormat: ExportFormat = .json @State private var includeChartData = true @State private var includeTopLinks = true @State private var isExporting = false enum ExportFormat: String, CaseIterable { case json = "JSON" case csv = "CSV" var displayName: String { return rawValue } } var body: some View { NavigationView { Form { Section("export_options") { Picker("format", selection: $exportFormat) { ForEach(ExportFormat.allCases, id: \.self) { format in Text(format.displayName).tag(format) } } Toggle("include_chart_data", isOn: $includeChartData) Toggle("include_top_links", isOn: $includeTopLinks) } Section("preview") { Text("total_links: \(analyticsData.totalLinks)") Text("total_clicks: \(analyticsData.totalClicks)") Text("chart_data_points: \(analyticsData.chartData.count)") Text("top_links: \(analyticsData.topLinks.count)") } } .navigationTitle("export_analytics") .navigationBarTitleDisplayMode(.inline) .toolbar { ToolbarItem(placement: .navigationBarLeading) { Button("cancel") { presentationMode.wrappedValue.dismiss() } } ToolbarItem(placement: .navigationBarTrailing) { Button("export") { exportData() } .disabled(isExporting) } } } } private func exportData() { isExporting = true // Implementation would depend on the specific export requirements // For now, we'll just simulate the export DispatchQueue.main.asyncAfter(deadline: .now() + 1) { isExporting = false presentationMode.wrappedValue.dismiss() } } } #Preview { AnalyticsView() .environmentObject(LinkViewModel()) }