creek
The AI Image Editor of 2030git clone https://github.com/maydayv7/creek
lib/ui/pages/define_brand_page.dart (12988B)
1import 'package:flutter/material.dart';
2import 'package:flutter_svg/flutter_svg.dart';
3import 'package:creekui/services/project_service.dart';
4import 'package:creekui/ui/styles/variables.dart';
5import 'package:creekui/ui/widgets/text_field.dart';
6import 'package:creekui/ui/widgets/primary_button.dart';
7import 'package:creekui/ui/widgets/app_bar.dart';
8import 'package:creekui/ui/widgets/tag_chip.dart';
9import 'package:creekui/ui/widgets/dialog.dart';
10import 'project_detail_page.dart';
11
12class DefineBrandPage extends StatefulWidget {
13 final String projectName;
14 final String? projectDescription;
15
16 const DefineBrandPage({
17 super.key,
18 this.projectDescription,
19 required this.projectName,
20 });
21 @override
22 State<DefineBrandPage> createState() => _DefineBrandPageState();
23}
24
25class _DefineBrandPageState extends State<DefineBrandPage> {
26 final ProjectService _projectService = ProjectService();
27 final _projectNameController = TextEditingController();
28 final _descriptionController = TextEditingController();
29 final _problemController = TextEditingController();
30 final _goalController = TextEditingController();
31 final _whereWillAppearController = TextEditingController();
32 final _competitorInputController = TextEditingController();
33
34 bool _isLoading = false;
35 final List<String> _keywords = ['Colour', 'Fonts', 'Composition'];
36 final List<Map<String, String>> _competitorBrands = [
37 {'name': 'Mcdonalds', 'initial': 'M'},
38 {'name': 'The Good Folks', 'initial': 'G'},
39 ];
40
41 @override
42 void initState() {
43 super.initState();
44 // Pre-fill description if passed from previous page
45 if (widget.projectDescription != null) {
46 _descriptionController.text = widget.projectDescription!;
47 }
48 // Pre-fill project name if provided
49 _projectNameController.text = widget.projectName;
50 }
51
52 @override
53 void dispose() {
54 _projectNameController.dispose();
55 _descriptionController.dispose();
56 _problemController.dispose();
57 _goalController.dispose();
58 _whereWillAppearController.dispose();
59 _competitorInputController.dispose();
60 super.dispose();
61 }
62
63 Future<void> _handleFinish() async {
64 // 1. Basic Validation
65 if (_projectNameController.text.trim().isEmpty) {
66 ScaffoldMessenger.of(
67 context,
68 ).showSnackBar(const SnackBar(content: Text('Project name is required')));
69 return;
70 }
71
72 setState(() => _isLoading = true);
73
74 try {
75 // 2. Prepare data
76 final String title = _projectNameController.text.trim();
77 final String description = _descriptionController.text.trim();
78
79 // 3. Call service and capture new ID
80 final int newId = await _projectService.createProject(
81 title,
82 description: description.isEmpty ? null : description,
83 );
84
85 // 4. Navigate to ProjectDetailPage
86 if (mounted) {
87 Navigator.pushReplacement(
88 context,
89 MaterialPageRoute(
90 builder: (context) => ProjectDetailPage(projectId: newId),
91 ),
92 );
93 }
94 } catch (e) {
95 // 5. Error Handling
96 if (mounted) {
97 ScaffoldMessenger.of(context).showSnackBar(
98 SnackBar(
99 content: Text('Error creating project: $e'),
100 backgroundColor: Colors.red,
101 ),
102 );
103 }
104 } finally {
105 if (mounted) setState(() => _isLoading = false);
106 }
107 }
108
109 void _addCompetitorBrand() {
110 final brandName = _competitorInputController.text.trim();
111 if (brandName.isNotEmpty) {
112 final initial = brandName[0].toUpperCase();
113 setState(() {
114 _competitorBrands.add({'name': brandName, 'initial': initial});
115 _competitorInputController.clear();
116 });
117 }
118 }
119
120 void _showAddKeywordDialog() {
121 final controller = TextEditingController();
122
123 ShowDialog.show(
124 context,
125 title: 'Add Keyword',
126 primaryButtonText: 'Add',
127 content: CommonTextField(
128 hintText: 'e.g., Minimal',
129 controller: controller,
130 autoFocus: true,
131 ),
132 onPrimaryPressed: () {
133 final val = controller.text.trim();
134 if (val.isNotEmpty && !_keywords.contains(val)) {
135 setState(() => _keywords.add(val));
136 }
137 Navigator.pop(context);
138 },
139 );
140 }
141
142 @override
143 Widget build(BuildContext context) {
144 return Scaffold(
145 backgroundColor: Variables.background,
146 appBar: CommonAppBar(
147 title: "",
148 showBack: true,
149 leading: IconButton(
150 icon: const Icon(
151 Icons.arrow_back_ios,
152 size: 24,
153 color: Variables.textSecondary,
154 ),
155 onPressed: () => Navigator.pop(context),
156 ),
157 ),
158 body: SafeArea(
159 child: Column(
160 children: [
161 Expanded(
162 child: SingleChildScrollView(
163 padding: const EdgeInsets.fromLTRB(16, 0, 16, 0),
164 child: Column(
165 crossAxisAlignment: CrossAxisAlignment.start,
166 children: [
167 const SizedBox(height: 16),
168
169 // Icon
170 Container(
171 width: 52,
172 height: 52,
173 decoration: BoxDecoration(
174 color: const Color(0xFFE0E7FF),
175 borderRadius: BorderRadius.circular(1000),
176 ),
177 child: Center(
178 child: SvgPicture.asset(
179 'assets/icons/painting-ai-line.svg',
180 width: 24,
181 height: 24,
182 colorFilter: const ColorFilter.mode(
183 Color(0xFF7C86FF),
184 BlendMode.srcIn,
185 ),
186 ),
187 ),
188 ),
189 const SizedBox(height: 16),
190 Text('Define Your Brand', style: Variables.headerStyle),
191 const SizedBox(height: 4),
192 Text(
193 'Answer a few quick questions to help us craft your unique style guide.',
194 style: Variables.captionStyle.copyWith(fontSize: 14),
195 ),
196 const SizedBox(height: 32),
197 CommonTextField(
198 label: 'Project Name',
199 hintText: 'Enter project name',
200 controller: _projectNameController,
201 isRequired: true,
202 ),
203 const SizedBox(height: 16),
204 CommonTextField(
205 label: 'What do you want & who is it for.',
206 hintText: 'Describe your work and your audience.',
207 controller: _descriptionController,
208 maxLines: 3,
209 ),
210 const SizedBox(height: 16),
211 CommonTextField(
212 label: 'What problem you solve.',
213 hintText: 'Explain the main issue your brand addresses.',
214 controller: _problemController,
215 maxLines: 3,
216 ),
217 const SizedBox(height: 16),
218 CommonTextField(
219 label: 'Long-term goal for the brand.',
220 hintText: 'E.g. - Improving food availability...',
221 controller: _goalController,
222 ),
223 const SizedBox(height: 32),
224 _buildKeywordsSection(),
225 const SizedBox(height: 32),
226 _buildCompetitorBrandsSection(),
227 const SizedBox(height: 32),
228 CommonTextField(
229 label: 'Where will the brand appear',
230 hintText: 'Banners, Posters, Instagram..',
231 controller: _whereWillAppearController,
232 ),
233 const SizedBox(height: 100),
234 ],
235 ),
236 ),
237 ),
238 ],
239 ),
240 ),
241
242 // Bottom Button
243 bottomNavigationBar: Container(
244 padding: const EdgeInsets.fromLTRB(16, 0, 16, 24),
245 decoration: BoxDecoration(
246 color: Variables.background,
247 boxShadow: [
248 BoxShadow(
249 color: Colors.black.withOpacity(0.05),
250 blurRadius: 10,
251 offset: const Offset(0, -2),
252 ),
253 ],
254 ),
255 child: SafeArea(
256 child: PrimaryButton(
257 text: 'Create Project',
258 isLoading: _isLoading,
259 onPressed: _handleFinish,
260 iconPath: 'assets/icons/generate_icon.svg',
261 ),
262 ),
263 ),
264 );
265 }
266
267 Widget _buildKeywordsSection() {
268 return Column(
269 crossAxisAlignment: CrossAxisAlignment.start,
270 children: [
271 Row(
272 children: [
273 Text(
274 '2-3 vibe keywords',
275 style: Variables.bodyStyle.copyWith(fontWeight: FontWeight.w500),
276 ),
277 Text(
278 '*',
279 style: Variables.bodyStyle.copyWith(
280 color: const Color(0xFF4F39F6),
281 fontSize: 12,
282 ),
283 ),
284 ],
285 ),
286 const SizedBox(height: 8),
287 Wrap(
288 spacing: 8,
289 runSpacing: 8,
290 children: [
291 for (final keyword in _keywords)
292 TagChip(
293 label: keyword,
294 onDelete: () => setState(() => _keywords.remove(keyword)),
295 ),
296 GestureDetector(
297 onTap: _showAddKeywordDialog,
298 child: Container(
299 padding: const EdgeInsets.symmetric(
300 horizontal: 12,
301 vertical: 6,
302 ),
303 decoration: BoxDecoration(
304 border: Border.all(color: Variables.borderSubtle),
305 borderRadius: BorderRadius.circular(48),
306 ),
307 child: Row(
308 mainAxisSize: MainAxisSize.min,
309 children: [
310 Text(
311 'Add More',
312 style: Variables.captionStyle.copyWith(
313 fontSize: 12,
314 color: Variables.textPrimary,
315 ),
316 ),
317 const SizedBox(width: 4),
318 const Icon(
319 Icons.add,
320 size: 14,
321 color: Variables.textPrimary,
322 ),
323 ],
324 ),
325 ),
326 ),
327 ],
328 ),
329 ],
330 );
331 }
332
333 Widget _buildCompetitorBrandsSection() {
334 return Column(
335 crossAxisAlignment: CrossAxisAlignment.start,
336 children: [
337 Row(
338 children: [
339 Text(
340 '2-3 reference/competitor brands.',
341 style: Variables.bodyStyle.copyWith(fontWeight: FontWeight.w500),
342 ),
343 Text(
344 '*',
345 style: Variables.bodyStyle.copyWith(
346 color: const Color(0xFF4F39F6),
347 fontSize: 12,
348 ),
349 ),
350 ],
351 ),
352 const SizedBox(height: 6),
353 CommonTextField(
354 label: '',
355 hintText: 'Type the name of brands here...',
356 controller: _competitorInputController,
357 onSubmitted: (_) => _addCompetitorBrand(),
358 ),
359 if (_competitorBrands.isNotEmpty) ...[
360 const SizedBox(height: 8),
361 SizedBox(
362 height: 44,
363 child: ListView.separated(
364 scrollDirection: Axis.horizontal,
365 itemCount: _competitorBrands.length,
366 separatorBuilder: (_, __) => const SizedBox(width: 8),
367 itemBuilder: (context, index) {
368 final brand = _competitorBrands[index];
369 return TagChip(
370 label: brand['name'] ?? '',
371 icon: Container(
372 width: 30,
373 height: 30,
374 decoration: const BoxDecoration(
375 color: Colors.white,
376 shape: BoxShape.circle,
377 ),
378 child: Center(
379 child: Text(
380 brand['initial'] ?? '',
381 style: Variables.captionStyle.copyWith(
382 fontWeight: FontWeight.bold,
383 fontSize: 12,
384 ),
385 ),
386 ),
387 ),
388 onDelete:
389 () => setState(() => _competitorBrands.removeAt(index)),
390 );
391 },
392 ),
393 ),
394 ],
395 ],
396 );
397 }
398}