Skip to content
Merged
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
43 changes: 43 additions & 0 deletions .github/workflows/flutter_ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
name: Flutter CI


on:
push:
branches:
- dev
- main
pull_request:
branches: [main]

jobs:
test:
name: Analyze & Test
runs-on: ubuntu-latest

steps:
- name: Checkout repo
uses: actions/checkout@v4

- name: Set up Flutter
uses: subosito/flutter-action@v2
with:
channel: stable
cache: true

- name: Restore firebase_options.dart from secret
run: echo "${{ secrets.FIREBASE_OPTIONS_B64 }}" | base64 --decode > lib/firebase_options.dart

- name: Create placeholder .env for CI
run: echo "PLACEHOLDER=true" > .env

- name: Install dependencies
run: flutter pub get

- name: Check formatting
run: dart format --output=none --set-exit-if-changed .

- name: Analyze
run: flutter analyze

- name: Run tests
run: flutter test
2 changes: 1 addition & 1 deletion lib/models/event.dart
Original file line number Diff line number Diff line change
Expand Up @@ -16,4 +16,4 @@ class Event {
required this.interestCount,
this.isUgc = false,
});
}
}
58 changes: 43 additions & 15 deletions lib/screens/home.dart
Original file line number Diff line number Diff line change
Expand Up @@ -10,24 +10,38 @@ class MapScreen extends StatefulWidget {
final void Function(Event event) onViewEventDetails;
final VoidCallback onOpenProfile;

const MapScreen({super.key, required this.onViewEventDetails, required this.onOpenProfile});
const MapScreen({
super.key,
required this.onViewEventDetails,
required this.onOpenProfile,
});

@override
State<MapScreen> createState() => _MapScreenState();
}

