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
4 changes: 4 additions & 0 deletions .fvmrc
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"flutter": "3.22.3",
"flavors": {}
}
6 changes: 5 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -46,4 +46,8 @@ app.*.map.json
/android/app/release

# fvm
.fvm/flutter_sdk
.fvm/flutter_sdk

# environments
environments/.env.dev
environments/.env.prod
16 changes: 14 additions & 2 deletions .vscode/launch.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,21 @@
"version": "0.2.0",
"configurations": [
{
"name": "app",
"name": "dev",
"request": "launch",
"type": "dart"
"type": "dart",
"args": [
"--dart-define=environment=dev"
]
},
{
"name": "prod",
"request": "launch",
"type": "dart",
"args": [
"--release",
"--dart-define=environment=prod"
]
}
]
}
2 changes: 2 additions & 0 deletions environments/.env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
API_KEY=YOUR_API_KEY
API_URL=YOUR_API_URL
1 change: 1 addition & 0 deletions ios/Flutter/Debug.xcconfig
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"
#include "Generated.xcconfig"
1 change: 1 addition & 0 deletions ios/Flutter/Release.xcconfig
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"
#include "Generated.xcconfig"
44 changes: 44 additions & 0 deletions ios/Podfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
# Uncomment this line to define a global platform for your project
# platform :ios, '12.0'

# CocoaPods analytics sends network stats synchronously affecting flutter build latency.
ENV['COCOAPODS_DISABLE_STATS'] = 'true'

project 'Runner', {
'Debug' => :debug,
'Profile' => :release,
'Release' => :release,
}

def flutter_root
generated_xcode_build_settings_path = File.expand_path(File.join('..', 'Flutter', 'Generated.xcconfig'), __FILE__)
unless File.exist?(generated_xcode_build_settings_path)
raise "#{generated_xcode_build_settings_path} must exist. If you're running pod install manually, make sure flutter pub get is executed first"
end

File.foreach(generated_xcode_build_settings_path) do |line|
matches = line.match(/FLUTTER_ROOT\=(.*)/)
return matches[1].strip if matches
end
raise "FLUTTER_ROOT not found in #{generated_xcode_build_settings_path}. Try deleting Generated.xcconfig, then run flutter pub get"
end

require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root)

flutter_ios_podfile_setup

target 'Runner' do
use_frameworks!
use_modular_headers!

flutter_install_all_ios_pods File.dirname(File.realpath(__FILE__))
target 'RunnerTests' do
inherit! :search_paths
end
end

post_install do |installer|
installer.pods_project.targets.each do |target|
flutter_additional_ios_build_settings(target)
end
end
37 changes: 37 additions & 0 deletions lib/core/database/app_database.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import 'dart:io';

import 'package:path/path.dart' as path;
import 'package:path_provider/path_provider.dart';
import 'package:sembast/sembast.dart';
import 'package:sembast/sembast_io.dart';

class AppDatabase {
static final AppDatabase _instance = AppDatabase._internal();
static final DatabaseFactory _dbFactory = databaseFactoryIo;
static Database? _database;

factory AppDatabase() => _instance;

AppDatabase._internal();

Future<Database> get database async {
if (_database != null) return _database!;

_database = await _initDatabase();
return _database!;
}

Future<Database> _initDatabase() async {
Directory appDocDir = await getApplicationDocumentsDirectory();
String dbPath = path.join(appDocDir.path, 'flutter_test.db');
Database database = await _dbFactory.openDatabase(dbPath);
return database;
}

Future closeDatabase() async {
if (_database != null) {
await _database!.close();
_database = null;
}
}
}
1 change: 1 addition & 0 deletions lib/core/database/database.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export 'favorite_restaurants_dao.dart';
36 changes: 36 additions & 0 deletions lib/core/database/favorite_restaurants_dao.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import 'dart:async';

import 'package:sembast/sembast.dart';

import 'package:restaurant_tour/core/database/app_database.dart';
import 'package:restaurant_tour/core/models/restaurant.dart';

class FavoriteRestaurantsDao {
static const String favoriteRestaurantsStoreName = 'favorite_restaurants';

final _favoriteRestaurantsStore =
intMapStoreFactory.store(favoriteRestaurantsStoreName);

Future get _db async => AppDatabase().database;

Future insert(Restaurant restaurant) async {
await _favoriteRestaurantsStore.add(await _db, restaurant.toJson());
}

Future<List<Restaurant>> getAll() async {
final list = await _favoriteRestaurantsStore.find(await _db);
return list.map((e) => Restaurant.fromJson(e.value)).toList();
}

Future delete({required String restaurantId}) async {
await _favoriteRestaurantsStore.delete(
await _db,
finder: Finder(
filter: Filter.equals(
'id',
restaurantId,
),
),
);
}
}
File renamed without changes.
File renamed without changes.
16 changes: 16 additions & 0 deletions lib/core/routes.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import 'package:flutter/material.dart';

