Add app source and tests
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
152
lib/Peach/designs/InsightsCard.dart
Normal file
152
lib/Peach/designs/InsightsCard.dart
Normal file
@@ -0,0 +1,152 @@
|
||||
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,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user