Skip to content
Merged
Show file tree
Hide file tree
Changes from 26 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
File renamed without changes.
File renamed without changes.
File renamed without changes.
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.
27 changes: 27 additions & 0 deletions compass_app/app/lib/config/dependencies.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import 'package:compass_app/data/repositories/destination/destination_repository_local.dart';
import 'package:compass_app/ui/results/business/search_destination_usecase.dart';
import 'package:compass_app/ui/results/presentation/results_viewmodel.dart';
import 'package:provider/provider.dart';
import 'package:provider/single_child_widget.dart';

/// Configure dependencies as a list of Providers
List<SingleChildWidget> get providers {
// These dependencies don't need to be in the widget tree (yet?)
final destinationRepository = DestinationRepositoryLocal();
// Configure usecase to use the local data repository implementation
final searchDestinationUsecase = SearchDestinationUsecase(
repository: destinationRepository,
);

// List of Providers
return [
// ViewModels are injected into Views using Provider
ChangeNotifierProvider(
create: (_) => ResultsViewModel(
searchDestinationUsecase: searchDestinationUsecase,
),
// create this ViewModel only when needed
lazy: true,
),
];
}
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 'package:compass_app/utils/result.dart';
import 'package:compass_app/data/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,36 @@
import 'dart:convert';

import 'package:compass_app/utils/result.dart';
import 'package:compass_app/data/models/destination.dart';
import 'package:compass_app/data/repositories/destination/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.
28 changes: 28 additions & 0 deletions compass_app/app/lib/main.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import 'package:compass_app/config/dependencies.dart';
import 'package:compass_app/ui/core/themes/theme.dart';
import 'package:compass_app/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.theme,
routerConfig: router,
);
}
}
15 changes: 15 additions & 0 deletions compass_app/app/lib/routing/router.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import 'package:compass_app/ui/results/presentation/results_screen.dart';
import 'package:go_router/go_router.dart';

/// Top go_router entry point
final router = GoRouter(
initialLocation: '/results',
routes: [
GoRoute(
path: '/results',
builder: (context, state) {
return const ResultsScreen();
},
),
],
);
10 changes: 10 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,10 @@
import 'dart:ui';

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)
}

33 changes: 33 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,33 @@
import 'package:flutter/material.dart';
import 'package:google_fonts/google_fonts.dart';

class TextStyles {
// Note: original Figma file uses Nikkei Maru
// which is not available on GoogleFonts
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,
)
],
),
);

// Note: original Figma file uses Google Sans
// which is not available on GoogleFonts
static final chipTagStyle = GoogleFonts.openSans(
textStyle: const TextStyle(
fontWeight: FontWeight.w500,
fontSize: 10,
color: Colors.white,
textBaseline: TextBaseline.alphabetic,
),
);
}
19 changes: 19 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,19 @@
import 'package:compass_app/ui/core/themes/colors.dart';
import 'package:flutter/material.dart';

class AppTheme {
static ThemeData theme = ThemeData(

Choose a reason for hiding this comment

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

All of my comments might be too early for this PR, but definitely want to call out that we should support light and dark theming to show what best practices there look like. The theming files will need to be modified pretty significantly in the future for that to work.

Copy link
Member Author

Choose a reason for hiding this comment

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

@ericwindmill do you think we could get a "night mode" version of the Figma document we are using?

useMaterial3: true,
colorScheme: const 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,
),
);
}
77 changes: 77 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,77 @@
import 'dart:ui';

import 'package:compass_app/ui/core/themes/colors.dart';
import 'package:compass_app/ui/core/themes/text_styles.dart';
import 'package:flutter/material.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: const BoxDecoration(
color: 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: Colors.white,
size: 10,
),
const SizedBox(width: 4),
Text(
tag,
textAlign: TextAlign.center,
style: TextStyles.chipTagStyle,
),
],
),
),
),
),
),
);
}

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,
};
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import 'package:compass_app/utils/result.dart';
import 'package:compass_app/data/models/destination.dart';
import 'package:compass_app/data/repositories/destination/destination_repository.dart';

/// Search Destinations Usecase
class SearchDestinationUsecase {
SearchDestinationUsecase({
required DestinationRepository repository,
}) : _repository = repository;

final DestinationRepository _repository;

/// Perform search over possible destinations
/// All search filter options are optional
Future<Result<List<Destination>>> search({String? continent}) async {
bool filter(Destination destination) {
return (continent == null || destination.continent == continent);
}

final result = await _repository.getDestinations();
return switch (result) {
Ok() => Result.ok(result.value.where(filter).toList()),
Error() => result,
};
}
}
54 changes: 54 additions & 0 deletions compass_app/app/lib/ui/results/presentation/result_card.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import 'package:compass_app/ui/core/themes/text_styles.dart';
import 'package:compass_app/ui/core/ui/tag_chip.dart';
import 'package:compass_app/data/models/destination.dart';
import 'package:flutter/material.dart';

class ResultCard extends StatelessWidget {
const ResultCard({
super.key,
required this.destination,
});

final Destination destination;

@override
Widget build(BuildContext context) {
return ClipRRect(
borderRadius: BorderRadius.circular(10.0),
// TODO: Improve image loading and caching
child: Stack(
fit: StackFit.expand,
children: [
Image.network(
destination.imageUrl,
fit: BoxFit.fitHeight,
),
Positioned(
bottom: 12.0,
left: 12.0,
right: 12.0,
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
destination.name.toUpperCase(),
style: TextStyles.cardTitleStyle,
),
const SizedBox(height: 6,),
Wrap(
spacing: 4.0,
runSpacing: 4.0,
direction: Axis.horizontal,
children: destination.tags
.map((e) => TagChip(tag: e))
.toList(),
),
],
),
)
],
),
);
}
}
Loading