import 'package:restaurant_tour/modules/home/home_page.dart';
import 'package:restaurant_tour/modules/restaurant_detail/restaurant_detail_page.dart';

class RoutePaths {
static const String initial = '/';
static const String restaurantDetail = 'restaurant-detail';
}

Map<String, WidgetBuilder> getRoutes() {
return {
RoutePaths.initial: (_) => const HomePage(),
RoutePaths.restaurantDetail: (_) => const RestaurantDetailPage(),
};
}
64 changes: 64 additions & 0 deletions lib/core/services/dio_service.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import 'package:flutter/material.dart';

import 'package:dio/dio.dart';

import 'package:restaurant_tour/core/services/dotenv_service.dart';
import 'package:restaurant_tour/design_system/design_system.dart';

late Dio dio;

class DioService {
static final BaseOptions dioOptions = BaseOptions(
baseUrl: DotenvService.apiUrl,
);

static init({
required String apiKey,
required GlobalKey<ScaffoldMessengerState> snackbarKey,
}) {
dio = Dio(dioOptions);

dio.interceptors.add(
LogInterceptor(
requestBody: true,
responseBody: true,
),
);

dio.interceptors.add(
InterceptorsWrapper(
onRequest: (options, handler) {
options.headers['Authorization'] = 'Bearer $apiKey';
return handler.next(options);
},
onResponse: (response, handler) => handler.next(response),
onError: (error, handler) async {
if (error.response?.statusCode == 401) {
showSnackBar(
snackbarKey: snackbarKey,
message: 'Invalid credentials',
);
}

return handler.next(error);
},
),
);
}

static showSnackBar({
required GlobalKey<ScaffoldMessengerState> snackbarKey,
required String message,
}) {
final snackBar = SnackBar(
backgroundColor: AppColors.error,
content: Text(
message,
style: const TextStyle(
color: DsColors.white,
),
),
);
snackbarKey.currentState?.showSnackBar(snackBar);
}
}
24 changes: 24 additions & 0 deletions lib/core/services/dotenv_service.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import 'package:flutter_dotenv/flutter_dotenv.dart';

class DotenvService {
static final DotenvService _instance = DotenvService._internal();

factory DotenvService() => _instance;

DotenvService._internal();

late String _environment;

Future<void> init() async {
_environment = _getEnvironment();
await dotenv.load(fileName: 'environments/.env.$_environment');
}

String _getEnvironment() => const String.fromEnvironment(
'environment',
defaultValue: 'dev',
);

static final apiKey = dotenv.get('API_KEY');
static final apiUrl = dotenv.get('API_URL');
}
3 changes: 3 additions & 0 deletions lib/design_system/atoms/atoms.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export 'ds_rating.dart';
export 'ds_texts.dart';
export 'ds_image_network.dart';
80 changes: 80 additions & 0 deletions lib/design_system/atoms/ds_image_network.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
import 'package:flutter/material.dart';

class DsImageNetwork extends StatelessWidget {
const DsImageNetwork({
super.key,
this.urlImage,
this.emptyIcon,
this.errorIcon,
this.fit,
this.height,
this.width = 100,
this.isRounded,
});

final String? urlImage;
final Widget? emptyIcon;
final Widget? errorIcon;
final BoxFit? fit;
final double? height;
final double width;
final bool? isRounded;

@override
Widget build(BuildContext context) {
return urlImage == null
? Icon(
Icons.image,
color: Colors.grey.shade300,
size: width,
)
: _RoundedBorderImage(
isRounded: isRounded ?? false,
child: Image.network(
urlImage!,
fit: fit ?? BoxFit.fill,
width: width,
height: height ?? width,
loadingBuilder: (context, child, loadingProgress) =>
loadingProgress == null
? child
: Center(
child: SizedBox(
width: width,
height: height ?? width,
child: const CircularProgressIndicator(),
),
),
errorBuilder: (context, error, stackTrace) =>
errorIcon ??
Icon(
Icons.image,
size: width,
),
),
);
}
}

class _RoundedBorderImage extends StatelessWidget {
const _RoundedBorderImage({
required this.isRounded,
required this.child,
});

final bool isRounded;
final Widget child;

@override
Widget build(BuildContext context) {
return isRounded
? Padding(
padding: const EdgeInsets.all(16.0),
child: ClipRRect(
borderRadius: BorderRadius.circular(8),
child: child,
),
)
: child;
}
}
Loading