sojorn/sojorn_app/lib/widgets/harmony_explainer_modal.dart

334 lines
12 KiB
Dart
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import 'package:flutter/material.dart';
import '../models/trust_state.dart';
import '../models/trust_tier.dart';
import '../theme/app_theme.dart';
import '../theme/tokens.dart';
/// Modal that explains the Harmony State system.
/// Shows current level, progression chart, and tips.
class HarmonyExplainerModal extends StatelessWidget {
final TrustState trustState;
const HarmonyExplainerModal({super.key, required this.trustState});
static void show(BuildContext context, TrustState trustState) {
showModalBottomSheet(
context: context,
backgroundColor: Colors.transparent,
isScrollControlled: true,
builder: (_) => HarmonyExplainerModal(trustState: trustState),
);
}
@override
Widget build(BuildContext context) {
return DraggableScrollableSheet(
initialChildSize: 0.75,
maxChildSize: 0.92,
minChildSize: 0.5,
builder: (_, controller) => Container(
decoration: BoxDecoration(
color: AppTheme.cardSurface,
borderRadius: const BorderRadius.vertical(top: Radius.circular(24)),
),
child: ListView(
controller: controller,
padding: const EdgeInsets.fromLTRB(24, 12, 24, 32),
children: [
// Handle
Center(child: Container(
width: 40, height: 4,
decoration: BoxDecoration(
color: AppTheme.navyBlue.withValues(alpha: 0.15),
borderRadius: BorderRadius.circular(2),
),
)),
const SizedBox(height: 20),
// Title
Text('What is Harmony State?', style: TextStyle(
fontSize: 20, fontWeight: FontWeight.w800, color: AppTheme.navyBlue,
)),
const SizedBox(height: 10),
Text(
'Your Harmony State is your community contribution score. It affects your reach multiplier — how far your posts travel.',
style: TextStyle(fontSize: 14, color: SojornColors.postContentLight, height: 1.5),
),
const SizedBox(height: 24),
// Current state card
_CurrentStateCard(trustState: trustState),
const SizedBox(height: 24),
// Progression chart
Text('Progression', style: TextStyle(
fontSize: 16, fontWeight: FontWeight.w700, color: AppTheme.navyBlue,
)),
const SizedBox(height: 12),
_ProgressionChart(currentTier: trustState.tier),
const SizedBox(height: 24),
// How to increase
Text('How to Increase Harmony', style: TextStyle(
fontSize: 16, fontWeight: FontWeight.w700, color: AppTheme.navyBlue,
)),
const SizedBox(height: 12),
_TipRow(icon: Icons.check_circle, color: const Color(0xFF4CAF50),
text: 'Post helpful beacons that get upvoted'),
_TipRow(icon: Icons.check_circle, color: const Color(0xFF4CAF50),
text: 'Create posts that receive positive engagement'),
_TipRow(icon: Icons.check_circle, color: const Color(0xFF4CAF50),
text: 'Participate in chains constructively'),
_TipRow(icon: Icons.check_circle, color: const Color(0xFF4CAF50),
text: 'Join and contribute to groups'),
const SizedBox(height: 16),
// What decreases
Text('What Decreases Harmony', style: TextStyle(
fontSize: 16, fontWeight: FontWeight.w700, color: AppTheme.navyBlue,
)),
const SizedBox(height: 12),
_TipRow(icon: Icons.cancel, color: SojornColors.destructive,
text: 'Spam or inappropriate content'),
_TipRow(icon: Icons.cancel, color: SojornColors.destructive,
text: 'Beacons that get downvoted as false'),
_TipRow(icon: Icons.cancel, color: SojornColors.destructive,
text: 'Repeated community guideline violations'),
],
),
),
);
}
}
class _CurrentStateCard extends StatelessWidget {
final TrustState trustState;
const _CurrentStateCard({required this.trustState});
@override
Widget build(BuildContext context) {
final tier = trustState.tier;
final score = trustState.harmonyScore;
final multiplier = _multiplierForTier(tier);
final nextTier = _nextTier(tier);
final nextThreshold = _thresholdForTier(nextTier);
return Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
gradient: LinearGradient(
colors: [
AppTheme.navyBlue.withValues(alpha: 0.06),
AppTheme.brightNavy.withValues(alpha: 0.04),
],
),
borderRadius: BorderRadius.circular(16),
border: Border.all(color: AppTheme.navyBlue.withValues(alpha: 0.1)),
),
child: Column(
children: [
Row(
children: [
Container(
width: 48, height: 48,
decoration: BoxDecoration(
color: _colorForTier(tier).withValues(alpha: 0.15),
shape: BoxShape.circle,
),
child: Icon(Icons.auto_graph, color: _colorForTier(tier), size: 24),
),
const SizedBox(width: 14),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text('Current: ${tier.displayName}', style: TextStyle(
fontSize: 16, fontWeight: FontWeight.w700, color: AppTheme.navyBlue,
)),
Text('Score: $score', style: TextStyle(
fontSize: 13, color: SojornColors.textDisabled,
)),
],
),
),
Container(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
decoration: BoxDecoration(
color: _colorForTier(tier).withValues(alpha: 0.12),
borderRadius: BorderRadius.circular(20),
),
child: Text('${multiplier}x reach', style: TextStyle(
fontSize: 13, fontWeight: FontWeight.w700, color: _colorForTier(tier),
)),
),
],
),
if (nextTier != null) ...[
const SizedBox(height: 14),
// Progress bar to next tier
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text('Next: ${nextTier.displayName}', style: TextStyle(
fontSize: 12, fontWeight: FontWeight.w600, color: SojornColors.textDisabled,
)),
Text('$score / $nextThreshold', style: TextStyle(
fontSize: 12, color: SojornColors.textDisabled,
)),
],
),
const SizedBox(height: 6),
ClipRRect(
borderRadius: BorderRadius.circular(4),
child: LinearProgressIndicator(
value: (score / nextThreshold).clamp(0.0, 1.0),
backgroundColor: AppTheme.navyBlue.withValues(alpha: 0.08),
valueColor: AlwaysStoppedAnimation(_colorForTier(tier)),
minHeight: 6,
),
),
],
),
],
],
),
);
}
String _multiplierForTier(TrustTier tier) {
switch (tier) {
case TrustTier.new_user: return '1.0';
case TrustTier.established: return '1.5';
case TrustTier.trusted: return '2.0';
}
}
TrustTier? _nextTier(TrustTier tier) {
switch (tier) {
case TrustTier.new_user: return TrustTier.established;
case TrustTier.established: return TrustTier.trusted;
case TrustTier.trusted: return null;
}
}
int _thresholdForTier(TrustTier? tier) {
switch (tier) {
case TrustTier.established: return 100;
case TrustTier.trusted: return 500;
default: return 100;
}
}
Color _colorForTier(TrustTier tier) {
switch (tier) {
case TrustTier.new_user: return AppTheme.egyptianBlue;
case TrustTier.established: return AppTheme.royalPurple;
case TrustTier.trusted: return const Color(0xFF4CAF50);
}
}
}
class _ProgressionChart extends StatelessWidget {
final TrustTier currentTier;
const _ProgressionChart({required this.currentTier});
@override
Widget build(BuildContext context) {
return Container(
padding: const EdgeInsets.all(14),
decoration: BoxDecoration(
color: AppTheme.navyBlue.withValues(alpha: 0.03),
borderRadius: BorderRadius.circular(14),
border: Border.all(color: AppTheme.navyBlue.withValues(alpha: 0.06)),
),
child: Column(
children: [
_LevelRow(label: 'New', range: '0100', multiplier: '1.0x',
color: AppTheme.egyptianBlue, isActive: currentTier == TrustTier.new_user),
const SizedBox(height: 10),
_LevelRow(label: 'Established', range: '100500', multiplier: '1.5x',
color: AppTheme.royalPurple, isActive: currentTier == TrustTier.established),
const SizedBox(height: 10),
_LevelRow(label: 'Trusted', range: '500+', multiplier: '2.0x',
color: const Color(0xFF4CAF50), isActive: currentTier == TrustTier.trusted),
],
),
);
}
}
class _LevelRow extends StatelessWidget {
final String label;
final String range;
final String multiplier;
final Color color;
final bool isActive;
const _LevelRow({
required this.label, required this.range,
required this.multiplier, required this.color, required this.isActive,
});
@override
Widget build(BuildContext context) {
return Row(
children: [
Container(
width: 12, height: 12,
decoration: BoxDecoration(
color: isActive ? color : color.withValues(alpha: 0.2),
shape: BoxShape.circle,
border: isActive ? Border.all(color: color, width: 2) : null,
),
),
const SizedBox(width: 12),
Expanded(child: Text(label, style: TextStyle(
fontSize: 14, fontWeight: isActive ? FontWeight.w700 : FontWeight.w500,
color: isActive ? AppTheme.navyBlue : SojornColors.textDisabled,
))),
Text(range, style: TextStyle(fontSize: 12, color: SojornColors.textDisabled)),
const SizedBox(width: 14),
Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 3),
decoration: BoxDecoration(
color: isActive ? color.withValues(alpha: 0.12) : AppTheme.navyBlue.withValues(alpha: 0.04),
borderRadius: BorderRadius.circular(8),
),
child: Text(multiplier, style: TextStyle(
fontSize: 12, fontWeight: FontWeight.w700,
color: isActive ? color : SojornColors.textDisabled,
)),
),
],
);
}
}
class _TipRow extends StatelessWidget {
final IconData icon;
final Color color;
final String text;
const _TipRow({required this.icon, required this.color, required this.text});
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.only(bottom: 8),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Icon(icon, size: 18, color: color),
const SizedBox(width: 10),
Expanded(child: Text(text, style: TextStyle(
fontSize: 13, color: SojornColors.postContentLight, height: 1.4,
))),
],
),
);
}
}