creek

The AI Image Editor of 2030

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}