Skip to content

Kitten edition#19

Open
jeen-40 wants to merge 7 commits intomainfrom
kitten-edition
Open

Kitten edition#19
jeen-40 wants to merge 7 commits intomainfrom
kitten-edition

Conversation

@jeen-40
Copy link
Copy Markdown
Collaborator

@jeen-40 jeen-40 commented Mar 27, 2026

🐾 Kitten Edition – Experimental Branch

The Kitten Edition is a lightweight experimental branch of LIORA focused on exploring a more playful, expressive, and personalized user experience.

✨ What’s Different from Main Branch

  • Enhanced emotional expression through UI elements and mascot behavior
  • More playful tone with subtle humor and warmth
  • Increased personalization in interactions and feedback
  • Softer, more dynamic visual responses to user input

🎯 Purpose

This branch is created as a research and testing environment to better understand how users respond to a more emotionally engaging and human-centered design approach. It is especially tailored to resonate with younger audiences and users who prefer a lighter, more relatable interface.

🔬 Why This Matters

Rather than replacing the core experience, this branch allows us to experiment freely with interaction styles, tone, and emotional design. The goal is to identify what feels more natural, comforting, and alive.

🔄 Future Direction

If certain features or behaviors from the Kitten Edition strongly resonate with users, they may be refined and gradually integrated into the main branch. This ensures that LIORA continues to evolve while staying grounded in real user experience.


This is not a separate product, but a curated variation designed for exploration, feedback, and growth.

Copy link
Copy Markdown
Owner

@alwin-m alwin-m left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Flutter Add Product Screen — Code Review & Explanation

Overview

This screen allows:

  • Picking image from gallery
  • Uploading to Cloudinary
  • Adding product to Firebase Firestore

Your code structure is very clean and production-ready. However, there are a few possible issues that could cause it to not work.


1. Most Likely Crash — int.parse()

You are using:

'price': int.parse(priceController.text.trim()),
'stock': int.parse(stockController.text.trim()),

Why This Fails

int.parse() crashes when:

  • User enters decimal (10.5)
  • User enters empty string
  • User enters text accidentally
  • User enters space

This throws:

FormatException: Invalid radix-10 number

Fix (Safe Version)

Use:

'price': double.tryParse(priceController.text.trim()) ?? 0,
'stock': int.tryParse(stockController.text.trim()) ?? 0,

This prevents app crashes.


2. Cloudinary Upload Status Code Issue

Your code:

if (response.statusCode == 200)

Cloudinary sometimes returns:

  • 200
  • 201

So upload may succeed but your code treats it as failure.

Fix

if (response.statusCode == 200 || response.statusCode == 201)

3. Silent Image Upload Failure

This logic:

'image': imageUrl.isEmpty
    ? 'https://via.placeholder.com/300x200?text=No+Image'
    : imageUrl,

If upload fails:

  • imageUrl remains empty
  • placeholder image used
  • You think upload failed silently

This makes debugging difficult.


4. Firestore Permission Issue

If Firestore rules are strict:

You may get:

Missing or insufficient permissions

Temporary test rule:

allow read, write: if true;

If this works → rules issue.


5. Cloudinary Upload Preset

Check:

const String uploadPreset = 'products_unsigned';

Make sure:

  • Upload preset exists
  • Unsigned upload enabled
  • Name correct

6. Recommended Debugging Prints

Add inside addProduct():

print("Uploading image...");

After upload:

print("Image URL: $imageUrl");

This helps identify failure.


Final Most Likely Causes

  1. int.parse crash
  2. Cloudinary 201 status
  3. Firestore permission
  4. Upload preset issue

Code Quality Feedback

Good things in your code:

  • Clean structure
  • Proper state handling
  • mounted check (very good)
  • Loading state
  • Error handling
  • Cloudinary upload logic

This is very good Flutter code quality.


Overall Rating

