Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion .claude/settings.local.json
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,10 @@
"mcp__Windows-MCP__Snapshot",
"mcp__Claude_in_Chrome__find",
"Bash(dir \"G:\\\\GitHub\\\\petfolio\\\\google_fonts\" /b)",
"mcp__5d4f3e29-8e00-4c0a-a2e0-3f9d9a2735f2__list_projects"
"mcp__5d4f3e29-8e00-4c0a-a2e0-3f9d9a2735f2__list_projects",
"mcp__5d4f3e29-8e00-4c0a-a2e0-3f9d9a2735f2__deploy_edge_function",
"Bash(Remove-Item \"G:\\\\GitHub\\\\petfolio\\\\lib\\\\features\\\\care\\\\data\\\\repositories\\\\care_repository.dart\")",
"PowerShell(Remove-Item \"G:\\\\GitHub\\\\petfolio\\\\lib\\\\features\\\\care\\\\data\\\\repositories\\\\care_repository.dart\" -Confirm:$false)"
]
}
}
156 changes: 89 additions & 67 deletions lib/core/router.dart
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import 'package:google_fonts/google_fonts.dart';
import 'theme/app_colors.dart';

import '../features/auth/presentation/controllers/auth_controller.dart';
import '../features/auth/presentation/screens/email_verification_screen.dart';
import '../features/auth/presentation/screens/login_screen.dart';
import '../features/auth/presentation/screens/registration_screen.dart';
import '../features/care/presentation/screens/care_screen.dart';
Expand Down Expand Up @@ -102,6 +103,12 @@ final routerProvider = Provider<GoRouter>((ref) {
path: '/register',
builder: (context, state) => const RegistrationScreen(),
),
GoRoute(
path: '/verify-email',
builder: (context, state) => EmailVerificationScreen(
email: state.uri.queryParameters['email'] ?? '',
),
),
Comment on lines +106 to +111
GoRoute(
path: '/onboarding',
builder: (context, state) {
Expand Down Expand Up @@ -165,14 +172,6 @@ final routerProvider = Provider<GoRouter>((ref) {
path: '/profile/orders',
builder: (context, state) => const BuyerOrderListScreen(),
),
GoRoute(
parentNavigatorKey: _rootNavigatorKey,
path: '/profile/orders/:id',
builder: (context, state) => BuyerOrderDetailScreen(
orderId: state.pathParameters['id']!,
order: state.extra as MarketplaceOrder?,
),
),
GoRoute(
parentNavigatorKey: _rootNavigatorKey,
path: '/shop/:id',
Expand Down Expand Up @@ -339,6 +338,9 @@ class _RouterNotifier extends ChangeNotifier {
}
notifyListeners();
});
// Also re-evaluate when emailConfirmedAt changes (e.g. user confirms email
// while the app is open) without a sign-in/sign-out transition.
_ref.listen<bool>(isEmailVerifiedProvider, (_, _) => notifyListeners());
_ref.listen(petListProvider, (_, _) => notifyListeners());
}

Expand All @@ -348,13 +350,24 @@ class _RouterNotifier extends ChangeNotifier {
final isLoggedIn = _ref.read(isLoggedInProvider);
final loc = state.matchedLocation;

// ── Not logged in → only /login and /register are allowed ────────
// ── Not logged in → only public auth screens are allowed ────────
if (!isLoggedIn) {
return (loc == '/login' || loc == '/register') ? null : '/login';
final isPublic = loc == '/login' ||
loc == '/register' ||
loc == '/verify-email';
return isPublic ? null : '/login';
}

// ── Logged in on an auth screen → leave ─────────────────────────
if (loc == '/login' || loc == '/register') return '/home';
// ── Logged in but email not yet confirmed → gate to /verify-email ─
final isVerified = _ref.read(isEmailVerifiedProvider);
if (!isVerified) {
return loc == '/verify-email' ? null : '/verify-email';
}

// ── Logged in & verified — redirect away from auth screens ────────
if (loc == '/login' || loc == '/register' || loc == '/verify-email') {
return '/home';
}

// ── Logged in but no pets → go to /onboarding ───────────────────
// Only redirect when the pet list has finished loading AND is empty,
Expand Down Expand Up @@ -399,11 +412,11 @@ class AppShell extends StatelessWidget {
final Widget child;

static const _destinations = [
_NavDestination(icon: Icons.pets_outlined, activeIcon: Icons.pets, label: 'Pets', path: '/home'),
_NavDestination(icon: Icons.local_fire_department_outlined, activeIcon: Icons.local_fire_department, label: 'Care', path: '/care'),
_NavDestination(icon: Icons.favorite_border, activeIcon: Icons.favorite, label: 'Social', path: '/social'),
_NavDestination(icon: Icons.auto_awesome_outlined, activeIcon: Icons.auto_awesome, label: 'Match', path: '/matching'),
_NavDestination(icon: Icons.storefront_outlined, activeIcon: Icons.storefront, label: 'Market', path: '/marketplace'),
_NavDestination(icon: Icons.pets_outlined, activeIcon: Icons.pets, label: 'Pets', path: '/home', color: AppColors.tangerine),
_NavDestination(icon: Icons.local_fire_department_outlined, activeIcon: Icons.local_fire_department, label: 'Care', path: '/care', color: AppColors.sunny),
_NavDestination(icon: Icons.favorite_border, activeIcon: Icons.favorite, label: 'Social', path: '/social', color: AppColors.poppy),
_NavDestination(icon: Icons.auto_awesome_outlined, activeIcon: Icons.auto_awesome, label: 'Match', path: '/matching', color: AppColors.lilac),
_NavDestination(icon: Icons.storefront_outlined, activeIcon: Icons.storefront, label: 'Market', path: '/marketplace', color: AppColors.mint),
];

int _selectedIndex(BuildContext context) {
Expand Down Expand Up @@ -477,7 +490,7 @@ class _PetEditMissingScreen extends StatelessWidget {
),
const SizedBox(height: 16),
FilledButton(
onPressed: () => context.go('/home'),
onPressed: () => context.go('/pets/manage'),
child: const Text('Back to Pets'),
),
],
Expand All @@ -493,23 +506,18 @@ class _NavDestination {
required this.activeIcon,
required this.label,
required this.path,
required this.color,
});

final IconData icon;
final IconData activeIcon;
final String label;
final String path;
/// Tab accent color — must stay co-located with its destination so a reorder
/// never silently mismatches color and route.
final Color color;
}

// ─── Tab accent colors (matches design system pillar colors) ─────────────────
const _tabColors = [
AppColors.tangerine, // Pets
AppColors.sunny, // Care
AppColors.poppy, // Social
AppColors.lilac, // Match
AppColors.mint, // Market
];

// ─── Floating pill bottom nav ─────────────────────────────────────────────────

class _FloatingNav extends StatelessWidget {
Expand Down Expand Up @@ -547,7 +555,6 @@ class _FloatingNav extends StatelessWidget {
child: _NavTab(
destination: destinations[i],
isSelected: i == selectedIndex,
accentColor: _tabColors[i],
isDark: isDark,
onTap: () => onSelect(i),
),
Expand All @@ -562,53 +569,59 @@ class _NavTab extends StatelessWidget {
const _NavTab({
required this.destination,
required this.isSelected,
required this.accentColor,
required this.isDark,
required this.onTap,
});

final _NavDestination destination;
final bool isSelected;
final Color accentColor;
final bool isDark;
final VoidCallback onTap;

@override
Widget build(BuildContext context) {
final unselectedColor = isDark ? AppColors.ink500D : AppColors.ink500;
final iconColor = isSelected ? accentColor : unselectedColor;
final softColor = Color.alphaBlend(accentColor.withAlpha(36), Colors.transparent);

return GestureDetector(
onTap: onTap,
behavior: HitTestBehavior.opaque,
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
AnimatedContainer(
duration: const Duration(milliseconds: 200),
padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 4),
decoration: BoxDecoration(
color: isSelected ? softColor : Colors.transparent,
borderRadius: BorderRadius.circular(999),
),
child: Icon(
isSelected ? destination.activeIcon : destination.icon,
color: iconColor,
size: 22,
final accentColor = destination.color;
final unselectedColor = isDark ? AppColors.ink500D : AppColors.ink700;
final iconColor = isSelected ? accentColor : unselectedColor;
final softColor = Color.alphaBlend(accentColor.withAlpha(36), Colors.transparent);

return Semantics(
label: destination.label,
selected: isSelected,
button: true,
child: InkWell(
onTap: onTap,
borderRadius: BorderRadius.circular(999),
splashColor: accentColor.withAlpha(30),
highlightColor: Colors.transparent,
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
AnimatedContainer(
duration: const Duration(milliseconds: 200),
padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 4),
decoration: BoxDecoration(
color: isSelected ? softColor : Colors.transparent,
borderRadius: BorderRadius.circular(999),
),
child: Icon(
isSelected ? destination.activeIcon : destination.icon,
color: iconColor,
size: 22,
),
),
),
const SizedBox(height: 2),
Text(
destination.label,
style: GoogleFonts.inter(
fontSize: 11,
fontWeight: isSelected ? FontWeight.w700 : FontWeight.w500,
color: iconColor,
height: 1.0,
const SizedBox(height: 2),
Text(
destination.label,
style: GoogleFonts.inter(
fontSize: 11,
fontWeight: isSelected ? FontWeight.w700 : FontWeight.w500,
color: iconColor,
height: 1.0,
),
),
),
],
],
),
),
);
}
Expand All @@ -635,21 +648,30 @@ class _WideNavRail extends StatelessWidget {
selectedIndex: selectedIndex,
labelType: NavigationRailLabelType.all,
backgroundColor: isDark ? AppColors.surface0D : AppColors.surface0,
indicatorColor: Colors.transparent,
onDestinationSelected: onSelect,
destinations: [
for (var i = 0; i < destinations.length; i++)
NavigationRailDestination(
padding: EdgeInsets.zero,
icon: Icon(destinations[i].icon,
color: selectedIndex == i ? _tabColors[i] : (isDark ? AppColors.ink500D : AppColors.ink500)),
selectedIcon: Icon(destinations[i].activeIcon, color: _tabColors[i]),
icon: Semantics(
label: destinations[i].label,
selected: selectedIndex == i,
child: Icon(
destinations[i].icon,
color: selectedIndex == i
? destinations[i].color
: (isDark ? AppColors.ink500D : AppColors.ink700),
),
),
selectedIcon: Icon(destinations[i].activeIcon, color: destinations[i].color),
label: Text(
destinations[i].label,
style: GoogleFonts.inter(
fontSize: 12,
fontWeight: selectedIndex == i ? FontWeight.w700 : FontWeight.w500,
color: selectedIndex == i ? _tabColors[i] : (isDark ? AppColors.ink500D : AppColors.ink500),
color: selectedIndex == i
? destinations[i].color
: (isDark ? AppColors.ink500D : AppColors.ink700),
),
),
),
Expand Down
35 changes: 0 additions & 35 deletions lib/core/theme/app_colors.dart
Original file line number Diff line number Diff line change
Expand Up @@ -110,39 +110,4 @@ abstract final class AppColors {
static const shadowE3D = Color(0x8C000000);
static const shadowE4D = Color(0xA6000000);
static const shadowGlassD = Color(0x8C000000);

// ── Backward-compat aliases (used by existing screens) ──────────────────────
static const blue50 = Color(0xFFFFE0CB); // → tangerineSoft
static const blue100 = tangerineSoft;
static const blue200 = tangerineSoft;
static const blue300 = tangerine;
static const blue400 = tangerine;
static const blue500 = tangerine;
static const blue600 = tangerine700;
static const blue700 = tangerine700;
static const blue100D = tangerineSoftD;
static const blue200D = tangerineSoftD;
static const blue300D = tangerineD;
static const blue400D = tangerineD;
static const blue500D = tangerineD;
static const blue600D = tangerine700D;
static const blue700D = tangerine700D;
static const blue800D = tangerine700D;
static const blue900D = tangerine700D;

static const sunset500 = tangerine;
static const sunset500D = tangerineD;
static const coral500 = poppy;
static const coral500D = poppyD;
static const meadow500 = mint;
static const meadow500D = mintD;
static const apricot500 = sunny;
static const apricot500D = sunnyD;
static const mulberry500 = lilac;
static const mulberry500D = lilacD;

static const line200 = line;
static const line200D = lineD;
static const line100 = line2;
static const line100D = line2D;
}
13 changes: 7 additions & 6 deletions lib/core/theme/app_theme.dart
Original file line number Diff line number Diff line change
Expand Up @@ -289,11 +289,12 @@ class PetfolioThemeExtension extends ThemeExtension<PetfolioThemeExtension> {
// ─────────────────────────────────────────────────────────────────────────────
final class AppThemeSpacing {
const AppThemeSpacing();
double get xs => 4;
double get sm => 8;
double get md => 12;
double get lg => 16;
double get xl => 24;
double get xs => 4;
double get sm => 8;
double get md => 12;
double get lg => 16;
double get xxl => 20;
double get xl => 24;
}

// ─────────────────────────────────────────────────────────────────────────────
Expand Down Expand Up @@ -517,7 +518,7 @@ abstract final class AppTheme {
static TextTheme _textTheme(bool isDark) {
final headColor = isDark ? AppColors.ink950D : AppColors.ink950;
final bodyColor = isDark ? AppColors.ink700D : AppColors.ink700;
final mutedColor = isDark ? AppColors.ink500D : AppColors.ink500;
final mutedColor = isDark ? AppColors.ink700D : AppColors.ink700;

// Bundled weights: Sora → w600 (SemiBold), w700 (Bold)
// Inter → w400 (Regular), w500 (Medium), w600 (SemiBold), w700 (Bold)
Expand Down
4 changes: 2 additions & 2 deletions lib/core/widgets/app_header.dart
Original file line number Diff line number Diff line change
Expand Up @@ -339,7 +339,7 @@ class _BadgePill extends StatelessWidget {
constraints: const BoxConstraints(minWidth: 16, minHeight: 16),
padding: const EdgeInsets.symmetric(horizontal: 4),
decoration: BoxDecoration(
color: AppColors.coral500,
color: AppColors.poppy,
borderRadius: BorderRadius.circular(999),
border: Border.all(color: pt.surface1, width: 1.5),
),
Expand All @@ -359,7 +359,7 @@ class _BadgePill extends StatelessWidget {
width: 9,
height: 9,
decoration: BoxDecoration(
color: AppColors.coral500,
color: AppColors.poppy,
shape: BoxShape.circle,
border: Border.all(color: pt.surface1, width: 1.5),
),
Expand Down
Loading
Loading