Files
PeachDeployments/lib/Peach/designs/InsightsCard.dart
alvocool 38881d1fe1 Add app source and tests
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-11 10:15:30 +03:00

153 lines
4.6 KiB
Dart

import 'package:flutter/material.dart';
import '../about/internal/application/TextType.dart';
import '../about/internal/application/ServerProfile.dart';
import '../about/internal/application/DeploymentRecord.dart';
import '../utils/Colors.dart';
import 'text/Text.dart';
/// Warm gradient "insights" card that surfaces computed tips from the data.
class InsightsCard extends StatelessWidget {
final List<ServerProfile> servers;
final List<DeploymentRecord> history;
final VoidCallback onViewHistory;
const InsightsCard({
super.key,
required this.servers,
required this.history,
required this.onViewHistory,
});
List<(IconData, String)> _tips() {
final tips = <(IconData, String)>[];
final failed = history
.where((h) => h.status == DeployStatus.failure)
.length;
final totalServices = servers.fold(0, (n, s) => n + s.services.length);
if (history.isEmpty) {
tips.add((
Icons.rocket_launch,
'Add a service and run your first deploy to unlock insights.',
));
}
if (failed > 0) {
tips.add((
Icons.warning_amber,
'$failed failed ${failed == 1 ? 'deploy' : 'deploys'} — open the service logs to investigate.',
));
}
// Busiest server
if (history.isNotEmpty) {
final counts = <String, int>{};
for (final h in history) {
counts[h.serverName] = (counts[h.serverName] ?? 0) + 1;
}
final busiest = counts.entries.reduce(
(a, b) => a.value >= b.value ? a : b,
);
tips.add((
Icons.trending_up,
'${busiest.key} is your busiest server (${busiest.value} deploys).',
));
}
final emptyServers = servers.where((s) => s.services.isEmpty).length;
if (emptyServers > 0) {
tips.add((
Icons.dns,
'$emptyServers ${emptyServers == 1 ? 'server has' : 'servers have'} no services yet — add one to start deploying.',
));
}
if (totalServices > 0) {
tips.add((
Icons.health_and_safety,
'Open Health to check the live status of your $totalServices ${totalServices == 1 ? 'service' : 'services'}.',
));
}
return tips.take(3).toList();
}
@override
Widget build(BuildContext context) {
const ink = PeachTheme.ink;
final tips = _tips();
return Container(
padding: const EdgeInsets.all(18),
decoration: BoxDecoration(
gradient: PeachTheme.peachGradient,
borderRadius: BorderRadius.circular(20),
boxShadow: PeachTheme.glow(PeachTheme.seed, opacity: 0.18, blur: 30),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
const Icon(Icons.auto_awesome, size: 18, color: ink),
const SizedBox(width: 8),
text(
'Insights',
16,
TextType.Bold,
color: ink,
weight: FontWeight.w800,
),
],
),
const SizedBox(height: 14),
for (final (icon, tip) in tips) ...[
_tip(icon, tip),
const SizedBox(height: 8),
],
const SizedBox(height: 4),
Align(
alignment: Alignment.centerRight,
child: FilledButton.icon(
onPressed: onViewHistory,
icon: const Icon(Icons.timeline, size: 16),
label: const Text('View history'),
style: FilledButton.styleFrom(
backgroundColor: ink,
foregroundColor: Colors.white,
elevation: 0,
padding: const EdgeInsets.symmetric(
horizontal: 16,
vertical: 12,
),
),
),
),
],
),
);
}
Widget _tip(IconData icon, String tip) {
return Container(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 11),
decoration: BoxDecoration(
color: Colors.white.withValues(alpha: 0.55),
borderRadius: BorderRadius.circular(12),
),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Icon(icon, size: 16, color: PeachTheme.ink.withValues(alpha: 0.7)),
const SizedBox(width: 10),
Expanded(
child: text(
tip,
12.5,
TextType.Regular,
color: PeachTheme.ink.withValues(alpha: 0.85),
height: 1.35,
weight: FontWeight.w600,
),
),
],
),
);
}
}