Code Quality: 9/10
Structure: 9/10
Error Handling: 8/10
Production Ready: Yes

You're writing near production

Comment on lines +594 to +803
*/
import 'dart:typed_data';
import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:image_picker/image_picker.dart';
import 'package:http/http.dart' as http;

// 🔥 PUT YOUR CLOUDINARY DETAILS HERE
const String cloudName = 'ddr1p1mv7';
const String uploadPreset = 'products_unsigned';


class AddProductScreen extends StatefulWidget {
const AddProductScreen({super.key});

@override
State<AddProductScreen> createState() => _AddProductScreenState();
}

class _AddProductScreenState extends State<AddProductScreen> {
final nameController = TextEditingController();
final priceController = TextEditingController();
final descController = TextEditingController();
final stockController = TextEditingController();
final imageUrlController = TextEditingController();

bool trending = false;
bool loading = false;

Uint8List? imageBytes;
final ImagePicker picker = ImagePicker();

@override
void dispose() {
nameController.dispose();
priceController.dispose();
descController.dispose();
stockController.dispose();
imageUrlController.dispose();
super.dispose();
}

// 📸 Pick image
Future<void> pickImage() async {
try {
final picked = await picker.pickImage(source: ImageSource.gallery);
if (picked != null) {
final bytes = await picked.readAsBytes();
setState(() => imageBytes = bytes);
}
} catch (e) {
ScaffoldMessenger.of(context)
.showSnackBar(SnackBar(content: Text('Image pick error: $e')));
}
}

// ☁️ Upload to Cloudinary
Future<String> uploadToCloudinary(Uint8List imageBytes) async {
final uri = Uri.parse(
'https://api.cloudinary.com/v1_1/$cloudName/image/upload',
);

final request = http.MultipartRequest('POST', uri)
..fields['upload_preset'] = uploadPreset
..files.add(
http.MultipartFile.fromBytes(
'file',
imageBytes,
filename: 'product.jpg',
),
);

final response = await request.send();

if (response.statusCode == 200) {
final resStr = await response.stream.bytesToString();
final data = jsonDecode(resStr);
return data['secure_url']; // 🔥 image URL
} else {
throw Exception('Cloudinary upload failed');
}
}

// ➕ Add product
Future<void> addProduct() async {
if (nameController.text.isEmpty ||
priceController.text.isEmpty ||
stockController.text.isEmpty) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Fill all required fields')),
);
return;
}

setState(() => loading = true);

try {
String imageUrl = imageUrlController.text.trim();

// 🔥 Upload to Cloudinary if image selected
if (imageBytes != null && imageBytes!.isNotEmpty) {
imageUrl = await uploadToCloudinary(imageBytes!);
}

await FirebaseFirestore.instance.collection('products').add({
'name': nameController.text.trim(),
'price': int.parse(priceController.text.trim()),
'details': [descController.text.trim()],
'stock': int.parse(stockController.text.trim()),
'image': imageUrl.isEmpty
? 'https://via.placeholder.com/300x200?text=No+Image'
: imageUrl,
'trending': trending,
'createdAt': Timestamp.now(),
});

if (!mounted) return;

ScaffoldMessenger.of(context)
.showSnackBar(const SnackBar(content: Text('Product added')));

Navigator.pop(context);
} catch (e) {
ScaffoldMessenger.of(context)
.showSnackBar(SnackBar(content: Text('Error: $e')));
} finally {
if (mounted) setState(() => loading = false);
}
}