class _MapScreenState extends State<MapScreen> {
// Centered on Metro Vancouver, per the brief's target market.
static const _initialPosition = CameraPosition(target: LatLng(49.2400, -123.0000), zoom: 10.5);
static const _mapTypeCycle = [MapType.normal, MapType.satellite, MapType.terrain, MapType.hybrid];
static const _initialPosition = CameraPosition(
target: LatLng(49.2400, -123.0000),
zoom: 10.5,
);
static const _mapTypeCycle = [
MapType.normal,
MapType.satellite,
MapType.terrain,
MapType.hybrid,
];

GoogleMapController? _controller;
bool _is3D = false;
MapType _mapType = MapType.normal;

Set<Marker> _buildMarkers(BuildContext context) {
final theme = Theme.of(context);
final hue = theme.brightness == Brightness.dark ? BitmapDescriptor.hueCyan : BitmapDescriptor.hueAzure;
final hue = theme.brightness == Brightness.dark
? BitmapDescriptor.hueCyan
: BitmapDescriptor.hueAzure;

return mockEvents.map((event) {
return Marker(
Expand All @@ -36,29 +50,39 @@ class _MapScreenState extends State<MapScreen> {
icon: BitmapDescriptor.defaultMarkerWithHue(
event.isUgc ? BitmapDescriptor.hueOrange : hue,
),
onTap: () => EventPreviewSheet.show(context, event, () => widget.onViewEventDetails(event)),
onTap: () => EventPreviewSheet.show(
context,
event,
() => widget.onViewEventDetails(event),
),
);
}).toSet();
}

void _toggle3D() {
setState(() => _is3D = !_is3D);
_controller?.animateCamera(CameraUpdate.newCameraPosition(
CameraPosition(
target: _initialPosition.target,
zoom: _initialPosition.zoom,
tilt: _is3D ? 60 : 0,
_controller?.animateCamera(
CameraUpdate.newCameraPosition(
CameraPosition(
target: _initialPosition.target,
zoom: _initialPosition.zoom,
tilt: _is3D ? 60 : 0,
),
),
));
);
}

void _cycleMapType() {
final next = _mapTypeCycle[(_mapTypeCycle.indexOf(_mapType) + 1) % _mapTypeCycle.length];
final next =
_mapTypeCycle[(_mapTypeCycle.indexOf(_mapType) + 1) %
_mapTypeCycle.length];
setState(() => _mapType = next);
}

void _recenter() {
_controller?.animateCamera(CameraUpdate.newCameraPosition(_initialPosition));
_controller?.animateCamera(
CameraUpdate.newCameraPosition(_initialPosition),
);
}

@override
Expand Down Expand Up @@ -90,10 +114,14 @@ class _MapScreenState extends State<MapScreen> {
MapBottomSheet(
suggestedEvents: mockEvents,
onOpenProfile: widget.onOpenProfile,
onSelectEvent: (event) => EventPreviewSheet.show(context, event, () => widget.onViewEventDetails(event)),
onSelectEvent: (event) => EventPreviewSheet.show(
context,
event,
() => widget.onViewEventDetails(event),
),
),
],
),
);
}
}
}
53 changes: 43 additions & 10 deletions lib/screens/login.dart
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,12 @@ class _LoginScreenState extends State<LoginScreen> {
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text('WELCOME BACK', style: theme.textTheme.labelSmall?.copyWith(color: theme.colorScheme.primary)),
Text(
'WELCOME BACK',
style: theme.textTheme.labelSmall?.copyWith(
color: theme.colorScheme.primary,
),
),
const SizedBox(height: 8),
Text('Log in', style: theme.textTheme.headlineMedium),
const SizedBox(height: 4),
Expand All @@ -46,12 +51,24 @@ class _LoginScreenState extends State<LoginScreen> {
const SizedBox(height: 20),
Row(
children: [
Expanded(child: Divider(color: theme.colorScheme.onSurface.withValues(alpha: 0.15))),
Expanded(
child: Divider(
color: theme.colorScheme.onSurface.withValues(
alpha: 0.15,
),
),
),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 12),
child: Text('or', style: theme.textTheme.bodyMedium),
),
Expanded(child: Divider(color: theme.colorScheme.onSurface.withValues(alpha: 0.15))),
Expanded(
child: Divider(
color: theme.colorScheme.onSurface.withValues(
alpha: 0.15,
),
),
),
],
),
const SizedBox(height: 20),
Expand All @@ -60,24 +77,36 @@ class _LoginScreenState extends State<LoginScreen> {
TextFormField(
controller: _emailCtrl,
keyboardType: TextInputType.emailAddress,
decoration: const InputDecoration(hintText: 'you@example.com', prefixIcon: Icon(Icons.mail_outline)),
validator: (v) => (v == null || !v.contains('@')) ? 'Enter a valid email' : null,
decoration: const InputDecoration(
hintText: 'you@example.com',
prefixIcon: Icon(Icons.mail_outline),
),
validator: (v) => (v == null || !v.contains('@'))
? 'Enter a valid email'
: null,
),
const SizedBox(height: 16),
Text('Password', style: theme.textTheme.bodyMedium),
const SizedBox(height: 6),
TextFormField(
controller: _passwordCtrl,
obscureText: true,
decoration: const InputDecoration(hintText: 'Your password', prefixIcon: Icon(Icons.lock_outline)),
validator: (v) => (v == null || v.isEmpty) ? 'Enter your password' : null,
decoration: const InputDecoration(
hintText: 'Your password',
prefixIcon: Icon(Icons.lock_outline),
),
validator: (v) =>
(v == null || v.isEmpty) ? 'Enter your password' : null,
),
const SizedBox(height: 8),
Align(
alignment: Alignment.centerRight,
child: TextButton(
onPressed: () {}, // TODO: forgot password flow
child: Text('Forgot password?', style: TextStyle(color: theme.colorScheme.primary)),
child: Text(
'Forgot password?',
style: TextStyle(color: theme.colorScheme.primary),
),
),
),
const SizedBox(height: 20),
Expand All @@ -87,7 +116,11 @@ class _LoginScreenState extends State<LoginScreen> {
},
child: const Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [Text('Log in'), SizedBox(width: 8), Icon(Icons.arrow_forward, size: 18)],
children: [
Text('Log in'),
SizedBox(width: 8),
Icon(Icons.arrow_forward, size: 18),
],
),
),
const SizedBox(height: 24),
Expand All @@ -97,4 +130,4 @@ class _LoginScreenState extends State<LoginScreen> {
),
);
}
}
}
Loading
Loading