Skip to content

Create compass-app first feature #2342

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
56ff720
move files to app folder
miquelbeltran Jun 26, 2024
f58fd1c
WIP building a results screen
miquelbeltran Jun 26, 2024
92e5de8
implement search destination usecase
miquelbeltran Jun 27, 2024
ce60385
add tests
miquelbeltran Jun 27, 2024
92af5b6
created viewmodel and multiple tests
miquelbeltran Jun 27, 2024
c97b83a
connect everything
miquelbeltran Jun 27, 2024
431177e
navigator setup
miquelbeltran Jun 27, 2024
df928c6
setup provider
miquelbeltran Jun 27, 2024
e978f4d
move dependency management to file
miquelbeltran Jun 27, 2024
16fa89a
load a fake list of destinations
miquelbeltran Jun 28, 2024
c343c82
creating first widgets
miquelbeltran Jun 28, 2024
9885024
load pictures in result card
miquelbeltran Jun 28, 2024
a583b31
add documentation
miquelbeltran Jul 1, 2024
c6ccc23
setup GoogleFonts
miquelbeltran Jul 1, 2024
717ab93
tag chips WIP
miquelbeltran Jul 1, 2024
9736a53
create tags and fix text styles
miquelbeltran Jul 1, 2024
aefd0d8
implemented tests
miquelbeltran Jul 1, 2024
ef5a639
remove .dart added by mistake
miquelbeltran Jul 1, 2024
9a3940b
remove .dart added by mistake
miquelbeltran Jul 1, 2024
f5e4c94
remove .dart added by mistake
miquelbeltran Jul 1, 2024
a2bd371
add end of file line
miquelbeltran Jul 1, 2024
cc7346a
fix tests
miquelbeltran Jul 1, 2024
d04c8e3
fix test and lint errors
miquelbeltran Jul 1, 2024
bb102a8
color blur in tags
miquelbeltran Jul 1, 2024
813ded4
add Result class example
miquelbeltran Jul 1, 2024
ec70627
reorganize code around
miquelbeltran Jul 2, 2024
3b1bd6c
applied feedback
miquelbeltran Jul 5, 2024
fbd136e
presentation folder cleanup
miquelbeltran Jul 5, 2024
f63e93c
simple dark theme
miquelbeltran Jul 8, 2024
ed0da99
create themeextension for tagchip
miquelbeltran Jul 8, 2024
7319afd
cleanup
miquelbeltran Jul 8, 2024
51b68a3
cleanup
miquelbeltran Jul 8, 2024
5f1f3e5
inject ViewModel via constructor param
miquelbeltran Jul 9, 2024
dc9aa3c
setup relative imports and cached network image
miquelbeltran Jul 9, 2024
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
1 change: 0 additions & 1 deletion compass_app/analysis_options.yaml

This file was deleted.

File renamed without changes.
File renamed without changes.
File renamed without changes.
5 changes: 5 additions & 0 deletions compass_app/app/analysis_options.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
include: package:flutter_lints/flutter.yaml

linter:
rules:
- prefer_relative_imports
File renamed without changes.
File renamed without changes.
1,235 changes: 1,235 additions & 0 deletions compass_app/app/assets/destinations.json

Large diffs are not rendered by default.

3 changes: 3 additions & 0 deletions compass_app/app/devtools_options.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
description: This file stores settings for Dart & Flutter DevTools.
documentation: https://docs.flutter.dev/tools/devtools/extensions#configure-extension-enablement-states
extensions:
File renamed without changes.
14 changes: 14 additions & 0 deletions compass_app/app/lib/config/dependencies.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import '../data/repositories/destination/destination_repository.dart';
import '../data/repositories/destination/destination_repository_local.dart';
import 'package:provider/single_child_widget.dart';
import 'package:provider/provider.dart';