@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text("Add Product"),
backgroundColor: Colors.pinkAccent,
),
body: SingleChildScrollView(
padding: const EdgeInsets.all(20),
child: Column(
children: [
// 🖼 Pick image
GestureDetector(
onTap: loading ? null : pickImage,
child: Container(
height: 160,
width: double.infinity,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(20),
color: Colors.grey.shade200,
border: Border.all(color: Colors.pinkAccent),
),
child: imageBytes == null
? const Icon(Icons.add_a_photo,
size: 40, color: Colors.pinkAccent)
: ClipRRect(
borderRadius: BorderRadius.circular(20),
child: Image.memory(imageBytes!, fit: BoxFit.cover),
),
),
),

const SizedBox(height: 20),
_field(nameController, "Product name"),
const SizedBox(height: 12),
_field(priceController, "Price", isNumber: true),
const SizedBox(height: 12),
_field(stockController, "Stock", isNumber: true),
const SizedBox(height: 12),
_field(descController, "Description"),
const SizedBox(height: 12),
_field(imageUrlController, "Or paste image URL (optional)"),

SwitchListTile(
value: trending,
onChanged: (v) => setState(() => trending = v),
title: const Text("Trending product"),
),

const SizedBox(height: 20),

ElevatedButton(
onPressed: loading ? null : addProduct,
style: ElevatedButton.styleFrom(
backgroundColor: Colors.pinkAccent,
minimumSize: const Size(double.infinity, 50),
),
child: loading
? const CircularProgressIndicator(color: Colors.white)
: const Text("Add Product"),
),
],
),
),
);
}

Widget _field(TextEditingController c, String label,
{bool isNumber = false}) {
return TextField(
controller: c,
keyboardType: isNumber ? TextInputType.number : TextInputType.text,
decoration: InputDecoration(
labelText: label,
border: OutlineInputBorder(borderRadius: BorderRadius.circular(12)),
),
);
}
}
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Flutter Add Product Screen — Code Review & Explanation

Overview

This screen allows:

  • Picking image from gallery
  • Uploading to Cloudinary
  • Adding product to Firebase Firestore

Your code structure is very clean and production-ready. However, there are a few possible issues that could cause it to not work.


1. Most Likely Crash — int.parse()

You are using:

'price': int.parse(priceController.text.trim()),
'stock': int.parse(stockController.text.trim()),

Why This Fails

int.parse() crashes when:

  • User enters decimal (10.5)
  • User enters empty string
  • User enters text accidentally
  • User enters space

This throws:

FormatException: Invalid radix-10 number

Fix (Safe Version)

Use:

'price': double.tryParse(priceController.text.trim()) ?? 0,
'stock': int.tryParse(stockController.text.trim()) ?? 0,

This prevents app crashes.


2. Cloudinary Upload Status Code Issue

Your code:

if (response.statusCode == 200)

Cloudinary sometimes returns:

  • 200
  • 201

So upload may succeed but your code treats it as failure.

Fix

if (response.statusCode == 200 || response.statusCode == 201)

3. Silent Image Upload Failure

This logic:

'image': imageUrl.isEmpty
    ? 'https://via.placeholder.com/300x200?text=No+Image'
    : imageUrl,

If upload fails:

  • imageUrl remains empty
  • placeholder image used
  • You think upload failed silently

This makes debugging difficult.


4. Firestore Permission Issue

If Firestore rules are strict:

You may get:

Missing or insufficient permissions

Temporary test rule:

allow read, write: if true;

If this works → rules issue.


5. Cloudinary Upload Preset

Check:

const String uploadPreset = 'products_unsigned';

Make sure:

  • Upload preset exists
  • Unsigned upload enabled
  • Name correct

6. Recommended Debugging Prints

Add inside addProduct():

print("Uploading image...");

After upload:

print("Image URL: $imageUrl");

This helps identify failure.


Final Most Likely Causes

  1. int.parse crash
  2. Cloudinary 201 status
  3. Firestore permission
  4. Upload preset issue

Code Quality Feedback

Good things in your code:

  • Clean structure
  • Proper state handling
  • mounted check (very good)
  • Loading state
  • Error handling
  • Cloudinary upload logic

This is very good Flutter code quality.


Overall Rating

Code Quality: 9/10
Structure: 9/10
Error Handling: 8/10
Production Ready: Yes

You're writing near production

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants