Initial commit: CleanPlate shared Dart package
Domain models, enums, constants, validators shared between frontend and backend.
This commit is contained in:
7
.gitignore
vendored
Normal file
7
.gitignore
vendored
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
# https://dart.dev/guides/libraries/private-files
|
||||||
|
# Created by `dart pub`
|
||||||
|
.dart_tool/
|
||||||
|
|
||||||
|
# Avoid committing pubspec.lock for library packages; see
|
||||||
|
# https://dart.dev/guides/libraries/private-files#pubspeclock.
|
||||||
|
pubspec.lock
|
||||||
3
CHANGELOG.md
Normal file
3
CHANGELOG.md
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
## 1.0.0
|
||||||
|
|
||||||
|
- Initial version.
|
||||||
39
README.md
Normal file
39
README.md
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
<!--
|
||||||
|
This README describes the package. If you publish this package to pub.dev,
|
||||||
|
this README's contents appear on the landing page for your package.
|
||||||
|
|
||||||
|
For information about how to write a good package README, see the guide for
|
||||||
|
[writing package pages](https://dart.dev/tools/pub/writing-package-pages).
|
||||||
|
|
||||||
|
For general information about developing packages, see the Dart guide for
|
||||||
|
[creating packages](https://dart.dev/guides/libraries/create-packages)
|
||||||
|
and the Flutter guide for
|
||||||
|
[developing packages and plugins](https://flutter.dev/to/develop-packages).
|
||||||
|
-->
|
||||||
|
|
||||||
|
TODO: Put a short description of the package here that helps potential users
|
||||||
|
know whether this package might be useful for them.
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
TODO: List what your package can do. Maybe include images, gifs, or videos.
|
||||||
|
|
||||||
|
## Getting started
|
||||||
|
|
||||||
|
TODO: List prerequisites and provide or point to information on how to
|
||||||
|
start using the package.
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
TODO: Include short and useful examples for package users. Add longer examples
|
||||||
|
to `/example` folder.
|
||||||
|
|
||||||
|
```dart
|
||||||
|
const like = 'sample';
|
||||||
|
```
|
||||||
|
|
||||||
|
## Additional information
|
||||||
|
|
||||||
|
TODO: Tell users more about the package: where to find more information, how to
|
||||||
|
contribute to the package, how to file issues, what response they can expect
|
||||||
|
from the package authors, and more.
|
||||||
30
analysis_options.yaml
Normal file
30
analysis_options.yaml
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
# This file configures the static analysis results for your project (errors,
|
||||||
|
# warnings, and lints).
|
||||||
|
#
|
||||||
|
# This enables the 'recommended' set of lints from `package:lints`.
|
||||||
|
# This set helps identify many issues that may lead to problems when running
|
||||||
|
# or consuming Dart code, and enforces writing Dart using a single, idiomatic
|
||||||
|
# style and format.
|
||||||
|
#
|
||||||
|
# If you want a smaller set of lints you can change this to specify
|
||||||
|
# 'package:lints/core.yaml'. These are just the most critical lints
|
||||||
|
# (the recommended set includes the core lints).
|
||||||
|
# The core lints are also what is used by pub.dev for scoring packages.
|
||||||
|
|
||||||
|
include: package:lints/recommended.yaml
|
||||||
|
|
||||||
|
# Uncomment the following section to specify additional rules.
|
||||||
|
|
||||||
|
# linter:
|
||||||
|
# rules:
|
||||||
|
# - camel_case_types
|
||||||
|
|
||||||
|
# analyzer:
|
||||||
|
# exclude:
|
||||||
|
# - path/to/excluded/files/**
|
||||||
|
|
||||||
|
# For more information about the core and recommended set of lints, see
|
||||||
|
# https://dart.dev/go/core-lints
|
||||||
|
|
||||||
|
# For additional information about configuring this file, see
|
||||||
|
# https://dart.dev/guides/language/analysis-options
|
||||||
13
example/cleanplate_shared_example.dart
Normal file
13
example/cleanplate_shared_example.dart
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
import 'package:cleanplate_shared/cleanplate_shared.dart';
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
// Example: validate a recipe title
|
||||||
|
final titleError = RecipeValidator.validateTitle('My Delicious Pasta');
|
||||||
|
print('Title validation: ${titleError ?? "valid"}');
|
||||||
|
|
||||||
|
// Example: use diet type enum
|
||||||
|
print('Diet: ${DietType.vegetarian.displayName}');
|
||||||
|
|
||||||
|
// Example: API constants
|
||||||
|
print('Login endpoint: ${ApiConstants.authLogin}');
|
||||||
|
}
|
||||||
26
lib/cleanplate_shared.dart
Normal file
26
lib/cleanplate_shared.dart
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
library;
|
||||||
|
|
||||||
|
// Models
|
||||||
|
export 'src/models/recipe.dart';
|
||||||
|
export 'src/models/ingredient.dart';
|
||||||
|
export 'src/models/cooking_step.dart';
|
||||||
|
export 'src/models/nutrition_info.dart';
|
||||||
|
export 'src/models/user.dart';
|
||||||
|
export 'src/models/dietary_profile.dart';
|
||||||
|
export 'src/models/review.dart';
|
||||||
|
export 'src/models/ai_recipe_request.dart';
|
||||||
|
export 'src/models/api_response.dart';
|
||||||
|
|
||||||
|
// Enums
|
||||||
|
export 'src/enums/diet_type.dart';
|
||||||
|
export 'src/enums/difficulty.dart';
|
||||||
|
export 'src/enums/meal_type.dart';
|
||||||
|
export 'src/enums/cuisine_type.dart';
|
||||||
|
|
||||||
|
// Constants
|
||||||
|
export 'src/constants/api_constants.dart';
|
||||||
|
export 'src/constants/error_codes.dart';
|
||||||
|
export 'src/constants/app_limits.dart';
|
||||||
|
|
||||||
|
// Validators
|
||||||
|
export 'src/validators/recipe_validator.dart';
|
||||||
47
lib/src/constants/api_constants.dart
Normal file
47
lib/src/constants/api_constants.dart
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
class ApiConstants {
|
||||||
|
ApiConstants._();
|
||||||
|
|
||||||
|
static const String apiVersion = 'v1';
|
||||||
|
static const String basePath = '/api/$apiVersion';
|
||||||
|
|
||||||
|
// Auth
|
||||||
|
static const String authRegister = '$basePath/auth/register';
|
||||||
|
static const String authLogin = '$basePath/auth/login';
|
||||||
|
static const String authRefresh = '$basePath/auth/refresh';
|
||||||
|
static const String authLogout = '$basePath/auth/logout';
|
||||||
|
|
||||||
|
// Recipes
|
||||||
|
static const String recipes = '$basePath/recipes';
|
||||||
|
static String recipe(String id) => '$basePath/recipes/$id';
|
||||||
|
static String recipeNutrition(String id) => '$basePath/recipes/$id/nutrition';
|
||||||
|
static String recipeSave(String id) => '$basePath/recipes/$id/save';
|
||||||
|
static const String savedRecipes = '$basePath/recipes/saved';
|
||||||
|
|
||||||
|
// AI
|
||||||
|
static const String aiGenerate = '$basePath/ai/generate';
|
||||||
|
static const String aiAdapt = '$basePath/ai/adapt';
|
||||||
|
static const String aiParseIngredients = '$basePath/ai/parse-ingredients';
|
||||||
|
static const String aiSuggestions = '$basePath/ai/suggestions';
|
||||||
|
|
||||||
|
// Search
|
||||||
|
static const String search = '$basePath/search';
|
||||||
|
static const String searchAutocomplete = '$basePath/search/autocomplete';
|
||||||
|
|
||||||
|
// Reviews
|
||||||
|
static String recipeReviews(String recipeId) => '$basePath/recipes/$recipeId/reviews';
|
||||||
|
|
||||||
|
// Media
|
||||||
|
static const String uploadImage = '$basePath/media/upload/image';
|
||||||
|
static const String uploadVideo = '$basePath/media/upload/video';
|
||||||
|
|
||||||
|
// Users
|
||||||
|
static const String currentUser = '$basePath/users/me';
|
||||||
|
static const String dietaryProfile = '$basePath/users/me/dietary';
|
||||||
|
|
||||||
|
// Meal Plans
|
||||||
|
static const String mealPlans = '$basePath/meal-plans';
|
||||||
|
static String mealPlanGroceryList(String id) => '$basePath/meal-plans/$id/grocery-list';
|
||||||
|
|
||||||
|
// Sync
|
||||||
|
static const String sync = '$basePath/users/me/sync';
|
||||||
|
}
|
||||||
29
lib/src/constants/app_limits.dart
Normal file
29
lib/src/constants/app_limits.dart
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
class AppLimits {
|
||||||
|
AppLimits._();
|
||||||
|
|
||||||
|
// Free tier
|
||||||
|
static const int freeMaxSavedRecipes = 20;
|
||||||
|
static const int freeMaxAiGenerationsPerDay = 5;
|
||||||
|
|
||||||
|
// Premium tier
|
||||||
|
static const int premiumMaxAiGenerationsPerDay = 25;
|
||||||
|
static const int premiumMaxHouseholdMembers = 6;
|
||||||
|
|
||||||
|
// Media
|
||||||
|
static const int maxImageSizeBytes = 10 * 1024 * 1024; // 10MB
|
||||||
|
static const int maxVideoSizeBytes = 200 * 1024 * 1024; // 200MB
|
||||||
|
static const int maxVideoDurationSeconds = 180; // 3 minutes
|
||||||
|
|
||||||
|
// Content
|
||||||
|
static const int maxRecipeTitleLength = 300;
|
||||||
|
static const int maxReviewBodyLength = 2000;
|
||||||
|
static const int maxIngredientsPerRecipe = 50;
|
||||||
|
static const int maxStepsPerRecipe = 30;
|
||||||
|
|
||||||
|
// Pagination
|
||||||
|
static const int defaultPageSize = 20;
|
||||||
|
static const int maxPageSize = 100;
|
||||||
|
|
||||||
|
// Reports
|
||||||
|
static const int autoHideReportThreshold = 3;
|
||||||
|
}
|
||||||
34
lib/src/constants/error_codes.dart
Normal file
34
lib/src/constants/error_codes.dart
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
class ErrorCodes {
|
||||||
|
ErrorCodes._();
|
||||||
|
|
||||||
|
// Auth
|
||||||
|
static const String invalidCredentials = 'INVALID_CREDENTIALS';
|
||||||
|
static const String emailAlreadyExists = 'EMAIL_ALREADY_EXISTS';
|
||||||
|
static const String tokenExpired = 'TOKEN_EXPIRED';
|
||||||
|
static const String tokenInvalid = 'TOKEN_INVALID';
|
||||||
|
static const String unauthorized = 'UNAUTHORIZED';
|
||||||
|
|
||||||
|
// Recipes
|
||||||
|
static const String recipeNotFound = 'RECIPE_NOT_FOUND';
|
||||||
|
static const String recipeAlreadySaved = 'RECIPE_ALREADY_SAVED';
|
||||||
|
|
||||||
|
// AI
|
||||||
|
static const String aiRateLimitExceeded = 'AI_RATE_LIMIT_EXCEEDED';
|
||||||
|
static const String aiGenerationFailed = 'AI_GENERATION_FAILED';
|
||||||
|
static const String aiServiceUnavailable = 'AI_SERVICE_UNAVAILABLE';
|
||||||
|
|
||||||
|
// Reviews
|
||||||
|
static const String reviewAlreadyExists = 'REVIEW_ALREADY_EXISTS';
|
||||||
|
static const String reviewNotFound = 'REVIEW_NOT_FOUND';
|
||||||
|
|
||||||
|
// Media
|
||||||
|
static const String fileTooLarge = 'FILE_TOO_LARGE';
|
||||||
|
static const String invalidFileType = 'INVALID_FILE_TYPE';
|
||||||
|
static const String uploadFailed = 'UPLOAD_FAILED';
|
||||||
|
|
||||||
|
// General
|
||||||
|
static const String validationError = 'VALIDATION_ERROR';
|
||||||
|
static const String notFound = 'NOT_FOUND';
|
||||||
|
static const String internalError = 'INTERNAL_ERROR';
|
||||||
|
static const String rateLimited = 'RATE_LIMITED';
|
||||||
|
}
|
||||||
39
lib/src/enums/cuisine_type.dart
Normal file
39
lib/src/enums/cuisine_type.dart
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
enum CuisineType {
|
||||||
|
american,
|
||||||
|
italian,
|
||||||
|
mexican,
|
||||||
|
chinese,
|
||||||
|
japanese,
|
||||||
|
indian,
|
||||||
|
thai,
|
||||||
|
french,
|
||||||
|
greek,
|
||||||
|
korean,
|
||||||
|
vietnamese,
|
||||||
|
mediterranean,
|
||||||
|
middleEastern,
|
||||||
|
african,
|
||||||
|
caribbean,
|
||||||
|
other;
|
||||||
|
|
||||||
|
String get displayName {
|
||||||
|
switch (this) {
|
||||||
|
case CuisineType.american: return 'American';
|
||||||
|
case CuisineType.italian: return 'Italian';
|
||||||
|
case CuisineType.mexican: return 'Mexican';
|
||||||
|
case CuisineType.chinese: return 'Chinese';
|
||||||
|
case CuisineType.japanese: return 'Japanese';
|
||||||
|
case CuisineType.indian: return 'Indian';
|
||||||
|
case CuisineType.thai: return 'Thai';
|
||||||
|
case CuisineType.french: return 'French';
|
||||||
|
case CuisineType.greek: return 'Greek';
|
||||||
|
case CuisineType.korean: return 'Korean';
|
||||||
|
case CuisineType.vietnamese: return 'Vietnamese';
|
||||||
|
case CuisineType.mediterranean: return 'Mediterranean';
|
||||||
|
case CuisineType.middleEastern: return 'Middle Eastern';
|
||||||
|
case CuisineType.african: return 'African';
|
||||||
|
case CuisineType.caribbean: return 'Caribbean';
|
||||||
|
case CuisineType.other: return 'Other';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
29
lib/src/enums/diet_type.dart
Normal file
29
lib/src/enums/diet_type.dart
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
enum DietType {
|
||||||
|
none,
|
||||||
|
vegetarian,
|
||||||
|
vegan,
|
||||||
|
pescatarian,
|
||||||
|
keto,
|
||||||
|
paleo,
|
||||||
|
glutenFree,
|
||||||
|
dairyFree,
|
||||||
|
lowCarb,
|
||||||
|
whole30,
|
||||||
|
mediterranean;
|
||||||
|
|
||||||
|
String get displayName {
|
||||||
|
switch (this) {
|
||||||
|
case DietType.none: return 'No Restriction';
|
||||||
|
case DietType.vegetarian: return 'Vegetarian';
|
||||||
|
case DietType.vegan: return 'Vegan';
|
||||||
|
case DietType.pescatarian: return 'Pescatarian';
|
||||||
|
case DietType.keto: return 'Keto';
|
||||||
|
case DietType.paleo: return 'Paleo';
|
||||||
|
case DietType.glutenFree: return 'Gluten-Free';
|
||||||
|
case DietType.dairyFree: return 'Dairy-Free';
|
||||||
|
case DietType.lowCarb: return 'Low Carb';
|
||||||
|
case DietType.whole30: return 'Whole30';
|
||||||
|
case DietType.mediterranean: return 'Mediterranean';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
13
lib/src/enums/difficulty.dart
Normal file
13
lib/src/enums/difficulty.dart
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
enum Difficulty {
|
||||||
|
easy,
|
||||||
|
medium,
|
||||||
|
hard;
|
||||||
|
|
||||||
|
String get displayName {
|
||||||
|
switch (this) {
|
||||||
|
case Difficulty.easy: return 'Easy';
|
||||||
|
case Difficulty.medium: return 'Medium';
|
||||||
|
case Difficulty.hard: return 'Hard';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
17
lib/src/enums/meal_type.dart
Normal file
17
lib/src/enums/meal_type.dart
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
enum MealType {
|
||||||
|
breakfast,
|
||||||
|
lunch,
|
||||||
|
dinner,
|
||||||
|
snack,
|
||||||
|
dessert;
|
||||||
|
|
||||||
|
String get displayName {
|
||||||
|
switch (this) {
|
||||||
|
case MealType.breakfast: return 'Breakfast';
|
||||||
|
case MealType.lunch: return 'Lunch';
|
||||||
|
case MealType.dinner: return 'Dinner';
|
||||||
|
case MealType.snack: return 'Snack';
|
||||||
|
case MealType.dessert: return 'Dessert';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
27
lib/src/models/ai_recipe_request.dart
Normal file
27
lib/src/models/ai_recipe_request.dart
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
import 'package:json_annotation/json_annotation.dart';
|
||||||
|
|
||||||
|
part 'ai_recipe_request.g.dart';
|
||||||
|
|
||||||
|
@JsonSerializable()
|
||||||
|
class AiRecipeRequest {
|
||||||
|
final List<String> ingredients;
|
||||||
|
final String? dietType;
|
||||||
|
final List<String> allergies;
|
||||||
|
final String? mood; // comfort, quick, healthy, fancy
|
||||||
|
final int? maxTimeMin;
|
||||||
|
final int servings;
|
||||||
|
final String? skillLevel; // beginner, intermediate, advanced
|
||||||
|
|
||||||
|
const AiRecipeRequest({
|
||||||
|
required this.ingredients,
|
||||||
|
this.dietType,
|
||||||
|
this.allergies = const [],
|
||||||
|
this.mood,
|
||||||
|
this.maxTimeMin,
|
||||||
|
this.servings = 4,
|
||||||
|
this.skillLevel,
|
||||||
|
});
|
||||||
|
|
||||||
|
factory AiRecipeRequest.fromJson(Map<String, dynamic> json) => _$AiRecipeRequestFromJson(json);
|
||||||
|
Map<String, dynamic> toJson() => _$AiRecipeRequestToJson(this);
|
||||||
|
}
|
||||||
33
lib/src/models/ai_recipe_request.g.dart
Normal file
33
lib/src/models/ai_recipe_request.g.dart
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||||
|
|
||||||
|
part of 'ai_recipe_request.dart';
|
||||||
|
|
||||||
|
// **************************************************************************
|
||||||
|
// JsonSerializableGenerator
|
||||||
|
// **************************************************************************
|
||||||
|
|
||||||
|
AiRecipeRequest _$AiRecipeRequestFromJson(
|
||||||
|
Map<String, dynamic> json,
|
||||||
|
) => AiRecipeRequest(
|
||||||
|
ingredients:
|
||||||
|
(json['ingredients'] as List<dynamic>).map((e) => e as String).toList(),
|
||||||
|
dietType: json['dietType'] as String?,
|
||||||
|
allergies:
|
||||||
|
(json['allergies'] as List<dynamic>?)?.map((e) => e as String).toList() ??
|
||||||
|
const [],
|
||||||
|
mood: json['mood'] as String?,
|
||||||
|
maxTimeMin: (json['maxTimeMin'] as num?)?.toInt(),
|
||||||
|
servings: (json['servings'] as num?)?.toInt() ?? 4,
|
||||||
|
skillLevel: json['skillLevel'] as String?,
|
||||||
|
);
|
||||||
|
|
||||||
|
Map<String, dynamic> _$AiRecipeRequestToJson(AiRecipeRequest instance) =>
|
||||||
|
<String, dynamic>{
|
||||||
|
'ingredients': instance.ingredients,
|
||||||
|
'dietType': instance.dietType,
|
||||||
|
'allergies': instance.allergies,
|
||||||
|
'mood': instance.mood,
|
||||||
|
'maxTimeMin': instance.maxTimeMin,
|
||||||
|
'servings': instance.servings,
|
||||||
|
'skillLevel': instance.skillLevel,
|
||||||
|
};
|
||||||
62
lib/src/models/api_response.dart
Normal file
62
lib/src/models/api_response.dart
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
import 'package:json_annotation/json_annotation.dart';
|
||||||
|
|
||||||
|
part 'api_response.g.dart';
|
||||||
|
|
||||||
|
@JsonSerializable(genericArgumentFactories: true)
|
||||||
|
class ApiResponse<T> {
|
||||||
|
final String status; // success, error
|
||||||
|
final T? data;
|
||||||
|
final ApiError? error;
|
||||||
|
final PaginationMeta? meta;
|
||||||
|
|
||||||
|
const ApiResponse({
|
||||||
|
required this.status,
|
||||||
|
this.data,
|
||||||
|
this.error,
|
||||||
|
this.meta,
|
||||||
|
});
|
||||||
|
|
||||||
|
factory ApiResponse.success(T data, {PaginationMeta? meta}) =>
|
||||||
|
ApiResponse(status: 'success', data: data, meta: meta);
|
||||||
|
|
||||||
|
factory ApiResponse.error(String code, String message) =>
|
||||||
|
ApiResponse(status: 'error', error: ApiError(code: code, message: message));
|
||||||
|
|
||||||
|
factory ApiResponse.fromJson(
|
||||||
|
Map<String, dynamic> json,
|
||||||
|
T Function(Object? json) fromJsonT,
|
||||||
|
) => _$ApiResponseFromJson(json, fromJsonT);
|
||||||
|
|
||||||
|
Map<String, dynamic> toJson(Object? Function(T value) toJsonT) =>
|
||||||
|
_$ApiResponseToJson(this, toJsonT);
|
||||||
|
}
|
||||||
|
|
||||||
|
@JsonSerializable()
|
||||||
|
class ApiError {
|
||||||
|
final String code;
|
||||||
|
final String message;
|
||||||
|
final dynamic details;
|
||||||
|
|
||||||
|
const ApiError({required this.code, required this.message, this.details});
|
||||||
|
|
||||||
|
factory ApiError.fromJson(Map<String, dynamic> json) => _$ApiErrorFromJson(json);
|
||||||
|
Map<String, dynamic> toJson() => _$ApiErrorToJson(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
@JsonSerializable()
|
||||||
|
class PaginationMeta {
|
||||||
|
final int page;
|
||||||
|
final int limit;
|
||||||
|
final int total;
|
||||||
|
final bool hasMore;
|
||||||
|
|
||||||
|
const PaginationMeta({
|
||||||
|
required this.page,
|
||||||
|
required this.limit,
|
||||||
|
required this.total,
|
||||||
|
required this.hasMore,
|
||||||
|
});
|
||||||
|
|
||||||
|
factory PaginationMeta.fromJson(Map<String, dynamic> json) => _$PaginationMetaFromJson(json);
|
||||||
|
Map<String, dynamic> toJson() => _$PaginationMetaToJson(this);
|
||||||
|
}
|
||||||
71
lib/src/models/api_response.g.dart
Normal file
71
lib/src/models/api_response.g.dart
Normal file
@@ -0,0 +1,71 @@
|
|||||||
|
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||||
|
|
||||||
|
part of 'api_response.dart';
|
||||||
|
|
||||||
|
// **************************************************************************
|
||||||
|
// JsonSerializableGenerator
|
||||||
|
// **************************************************************************
|
||||||
|
|
||||||
|
ApiResponse<T> _$ApiResponseFromJson<T>(
|
||||||
|
Map<String, dynamic> json,
|
||||||
|
T Function(Object? json) fromJsonT,
|
||||||
|
) => ApiResponse<T>(
|
||||||
|
status: json['status'] as String,
|
||||||
|
data: _$nullableGenericFromJson(json['data'], fromJsonT),
|
||||||
|
error:
|
||||||
|
json['error'] == null
|
||||||
|
? null
|
||||||
|
: ApiError.fromJson(json['error'] as Map<String, dynamic>),
|
||||||
|
meta:
|
||||||
|
json['meta'] == null
|
||||||
|
? null
|
||||||
|
: PaginationMeta.fromJson(json['meta'] as Map<String, dynamic>),
|
||||||
|
);
|
||||||
|
|
||||||
|
Map<String, dynamic> _$ApiResponseToJson<T>(
|
||||||
|
ApiResponse<T> instance,
|
||||||
|
Object? Function(T value) toJsonT,
|
||||||
|
) => <String, dynamic>{
|
||||||
|
'status': instance.status,
|
||||||
|
'data': _$nullableGenericToJson(instance.data, toJsonT),
|
||||||
|
'error': instance.error,
|
||||||
|
'meta': instance.meta,
|
||||||
|
};
|
||||||
|
|
||||||
|
T? _$nullableGenericFromJson<T>(
|
||||||
|
Object? input,
|
||||||
|
T Function(Object? json) fromJson,
|
||||||
|
) => input == null ? null : fromJson(input);
|
||||||
|
|
||||||
|
Object? _$nullableGenericToJson<T>(
|
||||||
|
T? input,
|
||||||
|
Object? Function(T value) toJson,
|
||||||
|
) => input == null ? null : toJson(input);
|
||||||
|
|
||||||
|
ApiError _$ApiErrorFromJson(Map<String, dynamic> json) => ApiError(
|
||||||
|
code: json['code'] as String,
|
||||||
|
message: json['message'] as String,
|
||||||
|
details: json['details'],
|
||||||
|
);
|
||||||
|
|
||||||
|
Map<String, dynamic> _$ApiErrorToJson(ApiError instance) => <String, dynamic>{
|
||||||
|
'code': instance.code,
|
||||||
|
'message': instance.message,
|
||||||
|
'details': instance.details,
|
||||||
|
};
|
||||||
|
|
||||||
|
PaginationMeta _$PaginationMetaFromJson(Map<String, dynamic> json) =>
|
||||||
|
PaginationMeta(
|
||||||
|
page: (json['page'] as num).toInt(),
|
||||||
|
limit: (json['limit'] as num).toInt(),
|
||||||
|
total: (json['total'] as num).toInt(),
|
||||||
|
hasMore: json['hasMore'] as bool,
|
||||||
|
);
|
||||||
|
|
||||||
|
Map<String, dynamic> _$PaginationMetaToJson(PaginationMeta instance) =>
|
||||||
|
<String, dynamic>{
|
||||||
|
'page': instance.page,
|
||||||
|
'limit': instance.limit,
|
||||||
|
'total': instance.total,
|
||||||
|
'hasMore': instance.hasMore,
|
||||||
|
};
|
||||||
27
lib/src/models/cooking_step.dart
Normal file
27
lib/src/models/cooking_step.dart
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
import 'package:json_annotation/json_annotation.dart';
|
||||||
|
|
||||||
|
part 'cooking_step.g.dart';
|
||||||
|
|
||||||
|
@JsonSerializable()
|
||||||
|
class CookingStep {
|
||||||
|
final String id;
|
||||||
|
final String recipeId;
|
||||||
|
final int stepNumber;
|
||||||
|
final String instruction;
|
||||||
|
final int? durationMin;
|
||||||
|
final String? imageUrl;
|
||||||
|
final String? tip;
|
||||||
|
|
||||||
|
const CookingStep({
|
||||||
|
required this.id,
|
||||||
|
required this.recipeId,
|
||||||
|
required this.stepNumber,
|
||||||
|
required this.instruction,
|
||||||
|
this.durationMin,
|
||||||
|
this.imageUrl,
|
||||||
|
this.tip,
|
||||||
|
});
|
||||||
|
|
||||||
|
factory CookingStep.fromJson(Map<String, dynamic> json) => _$CookingStepFromJson(json);
|
||||||
|
Map<String, dynamic> toJson() => _$CookingStepToJson(this);
|
||||||
|
}
|
||||||
28
lib/src/models/cooking_step.g.dart
Normal file
28
lib/src/models/cooking_step.g.dart
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||||
|
|
||||||
|
part of 'cooking_step.dart';
|
||||||
|
|
||||||
|
// **************************************************************************
|
||||||
|
// JsonSerializableGenerator
|
||||||
|
// **************************************************************************
|
||||||
|
|
||||||
|
CookingStep _$CookingStepFromJson(Map<String, dynamic> json) => CookingStep(
|
||||||
|
id: json['id'] as String,
|
||||||
|
recipeId: json['recipeId'] as String,
|
||||||
|
stepNumber: (json['stepNumber'] as num).toInt(),
|
||||||
|
instruction: json['instruction'] as String,
|
||||||
|
durationMin: (json['durationMin'] as num?)?.toInt(),
|
||||||
|
imageUrl: json['imageUrl'] as String?,
|
||||||
|
tip: json['tip'] as String?,
|
||||||
|
);
|
||||||
|
|
||||||
|
Map<String, dynamic> _$CookingStepToJson(CookingStep instance) =>
|
||||||
|
<String, dynamic>{
|
||||||
|
'id': instance.id,
|
||||||
|
'recipeId': instance.recipeId,
|
||||||
|
'stepNumber': instance.stepNumber,
|
||||||
|
'instruction': instance.instruction,
|
||||||
|
'durationMin': instance.durationMin,
|
||||||
|
'imageUrl': instance.imageUrl,
|
||||||
|
'tip': instance.tip,
|
||||||
|
};
|
||||||
27
lib/src/models/dietary_profile.dart
Normal file
27
lib/src/models/dietary_profile.dart
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
import 'package:json_annotation/json_annotation.dart';
|
||||||
|
|
||||||
|
part 'dietary_profile.g.dart';
|
||||||
|
|
||||||
|
@JsonSerializable()
|
||||||
|
class DietaryProfile {
|
||||||
|
final String id;
|
||||||
|
final String userId;
|
||||||
|
final String? dietType;
|
||||||
|
final List<String> allergies;
|
||||||
|
final List<String> intolerances;
|
||||||
|
final int? calorieTarget;
|
||||||
|
final List<String> excludedIngredients;
|
||||||
|
|
||||||
|
const DietaryProfile({
|
||||||
|
required this.id,
|
||||||
|
required this.userId,
|
||||||
|
this.dietType,
|
||||||
|
this.allergies = const [],
|
||||||
|
this.intolerances = const [],
|
||||||
|
this.calorieTarget,
|
||||||
|
this.excludedIngredients = const [],
|
||||||
|
});
|
||||||
|
|
||||||
|
factory DietaryProfile.fromJson(Map<String, dynamic> json) => _$DietaryProfileFromJson(json);
|
||||||
|
Map<String, dynamic> toJson() => _$DietaryProfileToJson(this);
|
||||||
|
}
|
||||||
41
lib/src/models/dietary_profile.g.dart
Normal file
41
lib/src/models/dietary_profile.g.dart
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||||
|
|
||||||
|
part of 'dietary_profile.dart';
|
||||||
|
|
||||||
|
// **************************************************************************
|
||||||
|
// JsonSerializableGenerator
|
||||||
|
// **************************************************************************
|
||||||
|
|
||||||
|
DietaryProfile _$DietaryProfileFromJson(Map<String, dynamic> json) =>
|
||||||
|
DietaryProfile(
|
||||||
|
id: json['id'] as String,
|
||||||
|
userId: json['userId'] as String,
|
||||||
|
dietType: json['dietType'] as String?,
|
||||||
|
allergies:
|
||||||
|
(json['allergies'] as List<dynamic>?)
|
||||||
|
?.map((e) => e as String)
|
||||||
|
.toList() ??
|
||||||
|
const [],
|
||||||
|
intolerances:
|
||||||
|
(json['intolerances'] as List<dynamic>?)
|
||||||
|
?.map((e) => e as String)
|
||||||
|
.toList() ??
|
||||||
|
const [],
|
||||||
|
calorieTarget: (json['calorieTarget'] as num?)?.toInt(),
|
||||||
|
excludedIngredients:
|
||||||
|
(json['excludedIngredients'] as List<dynamic>?)
|
||||||
|
?.map((e) => e as String)
|
||||||
|
.toList() ??
|
||||||
|
const [],
|
||||||
|
);
|
||||||
|
|
||||||
|
Map<String, dynamic> _$DietaryProfileToJson(DietaryProfile instance) =>
|
||||||
|
<String, dynamic>{
|
||||||
|
'id': instance.id,
|
||||||
|
'userId': instance.userId,
|
||||||
|
'dietType': instance.dietType,
|
||||||
|
'allergies': instance.allergies,
|
||||||
|
'intolerances': instance.intolerances,
|
||||||
|
'calorieTarget': instance.calorieTarget,
|
||||||
|
'excludedIngredients': instance.excludedIngredients,
|
||||||
|
};
|
||||||
31
lib/src/models/ingredient.dart
Normal file
31
lib/src/models/ingredient.dart
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
import 'package:json_annotation/json_annotation.dart';
|
||||||
|
|
||||||
|
part 'ingredient.g.dart';
|
||||||
|
|
||||||
|
@JsonSerializable()
|
||||||
|
class Ingredient {
|
||||||
|
final String id;
|
||||||
|
final String recipeId;
|
||||||
|
final String name;
|
||||||
|
final double? quantity;
|
||||||
|
final String? unit;
|
||||||
|
final String? groupName;
|
||||||
|
final int sortOrder;
|
||||||
|
final bool optional;
|
||||||
|
final String? canonicalIngredientId;
|
||||||
|
|
||||||
|
const Ingredient({
|
||||||
|
required this.id,
|
||||||
|
required this.recipeId,
|
||||||
|
required this.name,
|
||||||
|
this.quantity,
|
||||||
|
this.unit,
|
||||||
|
this.groupName,
|
||||||
|
required this.sortOrder,
|
||||||
|
this.optional = false,
|
||||||
|
this.canonicalIngredientId,
|
||||||
|
});
|
||||||
|
|
||||||
|
factory Ingredient.fromJson(Map<String, dynamic> json) => _$IngredientFromJson(json);
|
||||||
|
Map<String, dynamic> toJson() => _$IngredientToJson(this);
|
||||||
|
}
|
||||||
32
lib/src/models/ingredient.g.dart
Normal file
32
lib/src/models/ingredient.g.dart
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||||
|
|
||||||
|
part of 'ingredient.dart';
|
||||||
|
|
||||||
|
// **************************************************************************
|
||||||
|
// JsonSerializableGenerator
|
||||||
|
// **************************************************************************
|
||||||
|
|
||||||
|
Ingredient _$IngredientFromJson(Map<String, dynamic> json) => Ingredient(
|
||||||
|
id: json['id'] as String,
|
||||||
|
recipeId: json['recipeId'] as String,
|
||||||
|
name: json['name'] as String,
|
||||||
|
quantity: (json['quantity'] as num?)?.toDouble(),
|
||||||
|
unit: json['unit'] as String?,
|
||||||
|
groupName: json['groupName'] as String?,
|
||||||
|
sortOrder: (json['sortOrder'] as num).toInt(),
|
||||||
|
optional: json['optional'] as bool? ?? false,
|
||||||
|
canonicalIngredientId: json['canonicalIngredientId'] as String?,
|
||||||
|
);
|
||||||
|
|
||||||
|
Map<String, dynamic> _$IngredientToJson(Ingredient instance) =>
|
||||||
|
<String, dynamic>{
|
||||||
|
'id': instance.id,
|
||||||
|
'recipeId': instance.recipeId,
|
||||||
|
'name': instance.name,
|
||||||
|
'quantity': instance.quantity,
|
||||||
|
'unit': instance.unit,
|
||||||
|
'groupName': instance.groupName,
|
||||||
|
'sortOrder': instance.sortOrder,
|
||||||
|
'optional': instance.optional,
|
||||||
|
'canonicalIngredientId': instance.canonicalIngredientId,
|
||||||
|
};
|
||||||
37
lib/src/models/nutrition_info.dart
Normal file
37
lib/src/models/nutrition_info.dart
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
import 'package:json_annotation/json_annotation.dart';
|
||||||
|
|
||||||
|
part 'nutrition_info.g.dart';
|
||||||
|
|
||||||
|
@JsonSerializable()
|
||||||
|
class NutritionInfo {
|
||||||
|
final String id;
|
||||||
|
final String recipeId;
|
||||||
|
final bool perServing;
|
||||||
|
final double? calories;
|
||||||
|
final double? proteinG;
|
||||||
|
final double? fatG;
|
||||||
|
final double? saturatedFatG;
|
||||||
|
final double? carbsG;
|
||||||
|
final double? fiberG;
|
||||||
|
final double? sugarG;
|
||||||
|
final double? sodiumMg;
|
||||||
|
final String source; // auto, manual, usda
|
||||||
|
|
||||||
|
const NutritionInfo({
|
||||||
|
required this.id,
|
||||||
|
required this.recipeId,
|
||||||
|
this.perServing = true,
|
||||||
|
this.calories,
|
||||||
|
this.proteinG,
|
||||||
|
this.fatG,
|
||||||
|
this.saturatedFatG,
|
||||||
|
this.carbsG,
|
||||||
|
this.fiberG,
|
||||||
|
this.sugarG,
|
||||||
|
this.sodiumMg,
|
||||||
|
this.source = 'auto',
|
||||||
|
});
|
||||||
|
|
||||||
|
factory NutritionInfo.fromJson(Map<String, dynamic> json) => _$NutritionInfoFromJson(json);
|
||||||
|
Map<String, dynamic> toJson() => _$NutritionInfoToJson(this);
|
||||||
|
}
|
||||||
39
lib/src/models/nutrition_info.g.dart
Normal file
39
lib/src/models/nutrition_info.g.dart
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||||
|
|
||||||
|
part of 'nutrition_info.dart';
|
||||||
|
|
||||||
|
// **************************************************************************
|
||||||
|
// JsonSerializableGenerator
|
||||||
|
// **************************************************************************
|
||||||
|
|
||||||
|
NutritionInfo _$NutritionInfoFromJson(Map<String, dynamic> json) =>
|
||||||
|
NutritionInfo(
|
||||||
|
id: json['id'] as String,
|
||||||
|
recipeId: json['recipeId'] as String,
|
||||||
|
perServing: json['perServing'] as bool? ?? true,
|
||||||
|
calories: (json['calories'] as num?)?.toDouble(),
|
||||||
|
proteinG: (json['proteinG'] as num?)?.toDouble(),
|
||||||
|
fatG: (json['fatG'] as num?)?.toDouble(),
|
||||||
|
saturatedFatG: (json['saturatedFatG'] as num?)?.toDouble(),
|
||||||
|
carbsG: (json['carbsG'] as num?)?.toDouble(),
|
||||||
|
fiberG: (json['fiberG'] as num?)?.toDouble(),
|
||||||
|
sugarG: (json['sugarG'] as num?)?.toDouble(),
|
||||||
|
sodiumMg: (json['sodiumMg'] as num?)?.toDouble(),
|
||||||
|
source: json['source'] as String? ?? 'auto',
|
||||||
|
);
|
||||||
|
|
||||||
|
Map<String, dynamic> _$NutritionInfoToJson(NutritionInfo instance) =>
|
||||||
|
<String, dynamic>{
|
||||||
|
'id': instance.id,
|
||||||
|
'recipeId': instance.recipeId,
|
||||||
|
'perServing': instance.perServing,
|
||||||
|
'calories': instance.calories,
|
||||||
|
'proteinG': instance.proteinG,
|
||||||
|
'fatG': instance.fatG,
|
||||||
|
'saturatedFatG': instance.saturatedFatG,
|
||||||
|
'carbsG': instance.carbsG,
|
||||||
|
'fiberG': instance.fiberG,
|
||||||
|
'sugarG': instance.sugarG,
|
||||||
|
'sodiumMg': instance.sodiumMg,
|
||||||
|
'source': instance.source,
|
||||||
|
};
|
||||||
70
lib/src/models/recipe.dart
Normal file
70
lib/src/models/recipe.dart
Normal file
@@ -0,0 +1,70 @@
|
|||||||
|
import 'package:json_annotation/json_annotation.dart';
|
||||||
|
import 'ingredient.dart';
|
||||||
|
import 'cooking_step.dart';
|
||||||
|
import 'nutrition_info.dart';
|
||||||
|
|
||||||
|
part 'recipe.g.dart';
|
||||||
|
|
||||||
|
@JsonSerializable()
|
||||||
|
class Recipe {
|
||||||
|
final String id;
|
||||||
|
final String? authorId;
|
||||||
|
final String title;
|
||||||
|
final String slug;
|
||||||
|
final String? description;
|
||||||
|
final String? cuisine;
|
||||||
|
final String? difficulty; // easy, medium, hard
|
||||||
|
final int? prepTimeMin;
|
||||||
|
final int? cookTimeMin;
|
||||||
|
final int? totalTimeMin;
|
||||||
|
final int servings;
|
||||||
|
final String sourceType; // user, ai, curated
|
||||||
|
final String status; // draft, pending, published, rejected
|
||||||
|
final String? coverImageUrl;
|
||||||
|
final String? videoUrl;
|
||||||
|
final List<String> tags;
|
||||||
|
final List<String> dietLabels;
|
||||||
|
final double ratingAvg;
|
||||||
|
final int ratingCount;
|
||||||
|
final int saveCount;
|
||||||
|
final int version;
|
||||||
|
final DateTime createdAt;
|
||||||
|
final DateTime? updatedAt;
|
||||||
|
final DateTime? publishedAt;
|
||||||
|
final List<Ingredient>? ingredients;
|
||||||
|
final List<CookingStep>? steps;
|
||||||
|
final NutritionInfo? nutrition;
|
||||||
|
|
||||||
|
const Recipe({
|
||||||
|
required this.id,
|
||||||
|
this.authorId,
|
||||||
|
required this.title,
|
||||||
|
required this.slug,
|
||||||
|
this.description,
|
||||||
|
this.cuisine,
|
||||||
|
this.difficulty,
|
||||||
|
this.prepTimeMin,
|
||||||
|
this.cookTimeMin,
|
||||||
|
this.totalTimeMin,
|
||||||
|
this.servings = 4,
|
||||||
|
this.sourceType = 'user',
|
||||||
|
this.status = 'published',
|
||||||
|
this.coverImageUrl,
|
||||||
|
this.videoUrl,
|
||||||
|
this.tags = const [],
|
||||||
|
this.dietLabels = const [],
|
||||||
|
this.ratingAvg = 0,
|
||||||
|
this.ratingCount = 0,
|
||||||
|
this.saveCount = 0,
|
||||||
|
this.version = 1,
|
||||||
|
required this.createdAt,
|
||||||
|
this.updatedAt,
|
||||||
|
this.publishedAt,
|
||||||
|
this.ingredients,
|
||||||
|
this.steps,
|
||||||
|
this.nutrition,
|
||||||
|
});
|
||||||
|
|
||||||
|
factory Recipe.fromJson(Map<String, dynamic> json) => _$RecipeFromJson(json);
|
||||||
|
Map<String, dynamic> toJson() => _$RecipeToJson(this);
|
||||||
|
}
|
||||||
88
lib/src/models/recipe.g.dart
Normal file
88
lib/src/models/recipe.g.dart
Normal file
@@ -0,0 +1,88 @@
|
|||||||
|
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||||
|
|
||||||
|
part of 'recipe.dart';
|
||||||
|
|
||||||
|
// **************************************************************************
|
||||||
|
// JsonSerializableGenerator
|
||||||
|
// **************************************************************************
|
||||||
|
|
||||||
|
Recipe _$RecipeFromJson(Map<String, dynamic> json) => Recipe(
|
||||||
|
id: json['id'] as String,
|
||||||
|
authorId: json['authorId'] as String?,
|
||||||
|
title: json['title'] as String,
|
||||||
|
slug: json['slug'] as String,
|
||||||
|
description: json['description'] as String?,
|
||||||
|
cuisine: json['cuisine'] as String?,
|
||||||
|
difficulty: json['difficulty'] as String?,
|
||||||
|
prepTimeMin: (json['prepTimeMin'] as num?)?.toInt(),
|
||||||
|
cookTimeMin: (json['cookTimeMin'] as num?)?.toInt(),
|
||||||
|
totalTimeMin: (json['totalTimeMin'] as num?)?.toInt(),
|
||||||
|
servings: (json['servings'] as num?)?.toInt() ?? 4,
|
||||||
|
sourceType: json['sourceType'] as String? ?? 'user',
|
||||||
|
status: json['status'] as String? ?? 'published',
|
||||||
|
coverImageUrl: json['coverImageUrl'] as String?,
|
||||||
|
videoUrl: json['videoUrl'] as String?,
|
||||||
|
tags:
|
||||||
|
(json['tags'] as List<dynamic>?)?.map((e) => e as String).toList() ??
|
||||||
|
const [],
|
||||||
|
dietLabels:
|
||||||
|
(json['dietLabels'] as List<dynamic>?)
|
||||||
|
?.map((e) => e as String)
|
||||||
|
.toList() ??
|
||||||
|
const [],
|
||||||
|
ratingAvg: (json['ratingAvg'] as num?)?.toDouble() ?? 0,
|
||||||
|
ratingCount: (json['ratingCount'] as num?)?.toInt() ?? 0,
|
||||||
|
saveCount: (json['saveCount'] as num?)?.toInt() ?? 0,
|
||||||
|
version: (json['version'] as num?)?.toInt() ?? 1,
|
||||||
|
createdAt: DateTime.parse(json['createdAt'] as String),
|
||||||
|
updatedAt:
|
||||||
|
json['updatedAt'] == null
|
||||||
|
? null
|
||||||
|
: DateTime.parse(json['updatedAt'] as String),
|
||||||
|
publishedAt:
|
||||||
|
json['publishedAt'] == null
|
||||||
|
? null
|
||||||
|
: DateTime.parse(json['publishedAt'] as String),
|
||||||
|
ingredients:
|
||||||
|
(json['ingredients'] as List<dynamic>?)
|
||||||
|
?.map((e) => Ingredient.fromJson(e as Map<String, dynamic>))
|
||||||
|
.toList(),
|
||||||
|
steps:
|
||||||
|
(json['steps'] as List<dynamic>?)
|
||||||
|
?.map((e) => CookingStep.fromJson(e as Map<String, dynamic>))
|
||||||
|
.toList(),
|
||||||
|
nutrition:
|
||||||
|
json['nutrition'] == null
|
||||||
|
? null
|
||||||
|
: NutritionInfo.fromJson(json['nutrition'] as Map<String, dynamic>),
|
||||||
|
);
|
||||||
|
|
||||||
|
Map<String, dynamic> _$RecipeToJson(Recipe instance) => <String, dynamic>{
|
||||||
|
'id': instance.id,
|
||||||
|
'authorId': instance.authorId,
|
||||||
|
'title': instance.title,
|
||||||
|
'slug': instance.slug,
|
||||||
|
'description': instance.description,
|
||||||
|
'cuisine': instance.cuisine,
|
||||||
|
'difficulty': instance.difficulty,
|
||||||
|
'prepTimeMin': instance.prepTimeMin,
|
||||||
|
'cookTimeMin': instance.cookTimeMin,
|
||||||
|
'totalTimeMin': instance.totalTimeMin,
|
||||||
|
'servings': instance.servings,
|
||||||
|
'sourceType': instance.sourceType,
|
||||||
|
'status': instance.status,
|
||||||
|
'coverImageUrl': instance.coverImageUrl,
|
||||||
|
'videoUrl': instance.videoUrl,
|
||||||
|
'tags': instance.tags,
|
||||||
|
'dietLabels': instance.dietLabels,
|
||||||
|
'ratingAvg': instance.ratingAvg,
|
||||||
|
'ratingCount': instance.ratingCount,
|
||||||
|
'saveCount': instance.saveCount,
|
||||||
|
'version': instance.version,
|
||||||
|
'createdAt': instance.createdAt.toIso8601String(),
|
||||||
|
'updatedAt': instance.updatedAt?.toIso8601String(),
|
||||||
|
'publishedAt': instance.publishedAt?.toIso8601String(),
|
||||||
|
'ingredients': instance.ingredients,
|
||||||
|
'steps': instance.steps,
|
||||||
|
'nutrition': instance.nutrition,
|
||||||
|
};
|
||||||
33
lib/src/models/review.dart
Normal file
33
lib/src/models/review.dart
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
import 'package:json_annotation/json_annotation.dart';
|
||||||
|
|
||||||
|
part 'review.g.dart';
|
||||||
|
|
||||||
|
@JsonSerializable()
|
||||||
|
class Review {
|
||||||
|
final String id;
|
||||||
|
final String recipeId;
|
||||||
|
final String userId;
|
||||||
|
final String? userDisplayName;
|
||||||
|
final int rating; // 1-5
|
||||||
|
final String? title;
|
||||||
|
final String? body;
|
||||||
|
final int helpfulCount;
|
||||||
|
final DateTime createdAt;
|
||||||
|
final DateTime? updatedAt;
|
||||||
|
|
||||||
|
const Review({
|
||||||
|
required this.id,
|
||||||
|
required this.recipeId,
|
||||||
|
required this.userId,
|
||||||
|
this.userDisplayName,
|
||||||
|
required this.rating,
|
||||||
|
this.title,
|
||||||
|
this.body,
|
||||||
|
this.helpfulCount = 0,
|
||||||
|
required this.createdAt,
|
||||||
|
this.updatedAt,
|
||||||
|
});
|
||||||
|
|
||||||
|
factory Review.fromJson(Map<String, dynamic> json) => _$ReviewFromJson(json);
|
||||||
|
Map<String, dynamic> toJson() => _$ReviewToJson(this);
|
||||||
|
}
|
||||||
36
lib/src/models/review.g.dart
Normal file
36
lib/src/models/review.g.dart
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||||
|
|
||||||
|
part of 'review.dart';
|
||||||
|
|
||||||
|
// **************************************************************************
|
||||||
|
// JsonSerializableGenerator
|
||||||
|
// **************************************************************************
|
||||||
|
|
||||||
|
Review _$ReviewFromJson(Map<String, dynamic> json) => Review(
|
||||||
|
id: json['id'] as String,
|
||||||
|
recipeId: json['recipeId'] as String,
|
||||||
|
userId: json['userId'] as String,
|
||||||
|
userDisplayName: json['userDisplayName'] as String?,
|
||||||
|
rating: (json['rating'] as num).toInt(),
|
||||||
|
title: json['title'] as String?,
|
||||||
|
body: json['body'] as String?,
|
||||||
|
helpfulCount: (json['helpfulCount'] as num?)?.toInt() ?? 0,
|
||||||
|
createdAt: DateTime.parse(json['createdAt'] as String),
|
||||||
|
updatedAt:
|
||||||
|
json['updatedAt'] == null
|
||||||
|
? null
|
||||||
|
: DateTime.parse(json['updatedAt'] as String),
|
||||||
|
);
|
||||||
|
|
||||||
|
Map<String, dynamic> _$ReviewToJson(Review instance) => <String, dynamic>{
|
||||||
|
'id': instance.id,
|
||||||
|
'recipeId': instance.recipeId,
|
||||||
|
'userId': instance.userId,
|
||||||
|
'userDisplayName': instance.userDisplayName,
|
||||||
|
'rating': instance.rating,
|
||||||
|
'title': instance.title,
|
||||||
|
'body': instance.body,
|
||||||
|
'helpfulCount': instance.helpfulCount,
|
||||||
|
'createdAt': instance.createdAt.toIso8601String(),
|
||||||
|
'updatedAt': instance.updatedAt?.toIso8601String(),
|
||||||
|
};
|
||||||
29
lib/src/models/user.dart
Normal file
29
lib/src/models/user.dart
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
import 'package:json_annotation/json_annotation.dart';
|
||||||
|
|
||||||
|
part 'user.g.dart';
|
||||||
|
|
||||||
|
@JsonSerializable()
|
||||||
|
class User {
|
||||||
|
final String id;
|
||||||
|
final String email;
|
||||||
|
final String displayName;
|
||||||
|
final String? avatarUrl;
|
||||||
|
final String subscriptionTier; // free, premium
|
||||||
|
final DateTime? subscriptionExpiresAt;
|
||||||
|
final DateTime createdAt;
|
||||||
|
final DateTime? updatedAt;
|
||||||
|
|
||||||
|
const User({
|
||||||
|
required this.id,
|
||||||
|
required this.email,
|
||||||
|
required this.displayName,
|
||||||
|
this.avatarUrl,
|
||||||
|
this.subscriptionTier = 'free',
|
||||||
|
this.subscriptionExpiresAt,
|
||||||
|
required this.createdAt,
|
||||||
|
this.updatedAt,
|
||||||
|
});
|
||||||
|
|
||||||
|
factory User.fromJson(Map<String, dynamic> json) => _$UserFromJson(json);
|
||||||
|
Map<String, dynamic> toJson() => _$UserToJson(this);
|
||||||
|
}
|
||||||
35
lib/src/models/user.g.dart
Normal file
35
lib/src/models/user.g.dart
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||||
|
|
||||||
|
part of 'user.dart';
|
||||||
|
|
||||||
|
// **************************************************************************
|
||||||
|
// JsonSerializableGenerator
|
||||||
|
// **************************************************************************
|
||||||
|
|
||||||
|
User _$UserFromJson(Map<String, dynamic> json) => User(
|
||||||
|
id: json['id'] as String,
|
||||||
|
email: json['email'] as String,
|
||||||
|
displayName: json['displayName'] as String,
|
||||||
|
avatarUrl: json['avatarUrl'] as String?,
|
||||||
|
subscriptionTier: json['subscriptionTier'] as String? ?? 'free',
|
||||||
|
subscriptionExpiresAt:
|
||||||
|
json['subscriptionExpiresAt'] == null
|
||||||
|
? null
|
||||||
|
: DateTime.parse(json['subscriptionExpiresAt'] as String),
|
||||||
|
createdAt: DateTime.parse(json['createdAt'] as String),
|
||||||
|
updatedAt:
|
||||||
|
json['updatedAt'] == null
|
||||||
|
? null
|
||||||
|
: DateTime.parse(json['updatedAt'] as String),
|
||||||
|
);
|
||||||
|
|
||||||
|
Map<String, dynamic> _$UserToJson(User instance) => <String, dynamic>{
|
||||||
|
'id': instance.id,
|
||||||
|
'email': instance.email,
|
||||||
|
'displayName': instance.displayName,
|
||||||
|
'avatarUrl': instance.avatarUrl,
|
||||||
|
'subscriptionTier': instance.subscriptionTier,
|
||||||
|
'subscriptionExpiresAt': instance.subscriptionExpiresAt?.toIso8601String(),
|
||||||
|
'createdAt': instance.createdAt.toIso8601String(),
|
||||||
|
'updatedAt': instance.updatedAt?.toIso8601String(),
|
||||||
|
};
|
||||||
37
lib/src/validators/recipe_validator.dart
Normal file
37
lib/src/validators/recipe_validator.dart
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
import '../constants/app_limits.dart';
|
||||||
|
|
||||||
|
class RecipeValidator {
|
||||||
|
RecipeValidator._();
|
||||||
|
|
||||||
|
static String? validateTitle(String? title) {
|
||||||
|
if (title == null || title.trim().isEmpty) return 'Title is required';
|
||||||
|
if (title.length > AppLimits.maxRecipeTitleLength) {
|
||||||
|
return 'Title must be ${AppLimits.maxRecipeTitleLength} characters or less';
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
static String? validateServings(int? servings) {
|
||||||
|
if (servings == null || servings < 1) return 'Servings must be at least 1';
|
||||||
|
if (servings > 100) return 'Servings must be 100 or less';
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
static String? validateRating(int? rating) {
|
||||||
|
if (rating == null) return 'Rating is required';
|
||||||
|
if (rating < 1 || rating > 5) return 'Rating must be between 1 and 5';
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
static String? validatePrepTime(int? minutes) {
|
||||||
|
if (minutes != null && minutes < 0) return 'Prep time cannot be negative';
|
||||||
|
if (minutes != null && minutes > 1440) return 'Prep time cannot exceed 24 hours';
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
static String? validateCookTime(int? minutes) {
|
||||||
|
if (minutes != null && minutes < 0) return 'Cook time cannot be negative';
|
||||||
|
if (minutes != null && minutes > 2880) return 'Cook time cannot exceed 48 hours';
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
19
pubspec.yaml
Normal file
19
pubspec.yaml
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
name: cleanplate_shared
|
||||||
|
description: Shared models, enums, and utilities for CleanPlate recipe app
|
||||||
|
version: 0.1.0
|
||||||
|
publish_to: 'none'
|
||||||
|
|
||||||
|
environment:
|
||||||
|
sdk: ^3.7.0
|
||||||
|
|
||||||
|
dependencies:
|
||||||
|
json_annotation: ^4.9.0
|
||||||
|
freezed_annotation: ^2.4.0
|
||||||
|
equatable: ^2.0.7
|
||||||
|
|
||||||
|
dev_dependencies:
|
||||||
|
build_runner: ^2.4.0
|
||||||
|
json_serializable: ^6.8.0
|
||||||
|
freezed: ^2.5.0
|
||||||
|
lints: ^5.0.0
|
||||||
|
test: ^1.24.0
|
||||||
46
test/cleanplate_shared_test.dart
Normal file
46
test/cleanplate_shared_test.dart
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
import 'package:cleanplate_shared/cleanplate_shared.dart';
|
||||||
|
import 'package:test/test.dart';
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
group('RecipeValidator', () {
|
||||||
|
test('validateTitle returns null for valid title', () {
|
||||||
|
expect(RecipeValidator.validateTitle('My Recipe'), isNull);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('validateTitle returns error for empty title', () {
|
||||||
|
expect(RecipeValidator.validateTitle(''), isNotNull);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('validateServings returns null for valid servings', () {
|
||||||
|
expect(RecipeValidator.validateServings(4), isNull);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('validateServings returns error for zero servings', () {
|
||||||
|
expect(RecipeValidator.validateServings(0), isNotNull);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('validateRating returns null for valid rating', () {
|
||||||
|
expect(RecipeValidator.validateRating(5), isNull);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('validateRating returns error for out-of-range rating', () {
|
||||||
|
expect(RecipeValidator.validateRating(6), isNotNull);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
group('DietType', () {
|
||||||
|
test('displayName returns correct value', () {
|
||||||
|
expect(DietType.vegan.displayName, equals('Vegan'));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
group('ApiConstants', () {
|
||||||
|
test('basePath includes version', () {
|
||||||
|
expect(ApiConstants.basePath, equals('/api/v1'));
|
||||||
|
});
|
||||||
|
|
||||||
|
test('recipe path includes id', () {
|
||||||
|
expect(ApiConstants.recipe('abc'), equals('/api/v1/recipes/abc'));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user