Skip to content
This repository was archived by the owner on Nov 18, 2025. It is now read-only.

Commit b75af36

Browse files
committed
Map painter v1
1 parent 9705c05 commit b75af36

File tree

4 files changed

+268
-53
lines changed

4 files changed

+268
-53
lines changed

rema_1001/CLAUDE.md

Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
# CLAUDE.md
2+
3+
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
4+
5+
## Project Overview
6+
7+
This is a Flutter application for Rema 1001 (Norwegian grocery store chain) with a companion backend. The app includes shopping lists, trips tracking, and store maps functionality.
8+
9+
**Tech Stack:**
10+
- Frontend: Flutter (Dart SDK ^3.9.2)
11+
- State Management: flutter_bloc ^9.1.1
12+
- Routing: go_router ^16.2.5
13+
- Backend: Located in sibling `../backend/` directory (Hono + Prisma + PostgreSQL)
14+
15+
## Common Commands
16+
17+
### Flutter (Frontend)
18+
19+
```bash
20+
# Run the app
21+
flutter run
22+
23+
# Run on specific device
24+
flutter run -d chrome
25+
flutter run -d macos
26+
flutter run -d ios
27+
28+
# Build for production
29+
flutter build apk
30+
flutter build ios
31+
flutter build web
32+
33+
# Run tests
34+
flutter test
35+
36+
# Analyze code
37+
flutter analyze
38+
39+
# Install/update dependencies
40+
flutter pub get
41+
flutter pub upgrade
42+
```
43+
44+
### Backend (in ../backend/)
45+
46+
The backend is a separate project in a sibling directory. Switch to `../backend/` to work with backend code.
47+
48+
## Architecture
49+
50+
### Navigation Architecture
51+
52+
The app uses **go_router** with a **ShellRoute** pattern that wraps all main screens with a persistent bottom navigation bar (`RoutedNavBar`).
53+
54+
- **Router configuration:** `lib/router/router.dart`
55+
- **Route names:** `lib/router/route_names.dart` (centralized route name constants)
56+
- **Custom transitions:** `lib/router/fade_transition_page.dart` (fade-in transitions with 150ms duration)
57+
- **Navigation bar:** `lib/router/nav_bar.dart` (custom bottom nav with 4 destinations, badge support)
58+
59+
The ShellRoute wraps these main routes:
60+
- `/home` - Home screen with quick access buttons
61+
- `/trips` - Shopping trips with map visualization
62+
- `/lists` - Shopping lists management
63+
- `/profile` - User profile
64+
65+
### Page Structure
66+
67+
All screens are located in `lib/page/`:
68+
- `home.dart` - HomeScreen (welcome screen with navigation buttons)
69+
- `map.dart` - TripsScreen (about page renamed, shows maps)
70+
- `about.dart` - ListsScreen (shopping lists)
71+
- `profile.dart` - ProfileScreen (user profile)
72+
73+
Note: There's a naming mismatch where `about.dart` contains `ListsScreen` and `map.dart` contains `TripsScreen` (shown as "about" in old git history).
74+
75+
### Custom Components
76+
77+
**FadeTransitionPage** (`lib/router/fade_transition_page.dart`):
78+
- Custom page transition wrapper for go_router
79+
- Uses opacity animation for smooth fade-in/fade-out
80+
- Default 150ms duration for both directions
81+
82+
**RoutedNavBar** (`lib/router/nav_bar.dart`):
83+
- Custom bottom navigation with 4 destinations (Norwegian labels: "Hjem", "Handleturer", "Dine lister", "Profil")
84+
- Dark theme (#2D2D2D background)
85+
- Badge support (e.g., "12" badge on home screen)
86+
- Active route detection using GoRouterState
87+
- 102px height with SafeArea
88+
89+
**Map Components** (`lib/map/`):
90+
- `map_painter.dart` - Custom painter for store map visualizations
91+
92+
### State Management
93+
94+
The project uses `flutter_bloc` for state management (version ^9.1.1) but BLoC classes are not yet implemented in the visible codebase. When adding new features with state:
95+
- Create BLoCs in appropriate feature folders
96+
- Follow BLoC pattern: Events, States, and BLoC classes
97+
- Use `equatable` (already included) for value equality in events/states
98+
99+
## Code Style
100+
101+
- Uses `flutter_lints` package for linting rules
102+
- Standard Flutter conventions apply
103+
- Lint configuration in `analysis_options.yaml`
104+
- Norwegian language used in UI labels (see nav_bar.dart)
105+
106+
## Project Context
107+
108+
Based on git history, this project includes:
109+
- Prisma backend with seeding scripts (in sibling backend directory)
110+
- PostgreSQL database in Docker
111+
- Scalar API documentation
112+
- Mock dataset for seeding

rema_1001/lib/map/map.dart

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
import 'package:flutter/widgets.dart';
2+
import 'package:equatable/equatable.dart';
3+
4+
final class Map extends Equatable {
5+
final List<Offset> walkPoints;
6+
final List<Aisle> aisles;
7+
8+
const Map({required this.walkPoints, required this.aisles});
9+
10+
@override
11+
List<Object?> get props => [walkPoints, aisles];
12+
}
13+
14+
enum AisleStatus { obstruction, active, highlighted }
15+
16+
final class Aisle extends Equatable {
17+
final Offset topLeft;
18+
final double width;
19+
final double height;
20+
final AisleStatus status;
21+
22+
const Aisle({
23+
required this.topLeft,
24+
required this.width,
25+
required this.height,
26+
this.status = AisleStatus.obstruction,
27+
});
28+
29+
@override
30+
List<Object?> get props => [topLeft, width, height];
31+
}

rema_1001/lib/map/map_painter.dart

Lines changed: 79 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,38 +1,101 @@
11
import 'package:flutter/widgets.dart';
2+
import 'package:rema_1001/map/map.dart' as map_model;
3+
4+
final dimension = 64;
5+
6+
final borderRadius = Radius.circular(16);
7+
final backgroundPaint = Paint()..color = const Color(0xff434343);
8+
final borderPaint = Paint()
9+
..color = const Color(0xff2C2C2C)
10+
..style = PaintingStyle.stroke
11+
..strokeWidth = 30
12+
..strokeCap = StrokeCap.round;
13+
14+
final aislePaint = Paint()..color = const Color(0xFF636363);
15+
final aisleShadowPaint = Paint()..color = const Color(0xff4A4A4A);
16+
17+
final obstructionPaint = Paint()..color = const Color(0xff2C2C2C);
218

319
final class MapPainter implements CustomPainter {
4-
@override
5-
void addListener(VoidCallback listener) {
6-
// TODO: implement addListener
7-
}
20+
final map_model.Map map;
21+
22+
MapPainter({required this.map});
823

924
@override
10-
bool? hitTest(Offset position) {
11-
// TODO: implement hitTest
12-
throw UnimplementedError();
25+
void paint(Canvas canvas, Size size) {
26+
_paintBackground(canvas, size);
27+
_paintIsles(canvas, size, map);
1328
}
1429

1530
@override
16-
void paint(Canvas canvas, Size size) {}
31+
void addListener(VoidCallback listener) {}
1732

1833
@override
19-
void removeListener(VoidCallback listener) {
20-
// TODO: implement removeListener
34+
bool? hitTest(Offset position) {
35+
return null;
2136
}
2237

2338
@override
24-
// TODO: implement semanticsBuilder
25-
SemanticsBuilderCallback? get semanticsBuilder => throw UnimplementedError();
39+
void removeListener(VoidCallback listener) {}
40+
41+
@override
42+
SemanticsBuilderCallback? get semanticsBuilder => null;
2643

2744
@override
2845
bool shouldRebuildSemantics(covariant CustomPainter oldDelegate) {
29-
// TODO: implement shouldRebuildSemantics
30-
throw UnimplementedError();
46+
return false;
3147
}
3248

3349
@override
3450
bool shouldRepaint(covariant CustomPainter oldDelegate) {
35-
// TODO: implement shouldRepaint
36-
throw UnimplementedError();
51+
return true;
52+
}
53+
54+
void _paintIsle(Canvas canvas, Size size, map_model.Aisle aisle) {
55+
// Scale factors to convert map coordinates to canvas size
56+
final scaleX = size.width / dimension;
57+
final scaleY = size.height / dimension;
58+
59+
final rect = Rect.fromLTWH(
60+
aisle.topLeft.dx * scaleX,
61+
aisle.topLeft.dy * scaleY,
62+
aisle.width * scaleX,
63+
aisle.height * scaleY,
64+
);
65+
66+
// Soft shadow
67+
if (aisle.status != map_model.AisleStatus.highlighted) {
68+
canvas.drawRRect(
69+
RRect.fromRectAndRadius(rect.shift(Offset(4, 12)), Radius.circular(9)),
70+
Paint()
71+
..color = const Color(0x52000000)
72+
..maskFilter = MaskFilter.blur(BlurStyle.normal, 8),
73+
);
74+
}
75+
76+
// Hard shadow
77+
canvas.drawRRect(
78+
RRect.fromRectAndRadius(rect, Radius.circular(9)),
79+
aisleShadowPaint,
80+
);
81+
82+
// Aisle
83+
canvas.drawRRect(
84+
RRect.fromRectAndRadius(rect.shift(Offset(0, -6)), Radius.circular(9)),
85+
aislePaint,
86+
);
87+
}
88+
89+
void _paintIsles(Canvas canvas, Size size, map_model.Map map) {
90+
for (final aisle in map.aisles) {
91+
_paintIsle(canvas, size, aisle);
92+
}
93+
}
94+
95+
void _paintBackground(Canvas canvas, Size size) {
96+
canvas.drawRRect(
97+
RRect.fromLTRBR(0, 0, size.width, size.height, borderRadius),
98+
backgroundPaint,
99+
);
37100
}
38101
}

rema_1001/lib/page/map.dart

Lines changed: 46 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
import 'package:flutter/material.dart';
22
import 'package:go_router/go_router.dart';
3+
import 'package:rema_1001/map/map.dart';
4+
import 'package:rema_1001/map/map_painter.dart';
35
import 'package:rema_1001/router/route_names.dart';
46

57
class ListsScreen extends StatelessWidget {
@@ -29,30 +31,51 @@ class ListsScreen extends StatelessWidget {
2931
style: TextStyle(fontSize: 24, fontWeight: FontWeight.bold),
3032
),
3133
const SizedBox(height: 16),
32-
Expanded(
33-
child: ListView(
34-
children: [
35-
_buildListCard(
36-
'Weekly Groceries',
37-
'12 items',
38-
Icons.shopping_cart,
39-
Colors.blue,
40-
),
41-
const SizedBox(height: 12),
42-
_buildListCard(
43-
'Party Supplies',
44-
'8 items',
45-
Icons.celebration,
46-
Colors.orange,
47-
),
48-
const SizedBox(height: 12),
49-
_buildListCard(
50-
'Breakfast Items',
51-
'5 items',
52-
Icons.free_breakfast,
53-
Colors.green,
34+
AspectRatio(
35+
aspectRatio: 1,
36+
child: CustomPaint(
37+
painter: MapPainter(
38+
map: Map(
39+
walkPoints: const [],
40+
aisles: const [
41+
// Top-left counter/checkout area
42+
Aisle(topLeft: Offset(10, 4), width: 15, height: 6),
43+
44+
// Top-right three small blocks
45+
Aisle(topLeft: Offset(31, 4), width: 5, height: 5),
46+
Aisle(topLeft: Offset(38, 4), width: 5, height: 5),
47+
Aisle(topLeft: Offset(45, 4), width: 5, height: 5),
48+
49+
// Upper-middle left rectangle
50+
Aisle(topLeft: Offset(10, 17), width: 18, height: 7),
51+
52+
// Upper-middle center rectangle
53+
Aisle(topLeft: Offset(31, 17), width: 12, height: 7),
54+
55+
// Right tall vertical rectangle
56+
Aisle(topLeft: Offset(46, 17), width: 4, height: 18),
57+
58+
// Lower-middle left rectangle
59+
Aisle(topLeft: Offset(10, 27), width: 18, height: 7),
60+
61+
// Lower-middle center rectangle
62+
Aisle(topLeft: Offset(31, 27), width: 12, height: 7),
63+
64+
// Bottom three circles
65+
Aisle(topLeft: Offset(23, 42), width: 5, height: 5),
66+
Aisle(topLeft: Offset(31, 42), width: 3, height: 3),
67+
Aisle(topLeft: Offset(38, 42), width: 5, height: 5),
68+
69+
// Bottom large rectangle
70+
Aisle(topLeft: Offset(17, 50), width: 30, height: 9),
71+
72+
// Borders
73+
Aisle(topLeft: Offset(0, 0), width: 8, height: 64),
74+
Aisle(topLeft: Offset(0, 0), width: 64, height: 8),
75+
Aisle(topLeft: Offset(56, 0), width: 8, height: 64),
76+
],
5477
),
55-
],
78+
),
5679
),
5780
),
5881
const SizedBox(height: 16),
@@ -67,18 +90,4 @@ class ListsScreen extends StatelessWidget {
6790
),
6891
);
6992
}
70-
71-
Widget _buildListCard(String title, String itemCount, IconData icon, Color color) {
72-
return Card(
73-
child: ListTile(
74-
leading: CircleAvatar(
75-
backgroundColor: color.withValues(alpha: 0.2),
76-
child: Icon(icon, color: color),
77-
),
78-
title: Text(title, style: const TextStyle(fontWeight: FontWeight.bold)),
79-
subtitle: Text(itemCount),
80-
trailing: const Icon(Icons.arrow_forward_ios),
81-
),
82-
);
83-
}
8493
}

0 commit comments

Comments
 (0)