create-mobile-arch 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +180 -0
- package/bin/index.js +144 -0
- package/package.json +43 -0
- package/src/generator.js +229 -0
- package/src/prompts.js +140 -0
- package/src/utils.js +185 -0
- package/templates/flutter-clean/analysis_options.yaml +46 -0
- package/templates/flutter-clean/lib/core/constants/app_constants.dart +26 -0
- package/templates/flutter-clean/lib/core/theme/app_theme.dart +90 -0
- package/templates/flutter-clean/lib/core/utils/app_utils.dart +29 -0
- package/templates/flutter-clean/lib/data/datasources/.gitkeep +0 -0
- package/templates/flutter-clean/lib/data/models/user_model.dart +57 -0
- package/templates/flutter-clean/lib/data/repositories/.gitkeep +0 -0
- package/templates/flutter-clean/lib/domain/entities/user.dart +23 -0
- package/templates/flutter-clean/lib/domain/repositories/.gitkeep +0 -0
- package/templates/flutter-clean/lib/domain/usecases/.gitkeep +0 -0
- package/templates/flutter-clean/lib/main.dart +33 -0
- package/templates/flutter-clean/lib/presentation/providers/.gitkeep +0 -0
- package/templates/flutter-clean/lib/presentation/screens/home_screen.dart +106 -0
- package/templates/flutter-clean/lib/presentation/widgets/.gitkeep +0 -0
- package/templates/flutter-clean/pubspec.yaml +69 -0
package/src/prompts.js
ADDED
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
const inquirer = require('inquirer');
|
|
2
|
+
const chalk = require('chalk');
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Prompts the user for project configuration choices
|
|
6
|
+
* @returns {Promise<Object>} User's answers
|
|
7
|
+
*/
|
|
8
|
+
async function promptUserChoices() {
|
|
9
|
+
console.log('\n');
|
|
10
|
+
console.log(chalk.cyan.bold('⚙️ Configure your Flutter project'));
|
|
11
|
+
console.log(chalk.gray('Answer the following questions to customize your project setup:'));
|
|
12
|
+
console.log('\n');
|
|
13
|
+
|
|
14
|
+
const questions = [
|
|
15
|
+
{
|
|
16
|
+
type: 'list',
|
|
17
|
+
name: 'architecture',
|
|
18
|
+
message: 'Select your preferred architecture pattern:',
|
|
19
|
+
choices: [
|
|
20
|
+
{
|
|
21
|
+
name: 'Clean Architecture (Domain-driven with layers)',
|
|
22
|
+
value: 'clean',
|
|
23
|
+
short: 'Clean Architecture'
|
|
24
|
+
},
|
|
25
|
+
{
|
|
26
|
+
name: 'Feature First (Organized by features)',
|
|
27
|
+
value: 'feature-first',
|
|
28
|
+
short: 'Feature First'
|
|
29
|
+
}
|
|
30
|
+
],
|
|
31
|
+
default: 'clean'
|
|
32
|
+
},
|
|
33
|
+
{
|
|
34
|
+
type: 'list',
|
|
35
|
+
name: 'stateManagement',
|
|
36
|
+
message: 'Choose your state management solution:',
|
|
37
|
+
choices: [
|
|
38
|
+
{
|
|
39
|
+
name: 'Riverpod (Recommended - compile-safe, testable)',
|
|
40
|
+
value: 'riverpod',
|
|
41
|
+
short: 'Riverpod'
|
|
42
|
+
},
|
|
43
|
+
{
|
|
44
|
+
name: 'Bloc (Event-driven state management)',
|
|
45
|
+
value: 'bloc',
|
|
46
|
+
short: 'Bloc'
|
|
47
|
+
}
|
|
48
|
+
],
|
|
49
|
+
default: 'riverpod'
|
|
50
|
+
},
|
|
51
|
+
{
|
|
52
|
+
type: 'list',
|
|
53
|
+
name: 'backend',
|
|
54
|
+
message: 'Select your backend integration:',
|
|
55
|
+
choices: [
|
|
56
|
+
{
|
|
57
|
+
name: 'Firebase (Auth, Firestore, Storage)',
|
|
58
|
+
value: 'firebase',
|
|
59
|
+
short: 'Firebase'
|
|
60
|
+
},
|
|
61
|
+
{
|
|
62
|
+
name: 'REST API (Custom backend with HTTP)',
|
|
63
|
+
value: 'rest',
|
|
64
|
+
short: 'REST API'
|
|
65
|
+
}
|
|
66
|
+
],
|
|
67
|
+
default: 'rest'
|
|
68
|
+
},
|
|
69
|
+
{
|
|
70
|
+
type: 'confirm',
|
|
71
|
+
name: 'includeExamples',
|
|
72
|
+
message: 'Include example code and sample features?',
|
|
73
|
+
default: true
|
|
74
|
+
}
|
|
75
|
+
];
|
|
76
|
+
|
|
77
|
+
try {
|
|
78
|
+
const answers = await inquirer.prompt(questions);
|
|
79
|
+
return answers;
|
|
80
|
+
} catch (error) {
|
|
81
|
+
if (error.isTtyError) {
|
|
82
|
+
throw new Error('Prompt could not be rendered in this environment');
|
|
83
|
+
} else {
|
|
84
|
+
throw new Error(`Failed to get user input: ${error.message}`);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Confirms before overwriting an existing directory
|
|
91
|
+
* @param {string} projectName - Name of the existing project
|
|
92
|
+
* @returns {Promise<boolean>} User's confirmation
|
|
93
|
+
*/
|
|
94
|
+
async function confirmOverwrite(projectName) {
|
|
95
|
+
console.log('\n');
|
|
96
|
+
console.log(chalk.yellow.bold('⚠️ Warning:'), `Directory "${projectName}" already exists!`);
|
|
97
|
+
|
|
98
|
+
const { confirm } = await inquirer.prompt([
|
|
99
|
+
{
|
|
100
|
+
type: 'confirm',
|
|
101
|
+
name: 'confirm',
|
|
102
|
+
message: 'Do you want to overwrite it?',
|
|
103
|
+
default: false
|
|
104
|
+
}
|
|
105
|
+
]);
|
|
106
|
+
|
|
107
|
+
return confirm;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Displays a summary of user choices before generation
|
|
112
|
+
* @param {string} projectName - Project name
|
|
113
|
+
* @param {Object} choices - User's configuration choices
|
|
114
|
+
*/
|
|
115
|
+
function displayConfigSummary(projectName, choices) {
|
|
116
|
+
console.log('\n');
|
|
117
|
+
console.log(chalk.cyan.bold('📋 Project Configuration Summary:'));
|
|
118
|
+
console.log(chalk.gray('─'.repeat(50)));
|
|
119
|
+
console.log(chalk.white(' Project Name: '), chalk.green(projectName));
|
|
120
|
+
console.log(chalk.white(' Architecture: '), chalk.green(
|
|
121
|
+
choices.architecture === 'clean' ? 'Clean Architecture' : 'Feature First'
|
|
122
|
+
));
|
|
123
|
+
console.log(chalk.white(' State Management: '), chalk.green(
|
|
124
|
+
choices.stateManagement === 'riverpod' ? 'Riverpod' : 'Bloc'
|
|
125
|
+
));
|
|
126
|
+
console.log(chalk.white(' Backend: '), chalk.green(
|
|
127
|
+
choices.backend === 'firebase' ? 'Firebase' : 'REST API'
|
|
128
|
+
));
|
|
129
|
+
console.log(chalk.white(' Include Examples: '), chalk.green(
|
|
130
|
+
choices.includeExamples ? 'Yes' : 'No'
|
|
131
|
+
));
|
|
132
|
+
console.log(chalk.gray('─'.repeat(50)));
|
|
133
|
+
console.log('\n');
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
module.exports = {
|
|
137
|
+
promptUserChoices,
|
|
138
|
+
confirmOverwrite,
|
|
139
|
+
displayConfigSummary
|
|
140
|
+
};
|
package/src/utils.js
ADDED
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
const fs = require('fs-extra');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
const chalk = require('chalk');
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Validates the project name according to Flutter naming conventions
|
|
7
|
+
* @param {string} name - Project name to validate
|
|
8
|
+
* @returns {Object} - { valid: boolean, error: string }
|
|
9
|
+
*/
|
|
10
|
+
function validateProjectName(name) {
|
|
11
|
+
if (!name) {
|
|
12
|
+
return {
|
|
13
|
+
valid: false,
|
|
14
|
+
error: 'Project name is required'
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
// Flutter project names must be valid Dart package names
|
|
19
|
+
// Must be lowercase, can contain underscores and numbers, but not start with a number
|
|
20
|
+
const validNamePattern = /^[a-z][a-z0-9_]*$/;
|
|
21
|
+
|
|
22
|
+
if (!validNamePattern.test(name)) {
|
|
23
|
+
return {
|
|
24
|
+
valid: false,
|
|
25
|
+
error: 'Project name must start with a lowercase letter and can only contain lowercase letters, numbers, and underscores'
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// Reserved Dart keywords
|
|
30
|
+
const reservedWords = [
|
|
31
|
+
'abstract', 'as', 'assert', 'async', 'await', 'break', 'case', 'catch',
|
|
32
|
+
'class', 'const', 'continue', 'default', 'do', 'else', 'enum', 'export',
|
|
33
|
+
'extends', 'false', 'final', 'finally', 'for', 'if', 'import', 'in',
|
|
34
|
+
'is', 'library', 'new', 'null', 'operator', 'part', 'return', 'super',
|
|
35
|
+
'switch', 'this', 'throw', 'true', 'try', 'var', 'void', 'while', 'with'
|
|
36
|
+
];
|
|
37
|
+
|
|
38
|
+
if (reservedWords.includes(name)) {
|
|
39
|
+
return {
|
|
40
|
+
valid: false,
|
|
41
|
+
error: `Project name "${name}" is a reserved Dart keyword`
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
return { valid: true };
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Checks if a directory exists
|
|
50
|
+
* @param {string} dirPath - Directory path to check
|
|
51
|
+
* @returns {boolean}
|
|
52
|
+
*/
|
|
53
|
+
async function directoryExists(dirPath) {
|
|
54
|
+
try {
|
|
55
|
+
const stats = await fs.stat(dirPath);
|
|
56
|
+
return stats.isDirectory();
|
|
57
|
+
} catch (error) {
|
|
58
|
+
return false;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Converts a string to PascalCase (for app names)
|
|
64
|
+
* @param {string} str - String to convert
|
|
65
|
+
* @returns {string}
|
|
66
|
+
*/
|
|
67
|
+
function toPascalCase(str) {
|
|
68
|
+
return str
|
|
69
|
+
.split('_')
|
|
70
|
+
.map(word => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase())
|
|
71
|
+
.join('');
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Converts a string to snake_case
|
|
76
|
+
* @param {string} str - String to convert
|
|
77
|
+
* @returns {string}
|
|
78
|
+
*/
|
|
79
|
+
function toSnakeCase(str) {
|
|
80
|
+
return str
|
|
81
|
+
.toLowerCase()
|
|
82
|
+
.replace(/[^a-z0-9]/g, '_')
|
|
83
|
+
.replace(/_+/g, '_')
|
|
84
|
+
.replace(/^_|_$/g, '');
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Recursively copies directory and replaces placeholders in file contents
|
|
89
|
+
* @param {string} srcDir - Source directory
|
|
90
|
+
* @param {string} destDir - Destination directory
|
|
91
|
+
* @param {Object} replacements - Key-value pairs for placeholder replacement
|
|
92
|
+
*/
|
|
93
|
+
async function copyAndReplace(srcDir, destDir, replacements) {
|
|
94
|
+
try {
|
|
95
|
+
// Ensure destination directory exists
|
|
96
|
+
await fs.ensureDir(destDir);
|
|
97
|
+
|
|
98
|
+
// Read all items in source directory
|
|
99
|
+
const items = await fs.readdir(srcDir);
|
|
100
|
+
|
|
101
|
+
for (const item of items) {
|
|
102
|
+
const srcPath = path.join(srcDir, item);
|
|
103
|
+
const destPath = path.join(destDir, item);
|
|
104
|
+
|
|
105
|
+
const stats = await fs.stat(srcPath);
|
|
106
|
+
|
|
107
|
+
if (stats.isDirectory()) {
|
|
108
|
+
// Recursively copy subdirectories
|
|
109
|
+
await copyAndReplace(srcPath, destPath, replacements);
|
|
110
|
+
} else if (stats.isFile()) {
|
|
111
|
+
// Read file content
|
|
112
|
+
let content = await fs.readFile(srcPath, 'utf8');
|
|
113
|
+
|
|
114
|
+
// Replace all placeholders
|
|
115
|
+
Object.keys(replacements).forEach(placeholder => {
|
|
116
|
+
const regex = new RegExp(placeholder, 'g');
|
|
117
|
+
content = content.replace(regex, replacements[placeholder]);
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
// Write to destination
|
|
121
|
+
await fs.writeFile(destPath, content, 'utf8');
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
} catch (error) {
|
|
125
|
+
throw new Error(`Failed to copy template: ${error.message}`);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Checks if Flutter is installed on the system
|
|
131
|
+
* @returns {boolean}
|
|
132
|
+
*/
|
|
133
|
+
async function isFlutterInstalled() {
|
|
134
|
+
const { exec } = require('child_process');
|
|
135
|
+
const util = require('util');
|
|
136
|
+
const execPromise = util.promisify(exec);
|
|
137
|
+
|
|
138
|
+
try {
|
|
139
|
+
await execPromise('flutter --version');
|
|
140
|
+
return true;
|
|
141
|
+
} catch (error) {
|
|
142
|
+
return false;
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Prints a success message with next steps
|
|
148
|
+
* @param {string} projectName - Name of the created project
|
|
149
|
+
* @param {string} projectPath - Path to the project
|
|
150
|
+
*/
|
|
151
|
+
function printSuccessMessage(projectName, projectPath) {
|
|
152
|
+
console.log('\n');
|
|
153
|
+
console.log(chalk.green.bold('✅ Success!'), `Your Flutter project "${projectName}" has been created!`);
|
|
154
|
+
console.log('\n');
|
|
155
|
+
console.log(chalk.cyan('📁 Project location:'), chalk.white(projectPath));
|
|
156
|
+
console.log('\n');
|
|
157
|
+
console.log(chalk.yellow.bold('Next steps:'));
|
|
158
|
+
console.log(chalk.white(' 1.'), `cd ${projectName}`);
|
|
159
|
+
console.log(chalk.white(' 2.'), 'Open the project in your IDE (VS Code, Android Studio, etc.)');
|
|
160
|
+
console.log(chalk.white(' 3.'), 'Run:', chalk.cyan('flutter run'));
|
|
161
|
+
console.log('\n');
|
|
162
|
+
console.log(chalk.gray('💡 Tip: Check the README.md file in your project for architecture details'));
|
|
163
|
+
console.log('\n');
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* Prints an error message
|
|
168
|
+
* @param {string} message - Error message
|
|
169
|
+
*/
|
|
170
|
+
function printError(message) {
|
|
171
|
+
console.log('\n');
|
|
172
|
+
console.log(chalk.red.bold('❌ Error:'), chalk.white(message));
|
|
173
|
+
console.log('\n');
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
module.exports = {
|
|
177
|
+
validateProjectName,
|
|
178
|
+
directoryExists,
|
|
179
|
+
toPascalCase,
|
|
180
|
+
toSnakeCase,
|
|
181
|
+
copyAndReplace,
|
|
182
|
+
isFlutterInstalled,
|
|
183
|
+
printSuccessMessage,
|
|
184
|
+
printError
|
|
185
|
+
};
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
include: package:flutter_lints/flutter.yaml
|
|
2
|
+
|
|
3
|
+
linter:
|
|
4
|
+
rules:
|
|
5
|
+
# Error rules
|
|
6
|
+
- avoid_empty_else
|
|
7
|
+
- avoid_relative_lib_imports
|
|
8
|
+
- avoid_types_as_parameter_names
|
|
9
|
+
- no_duplicate_case_values
|
|
10
|
+
- prefer_void_to_null
|
|
11
|
+
- valid_regexps
|
|
12
|
+
|
|
13
|
+
# Style rules
|
|
14
|
+
- always_declare_return_types
|
|
15
|
+
- always_require_non_null_named_parameters
|
|
16
|
+
- annotate_overrides
|
|
17
|
+
- avoid_init_to_null
|
|
18
|
+
- avoid_return_types_on_setters
|
|
19
|
+
- camel_case_extensions
|
|
20
|
+
- camel_case_types
|
|
21
|
+
- constant_identifier_names
|
|
22
|
+
- curly_braces_in_flow_control_structures
|
|
23
|
+
- empty_catches
|
|
24
|
+
- empty_constructor_bodies
|
|
25
|
+
- library_names
|
|
26
|
+
- library_prefixes
|
|
27
|
+
- prefer_contains
|
|
28
|
+
- prefer_final_fields
|
|
29
|
+
- prefer_is_empty
|
|
30
|
+
- prefer_is_not_empty
|
|
31
|
+
- prefer_single_quotes
|
|
32
|
+
- slash_for_doc_comments
|
|
33
|
+
- unnecessary_const
|
|
34
|
+
- unnecessary_new
|
|
35
|
+
- unnecessary_this
|
|
36
|
+
- use_rethrow_when_possible
|
|
37
|
+
|
|
38
|
+
analyzer:
|
|
39
|
+
exclude:
|
|
40
|
+
- "**/*.g.dart"
|
|
41
|
+
- "**/*.freezed.dart"
|
|
42
|
+
- "**/*.config.dart"
|
|
43
|
+
- "build/**"
|
|
44
|
+
|
|
45
|
+
errors:
|
|
46
|
+
invalid_annotation_target: ignore
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/// Application-wide constants
|
|
2
|
+
class AppConstants {
|
|
3
|
+
// Private constructor to prevent instantiation
|
|
4
|
+
AppConstants._();
|
|
5
|
+
|
|
6
|
+
/// App Information
|
|
7
|
+
static const String appName = '__APP_NAME_PASCAL__';
|
|
8
|
+
static const String appVersion = '1.0.0';
|
|
9
|
+
|
|
10
|
+
/// API Configuration
|
|
11
|
+
static const String baseUrl = 'https://api.example.com';
|
|
12
|
+
static const String apiVersion = 'v1';
|
|
13
|
+
static const Duration apiTimeout = Duration(seconds: 30);
|
|
14
|
+
|
|
15
|
+
/// Storage Keys
|
|
16
|
+
static const String tokenKey = 'auth_token';
|
|
17
|
+
static const String userKey = 'user_data';
|
|
18
|
+
static const String themeKey = 'theme_mode';
|
|
19
|
+
|
|
20
|
+
/// Pagination
|
|
21
|
+
static const int defaultPageSize = 20;
|
|
22
|
+
|
|
23
|
+
/// Validation
|
|
24
|
+
static const int minPasswordLength = 8;
|
|
25
|
+
static const int maxUsernameLength = 30;
|
|
26
|
+
}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import 'package:flutter/material.dart';
|
|
2
|
+
|
|
3
|
+
/// Application theme configuration
|
|
4
|
+
/// Defines light and dark theme for the app
|
|
5
|
+
class AppTheme {
|
|
6
|
+
// Private constructor to prevent instantiation
|
|
7
|
+
AppTheme._();
|
|
8
|
+
|
|
9
|
+
// Color palette
|
|
10
|
+
static const Color primaryColor = Color(0xFF2196F3);
|
|
11
|
+
static const Color secondaryColor = Color(0xFF03DAC6);
|
|
12
|
+
static const Color errorColor = Color(0xFFB00020);
|
|
13
|
+
static const Color backgroundColor = Color(0xFFF5F5F5);
|
|
14
|
+
|
|
15
|
+
/// Light theme configuration
|
|
16
|
+
static ThemeData get lightTheme {
|
|
17
|
+
return ThemeData(
|
|
18
|
+
useMaterial3: true,
|
|
19
|
+
brightness: Brightness.light,
|
|
20
|
+
primaryColor: primaryColor,
|
|
21
|
+
scaffoldBackgroundColor: backgroundColor,
|
|
22
|
+
colorScheme: const ColorScheme.light(
|
|
23
|
+
primary: primaryColor,
|
|
24
|
+
secondary: secondaryColor,
|
|
25
|
+
error: errorColor,
|
|
26
|
+
background: backgroundColor,
|
|
27
|
+
),
|
|
28
|
+
appBarTheme: const AppBarTheme(
|
|
29
|
+
centerTitle: true,
|
|
30
|
+
elevation: 0,
|
|
31
|
+
backgroundColor: primaryColor,
|
|
32
|
+
foregroundColor: Colors.white,
|
|
33
|
+
),
|
|
34
|
+
elevatedButtonTheme: ElevatedButtonThemeData(
|
|
35
|
+
style: ElevatedButton.styleFrom(
|
|
36
|
+
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 12),
|
|
37
|
+
shape: RoundedRectangleBorder(
|
|
38
|
+
borderRadius: BorderRadius.circular(8),
|
|
39
|
+
),
|
|
40
|
+
),
|
|
41
|
+
),
|
|
42
|
+
inputDecorationTheme: InputDecorationTheme(
|
|
43
|
+
border: OutlineInputBorder(
|
|
44
|
+
borderRadius: BorderRadius.circular(8),
|
|
45
|
+
),
|
|
46
|
+
contentPadding: const EdgeInsets.symmetric(
|
|
47
|
+
horizontal: 16,
|
|
48
|
+
vertical: 12,
|
|
49
|
+
),
|
|
50
|
+
),
|
|
51
|
+
);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/// Dark theme configuration
|
|
55
|
+
static ThemeData get darkTheme {
|
|
56
|
+
return ThemeData(
|
|
57
|
+
useMaterial3: true,
|
|
58
|
+
brightness: Brightness.dark,
|
|
59
|
+
primaryColor: primaryColor,
|
|
60
|
+
scaffoldBackgroundColor: const Color(0xFF121212),
|
|
61
|
+
colorScheme: const ColorScheme.dark(
|
|
62
|
+
primary: primaryColor,
|
|
63
|
+
secondary: secondaryColor,
|
|
64
|
+
error: errorColor,
|
|
65
|
+
),
|
|
66
|
+
appBarTheme: const AppBarTheme(
|
|
67
|
+
centerTitle: true,
|
|
68
|
+
elevation: 0,
|
|
69
|
+
backgroundColor: Color(0xFF1F1F1F),
|
|
70
|
+
),
|
|
71
|
+
elevatedButtonTheme: ElevatedButtonThemeData(
|
|
72
|
+
style: ElevatedButton.styleFrom(
|
|
73
|
+
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 12),
|
|
74
|
+
shape: RoundedRectangleBorder(
|
|
75
|
+
borderRadius: BorderRadius.circular(8),
|
|
76
|
+
),
|
|
77
|
+
),
|
|
78
|
+
),
|
|
79
|
+
inputDecorationTheme: InputDecorationTheme(
|
|
80
|
+
border: OutlineInputBorder(
|
|
81
|
+
borderRadius: BorderRadius.circular(8),
|
|
82
|
+
),
|
|
83
|
+
contentPadding: const EdgeInsets.symmetric(
|
|
84
|
+
horizontal: 16,
|
|
85
|
+
vertical: 12,
|
|
86
|
+
),
|
|
87
|
+
),
|
|
88
|
+
);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/// Common utility functions for the application
|
|
2
|
+
class AppUtils {
|
|
3
|
+
// Private constructor to prevent instantiation
|
|
4
|
+
AppUtils._();
|
|
5
|
+
|
|
6
|
+
/// Validates email format
|
|
7
|
+
static bool isValidEmail(String email) {
|
|
8
|
+
final emailRegex = RegExp(
|
|
9
|
+
r'^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$',
|
|
10
|
+
);
|
|
11
|
+
return emailRegex.hasMatch(email);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/// Validates password strength
|
|
15
|
+
static bool isValidPassword(String password) {
|
|
16
|
+
return password.length >= 8;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/// Formats date to readable string
|
|
20
|
+
static String formatDate(DateTime date) {
|
|
21
|
+
return '${date.day}/${date.month}/${date.year}';
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/// Capitalizes first letter of string
|
|
25
|
+
static String capitalize(String text) {
|
|
26
|
+
if (text.isEmpty) return text;
|
|
27
|
+
return text[0].toUpperCase() + text.substring(1);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
File without changes
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import '../domain/entities/user.dart';
|
|
2
|
+
|
|
3
|
+
/// Data model for User
|
|
4
|
+
/// Models are responsible for JSON serialization/deserialization
|
|
5
|
+
class UserModel {
|
|
6
|
+
final String id;
|
|
7
|
+
final String name;
|
|
8
|
+
final String email;
|
|
9
|
+
final String createdAt;
|
|
10
|
+
|
|
11
|
+
UserModel({
|
|
12
|
+
required this.id,
|
|
13
|
+
required this.name,
|
|
14
|
+
required this.email,
|
|
15
|
+
required this.createdAt,
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
/// Convert JSON to UserModel
|
|
19
|
+
factory UserModel.fromJson(Map<String, dynamic> json) {
|
|
20
|
+
return UserModel(
|
|
21
|
+
id: json['id'] as String,
|
|
22
|
+
name: json['name'] as String,
|
|
23
|
+
email: json['email'] as String,
|
|
24
|
+
createdAt: json['created_at'] as String,
|
|
25
|
+
);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/// Convert UserModel to JSON
|
|
29
|
+
Map<String, dynamic> toJson() {
|
|
30
|
+
return {
|
|
31
|
+
'id': id,
|
|
32
|
+
'name': name,
|
|
33
|
+
'email': email,
|
|
34
|
+
'created_at': createdAt,
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/// Convert UserModel to domain entity
|
|
39
|
+
User toEntity() {
|
|
40
|
+
return User(
|
|
41
|
+
id: id,
|
|
42
|
+
name: name,
|
|
43
|
+
email: email,
|
|
44
|
+
createdAt: DateTime.parse(createdAt),
|
|
45
|
+
);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/// Create UserModel from domain entity
|
|
49
|
+
factory UserModel.fromEntity(User user) {
|
|
50
|
+
return UserModel(
|
|
51
|
+
id: user.id,
|
|
52
|
+
name: user.name,
|
|
53
|
+
email: user.email,
|
|
54
|
+
createdAt: user.createdAt.toIso8601String(),
|
|
55
|
+
);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
File without changes
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import 'package:equatable/equatable.dart';
|
|
2
|
+
|
|
3
|
+
/// Example entity representing a user in the domain layer
|
|
4
|
+
/// Entities are plain Dart classes with business logic
|
|
5
|
+
class User extends Equatable {
|
|
6
|
+
final String id;
|
|
7
|
+
final String name;
|
|
8
|
+
final String email;
|
|
9
|
+
final DateTime createdAt;
|
|
10
|
+
|
|
11
|
+
const User({
|
|
12
|
+
required this.id,
|
|
13
|
+
required this.name,
|
|
14
|
+
required this.email,
|
|
15
|
+
required this.createdAt,
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
@override
|
|
19
|
+
List<Object?> get props => [id, name, email, createdAt];
|
|
20
|
+
|
|
21
|
+
@override
|
|
22
|
+
String toString() => 'User(id: $id, name: $name, email: $email)';
|
|
23
|
+
}
|
|
File without changes
|
|
File without changes
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import 'package:flutter/material.dart';
|
|
2
|
+
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
|
3
|
+
import 'core/theme/app_theme.dart';
|
|
4
|
+
import 'presentation/screens/home_screen.dart';
|
|
5
|
+
|
|
6
|
+
void main() async {
|
|
7
|
+
WidgetsFlutterBinding.ensureInitialized();
|
|
8
|
+
|
|
9
|
+
// Initialize dependencies here
|
|
10
|
+
// await setupDependencyInjection();
|
|
11
|
+
|
|
12
|
+
runApp(
|
|
13
|
+
const ProviderScope(
|
|
14
|
+
child: MyApp(),
|
|
15
|
+
),
|
|
16
|
+
);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
class MyApp extends StatelessWidget {
|
|
20
|
+
const MyApp({super.key});
|
|
21
|
+
|
|
22
|
+
@override
|
|
23
|
+
Widget build(BuildContext context) {
|
|
24
|
+
return MaterialApp(
|
|
25
|
+
title: '__APP_NAME_PASCAL__',
|
|
26
|
+
debugShowCheckedModeBanner: false,
|
|
27
|
+
theme: AppTheme.lightTheme,
|
|
28
|
+
darkTheme: AppTheme.darkTheme,
|
|
29
|
+
themeMode: ThemeMode.system,
|
|
30
|
+
home: const HomeScreen(),
|
|
31
|
+
);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
File without changes
|