/// Configure dependencies as a list of Providers
List<SingleChildWidget> get providers {
// List of Providers
return [
Provider.value(
value: DestinationRepositoryLocal() as DestinationRepository,
),
];
}
50 changes: 50 additions & 0 deletions compass_app/app/lib/data/models/destination.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
/// Model class for Destination data
class Destination {
Destination({
required this.ref,
required this.name,
required this.country,
required this.continent,
required this.knownFor,
required this.tags,
required this.imageUrl,
});

/// e.g. 'alaska'
final String ref;

/// e.g. 'Alaska'
final String name;

/// e.g. 'United States'
final String country;

/// e.g. 'North America'
final String continent;

/// e.g. 'Alaska is a haven for outdoor enthusiasts ...'
final String knownFor;

/// e.g. ['Mountain', 'Off-the-beaten-path', 'Wildlife watching']
final List<String> tags;

/// e.g. 'https://storage.googleapis.com/tripedia-images/destinations/alaska.jpg'
final String imageUrl;

@override
String toString() {
return 'Destination{ref: $ref, name: $name, country: $country, continent: $continent, knownFor: $knownFor, tags: $tags, imageUrl: $imageUrl}';
}

factory Destination.fromJson(Map<String, dynamic> json) {
return Destination(
ref: json['ref'] as String,
name: json['name'] as String,
country: json['country'] as String,
continent: json['continent'] as String,
knownFor: json['knownFor'] as String,
tags: (json['tags'] as List<dynamic>).map((e) => e as String).toList(),
imageUrl: json['imageUrl'] as String,
);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import '../../../utils/result.dart';
import '../../models/destination.dart';

/// Data source with all possible destinations
abstract class DestinationRepository {
/// Get complete list of destinations
Future<Result<List<Destination>>> getDestinations();
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import 'dart:convert';

import '../../../utils/result.dart';
import '../../models/destination.dart';
import 'destination_repository.dart';

import 'package:flutter/services.dart' show rootBundle;

/// Local implementation of DestinationRepository
/// Uses data from assets folder
class DestinationRepositoryLocal implements DestinationRepository {
/// Obtain list of destinations from local assets
@override
Future<Result<List<Destination>>> getDestinations() async {
try {
final localData = await _loadAsset();
final list = _parse(localData);
return Result.ok(list);
} on Exception catch (error) {
return Result.error(error);
}
}

Future<String> _loadAsset() async {
return await rootBundle.loadString('assets/destinations.json');
}

List<Destination> _parse(String localData) {
final parsed = (jsonDecode(localData) as List).cast<Map<String, dynamic>>();

return parsed
.map<Destination>((json) => Destination.fromJson(json))
.toList();
}
}
Empty file.
30 changes: 30 additions & 0 deletions compass_app/app/lib/main.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import 'config/dependencies.dart';
import 'ui/core/themes/theme.dart';
import 'routing/router.dart';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';

void main() {
runApp(
MultiProvider(
// Loading the default providers
// NOTE: We can load different configurations e.g. fakes
providers: providers,
child: const MainApp(),
),
);
}

class MainApp extends StatelessWidget {
const MainApp({super.key});

@override
Widget build(BuildContext context) {
return MaterialApp.router(
theme: AppTheme.lightTheme,
darkTheme: AppTheme.darkTheme,
themeMode: ThemeMode.system,
routerConfig: router,
);
}
}
21 changes: 21 additions & 0 deletions compass_app/app/lib/routing/router.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import '../ui/results/widgets/results_screen.dart';
import 'package:go_router/go_router.dart';
import 'package:provider/provider.dart';

import '../ui/results/view_models/results_viewmodel.dart';

/// Top go_router entry point
final router = GoRouter(
initialLocation: '/results',
routes: [
GoRoute(
path: '/results',
builder: (context, state) {
final viewModel = ResultsViewModel(
destinationRepository: context.read(),
);
return ResultsScreen(viewModel: viewModel);
},
),
],
);
36 changes: 36 additions & 0 deletions compass_app/app/lib/ui/core/themes/colors.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import 'package:flutter/material.dart';

class AppColors {
static const black1 = Color(0xFF101010);
static const white1 = Color(0xFFFFF7FA);
static const grey1 = Color(0xFFF2F2F2);
static const grey2 = Color(0xFF4D4D4D);
static const whiteTransparent =
Color(0x4DFFFFFF); // Figma rgba(255, 255, 255, 0.3)
static const blackTransparent =
Color(0x4D000000); // Figma rgba(255, 255, 255, 0.3)

static const lightColorScheme = ColorScheme(
brightness: Brightness.light,
primary: AppColors.black1,
onPrimary: AppColors.white1,
secondary: AppColors.black1,
onSecondary: AppColors.white1,
surface: Colors.white,
onSurface: AppColors.black1,
error: Colors.red,
onError: Colors.white,
);

static const darkColorScheme = ColorScheme(
brightness: Brightness.dark,
primary: AppColors.white1,
onPrimary: AppColors.black1,
secondary: AppColors.white1,
onSecondary: AppColors.black1,
surface: AppColors.black1,
onSurface: Colors.white,
error: Colors.red,
onError: Colors.white,
);
}
24 changes: 24 additions & 0 deletions compass_app/app/lib/ui/core/themes/text_styles.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import 'package:flutter/material.dart';
import 'package:google_fonts/google_fonts.dart';

// TODO: Maybe the text styles here should be moved to the respective widgets
class TextStyles {
// Note: original Figma file uses Nikkei Maru
// which is not available on GoogleFonts
// Note: Card title theme doesn't change based on light/dark mode
static final cardTitleStyle = GoogleFonts.rubik(
textStyle: const TextStyle(
fontWeight: FontWeight.w800,
fontSize: 15.0,
color: Colors.white,
letterSpacing: 1,
shadows: [
// Helps to read the text a bit better
Shadow(
blurRadius: 3.0,
color: Colors.black,
)
],
),
);
}
27 changes: 27 additions & 0 deletions compass_app/app/lib/ui/core/themes/theme.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import 'colors.dart';
import '../ui/tag_chip.dart';
import 'package:flutter/material.dart';

class AppTheme {
static ThemeData lightTheme = ThemeData(
useMaterial3: true,
colorScheme: AppColors.lightColorScheme,
extensions: [
TagChipTheme(
chipColor: AppColors.whiteTransparent,
onChipColor: Colors.white,
),
],
);

static ThemeData darkTheme = ThemeData(
useMaterial3: true,
colorScheme: AppColors.darkColorScheme,
extensions: [
TagChipTheme(
chipColor: AppColors.blackTransparent,
onChipColor: Colors.white,
),
],
);
}
127 changes: 127 additions & 0 deletions compass_app/app/lib/ui/core/ui/tag_chip.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
import 'dart:ui';

import '../themes/colors.dart';
import 'package:flutter/material.dart';
import 'package:google_fonts/google_fonts.dart';

class TagChip extends StatelessWidget {
const TagChip({
super.key,
required this.tag,
});

final String tag;

@override
Widget build(BuildContext context) {
return ClipRRect(
borderRadius: BorderRadius.circular(10.0),
child: BackdropFilter(
filter: ImageFilter.blur(sigmaX: 3, sigmaY: 3),
child: DecoratedBox(
decoration: BoxDecoration(
color: Theme.of(context).extension<TagChipTheme>()?.chipColor ??

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh this would be an excellent spot to demonstrate good theming defaults best practices! It's common in many of the Flutter material widgets, like here:

Basically, there's an order of priority of where themed elements of a widget come from. They're used in this order:

  1. Arguments passed in through the widget's constructor
  2. The widget-specific theme, grabbed from the context (in our case the theme extension defined below)
  3. A default baked in to the widget itself

If you added a chipColor and onChipColor to the constructor of this widget, we'd be able to follow that pattern.

This pattern is really great as it gives devs flexibility to theme one-off widgets easily, while still having an app-wide theme to fall back on.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good idea! I have copied your comment into our internal doc to comment on this when we talk about widget design

AppColors.whiteTransparent,
),
child: SizedBox(
height: 20.0,
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 6.0),
child: Row(
crossAxisAlignment: CrossAxisAlignment.center,
mainAxisSize: MainAxisSize.min,
children: [
Icon(
_iconFrom(tag),
color: Theme.of(context)
.extension<TagChipTheme>()
?.onChipColor,
size: 10,
),
const SizedBox(width: 4),
Text(
tag,
textAlign: TextAlign.center,
style: _textStyle(context),
),
],
),
),
),
),
),
);
}

IconData? _iconFrom(String tag) {
return switch (tag) {
'Adventure sports' => Icons.kayaking_outlined,
'Beach' => Icons.beach_access_outlined,
'City' => Icons.location_city_outlined,
'Cultural experiences' => Icons.museum_outlined,
'Foodie' || 'Food tours' => Icons.restaurant,
'Hiking' => Icons.hiking,
'Historic' => Icons.menu_book_outlined,
'Island' || 'Coastal' || 'Lake' || 'River' => Icons.water,
'Luxury' => Icons.attach_money_outlined,
'Mountain' || 'Wildlife watching' => Icons.landscape_outlined,
'Nightlife' => Icons.local_bar_outlined,
'Off-the-beaten-path' => Icons.do_not_step_outlined,
'Romantic' => Icons.favorite_border_outlined,
'Rural' => Icons.agriculture_outlined,
'Secluded' => Icons.church_outlined,
'Sightseeing' => Icons.attractions_outlined,
'Skiing' => Icons.downhill_skiing_outlined,
'Wine tasting' => Icons.wine_bar_outlined,
'Winter destination' => Icons.ac_unit,
_ => Icons.label_outlined,
};
}

// Note: original Figma file uses Google Sans
// which is not available on GoogleFonts
_textStyle(BuildContext context) => GoogleFonts.openSans(
textStyle: TextStyle(
fontWeight: FontWeight.w500,
fontSize: 10,
color: Theme.of(context).extension<TagChipTheme>()?.onChipColor ??
Colors.white,
textBaseline: TextBaseline.alphabetic,
),
);
}

class TagChipTheme extends ThemeExtension<TagChipTheme> {
final Color chipColor;
final Color onChipColor;

TagChipTheme({
required this.chipColor,
required this.onChipColor,
});

@override
ThemeExtension<TagChipTheme> copyWith({
Color? chipColor,
Color? onChipColor,
}) {
return TagChipTheme(
chipColor: chipColor ?? this.chipColor,
onChipColor: onChipColor ?? this.onChipColor,
);
}

@override
ThemeExtension<TagChipTheme> lerp(
covariant ThemeExtension<TagChipTheme> other,
double t,
) {
if (other is! TagChipTheme) {
return this;
}
return TagChipTheme(
chipColor: Color.lerp(chipColor, other.chipColor, t) ?? chipColor,
onChipColor: Color.lerp(onChipColor, other.onChipColor, t) ?? onChipColor,
);
}
}
Loading