serenay_ecommerce_widgets 1.1.5 copy "serenay_ecommerce_widgets: ^1.1.5" to clipboard
serenay_ecommerce_widgets: ^1.1.5 copied to clipboard

A backend-driven widget system for e-commerce screens. Renders TEXT, IMAGE, CAROUSEL, GRID, PRODUCTCARD, FLASHSALE and more from JSON, with nav/data/cart injected by the host app.

example/lib/main.dart

import 'package:flutter/material.dart';
import 'package:serenay_ecommerce_widgets/serenay_ecommerce_widgets.dart';
import 'package:url_launcher/url_launcher.dart';

void main() {
  runApp(const DemoApp());
}

/// A showcase app that renders a mock backend JSON payload through
/// [WidgetCatalog] inside a device-frame preview (mobile/tablet/web) with a
/// clickable widget index and live color-palette switching, to manually
/// exercise every catalog widget during development.
class DemoApp extends StatelessWidget {
  const DemoApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Serenay E-commerce Widgets',
      debugShowCheckedModeBanner: false,
      theme: ThemeData(
        colorScheme: ColorScheme.fromSeed(seedColor: AppColors.primary),
      ),
      home: const ScreenPage(),
    );
  }
}

enum _DeviceType { mobile, tablet, web }

class _Palette {
  const _Palette(this.name, this.swatch, this.theme);
  final String name;
  final Color swatch;
  final EcommerceWidgetTheme theme;
}

final _palettes = <_Palette>[
  const _Palette('Ocean', Color(0xFF1F6FEB), EcommerceWidgetTheme()),
  const _Palette(
    'Grape',
    Color(0xFF7B2CBF),
    EcommerceWidgetTheme(
      primaryColor: Color(0xFF7B2CBF),
      secondaryColor: Color(0xFFFF9F1C),
      discountColor: Color(0xFFE63946),
      surfaceColor: Color(0xFFF5F0FF),
      textPrimaryColor: Color(0xFF2B0A45),
    ),
  ),
  const _Palette(
    'Emerald',
    Color(0xFF0E9F6E),
    EcommerceWidgetTheme(
      primaryColor: Color(0xFF0E9F6E),
      secondaryColor: Color(0xFFFACC15),
      discountColor: Color(0xFFDC2626),
      surfaceColor: Color(0xFFECFDF5),
      textPrimaryColor: Color(0xFF064E3B),
    ),
  ),
  const _Palette(
    'Sunset',
    Color(0xFFF97316),
    EcommerceWidgetTheme(
      primaryColor: Color(0xFFF97316),
      secondaryColor: Color(0xFF14B8A6),
      discountColor: Color(0xFFDB2777),
      surfaceColor: Color(0xFFFFF7ED),
      textPrimaryColor: Color(0xFF7C2D12),
    ),
  ),
  const _Palette(
    'Rose',
    Color(0xFFE11D48),
    EcommerceWidgetTheme(
      primaryColor: Color(0xFFE11D48),
      secondaryColor: Color(0xFF6366F1),
      discountColor: Color(0xFF9333EA),
      surfaceColor: Color(0xFFFFF1F2),
      textPrimaryColor: Color(0xFF4C0519),
    ),
  ),
  const _Palette(
    'Midnight',
    Color(0xFF111827),
    EcommerceWidgetTheme(
      primaryColor: Color(0xFF111827),
      secondaryColor: Color(0xFF38BDF8),
      discountColor: Color(0xFFF43F5E),
      surfaceColor: Color(0xFFF3F4F6),
      textPrimaryColor: Color(0xFF0B0F19),
    ),
  ),
];

/// Demonstrates a full backend-driven screen backed by an in-memory mock
/// repository standing in for a real backend.
class ScreenPage extends StatefulWidget {
  const ScreenPage({super.key});

  @override
  State<ScreenPage> createState() => _ScreenPageState();
}

class _ScreenPageState extends State<ScreenPage> {
  final bool _isLoggedIn = true;
  final List<ProductCardData> _products = _mockProducts();
  final List<ProductCardData> _visited = _mockProducts().take(3).toList();

  _DeviceType _device = _DeviceType.mobile;
  int _paletteIndex = 0;

  final List<Map<String, dynamic>> _catalogData =
      (_mockScreenJson['data'] as List).cast<Map<String, dynamic>>();

  late final List<GlobalKey> _itemKeys = List.generate(
    _catalogData.length,
    (_) => GlobalKey(),
  );

  late final List<_CatalogEntry> _sidebarEntries = [
    for (var i = 0; i < _catalogData.length; i++)
      if (_catalogData[i]['type'] != 'DIVIDER')
        _CatalogEntry(index: i, label: _labelFor(_catalogData[i])),
  ];

  late final _callbacks = WidgetCallbacks(
    onAction: (action) => ScaffoldMessenger.of(context).showSnackBar(
      SnackBar(
        content: Text(
          'Action: ${action.type.name} id=${action.id ?? action.goto ?? '-'}',
        ),
      ),
    ),
    fetchProducts: (query) async {
      await Future.delayed(const Duration(milliseconds: 300));
      return _products;
    },
    fetchSlides: (id) async {
      await Future.delayed(const Duration(milliseconds: 200));
      return List.generate(
        3,
        (i) => SlideItem(
          image: 'https://picsum.photos/seed/slide$id$i/800/400',
          action: const WidgetAction(type: WidgetActionType.category, id: 1),
        ),
      );
    },
    fetchVideos: (id) async => const [
      VideoItem(
        video:
            'https://commondatastorage.googleapis.com/gtv-videos-bucket/sample/BigBuckBunny.mp4',
        action: WidgetAction(type: WidgetActionType.category, id: 1),
      ),
    ],
    fetchModal: (id) async => 'https://picsum.photos/seed/modal$id/600/800',
    isLoggedIn: () => _isLoggedIn,
    onRequireAuth: () => ScaffoldMessenger.of(
      context,
    ).showSnackBar(const SnackBar(content: Text('You need to log in'))),
    onToggleFavorite: (product) async {
      final updated = !product.isFavorited;
      setState(() {
        final index = _products.indexWhere((p) => p.id == product.id);
        if (index != -1)
          _products[index] = product.copyWith(isFavorited: updated);
      });
      return updated;
    },
    onAddToCart: (product, variant, quantity) async {
      ScaffoldMessenger.of(context).showSnackBar(
        SnackBar(content: Text('Added to cart: ${product.title} x$quantity')),
      );
    },
    visitedProducts: () => _visited,
  );

  void _scrollToIndex(int index) {
    final ctx = _itemKeys[index].currentContext;
    if (ctx == null) return;
    Scrollable.ensureVisible(
      ctx,
      duration: const Duration(milliseconds: 350),
      curve: Curves.easeInOut,
      alignment: 0.05,
    );
  }

  @override
  Widget build(BuildContext context) {
    final screenData = WidgetCatalog.fromJson(_mockScreenJson);
    final widgets = WidgetCatalog.getScreen(
      data: screenData,
      callbacks: _callbacks,
      theme: _palettes[_paletteIndex].theme,
    );

    return Scaffold(
      appBar: AppBar(
        backgroundColor: AppColors.primary,
        foregroundColor: Colors.white,
        title: const Text('Serenay E-commerce Widgets'),
        actions: [
          TextButton.icon(
            onPressed: () => launchUrl(
              Uri.parse(
                'https://serenayyazilim.github.io/serenay-ecommerce-widgets/docs/',
              ),
              webOnlyWindowName: '_blank',
            ),
            icon: const Icon(Icons.menu_book_outlined, color: Colors.white),
            label: const Text('Docs', style: TextStyle(color: Colors.white)),
          ),
          const SizedBox(width: 8),
        ],
      ),
      body: Row(
        crossAxisAlignment: CrossAxisAlignment.stretch,
        children: [
          _Sidebar(entries: _sidebarEntries, onTap: _scrollToIndex),
          const VerticalDivider(width: 1),
          Expanded(
            child: Column(
              children: [
                _Toolbar(
                  device: _device,
                  onDeviceChanged: (d) => setState(() => _device = d),
                  paletteIndex: _paletteIndex,
                  onPaletteChanged: (i) => setState(() => _paletteIndex = i),
                ),
                const Divider(height: 1),
                Expanded(
                  child: Container(
                    color: const Color(0xFFEFF1F4),
                    child: _PreviewArea(
                      device: _device,
                      itemKeys: _itemKeys,
                      widgets: widgets,
                    ),
                  ),
                ),
              ],
            ),
          ),
        ],
      ),
    );
  }
}

class _CatalogEntry {
  const _CatalogEntry({required this.index, required this.label});
  final int index;
  final String label;
}

const _catalogLabels = {
  'TEXT': 'Text',
  'SEARCH': 'Search',
  'SLIDER': 'Slider',
  'STORY': 'Story',
  'CAROUSEL': 'Carousel',
  'FLASHSALE': 'Flash sale',
  'IMAGE': 'Image',
  'IMAGELIST': 'Image list',
  'IMAGECAROUSEL': 'Image carousel',
  'GRID': 'Grid',
  'PRODUCTCARD': 'Product card',
  'MIXEDCAROUSEL': 'Mixed carousel',
  'VISITEDPRODUCTS': 'Visited products',
  'TIMEIMAGE': 'Time image',
  'YOUTUBE': 'YouTube',
  'VIDEOLIST': 'Video list',
  'FASTREGISTER': 'Fast register',
  'MODAL': 'Modal',
};

String _labelFor(Map<String, dynamic> item) {
  final type = item['type'] as String;
  if (type == 'TEXT') {
    final params = (item['params'] as Map?) ?? {};
    final text = params['text'];
    if (text is String && text.isNotEmpty) return text;
  }
  return _catalogLabels[type] ?? type;
}

/// Left rail listing every widget in the mock screen; tapping an entry
/// scrolls the device-frame preview to that widget.
class _Sidebar extends StatelessWidget {
  const _Sidebar({required this.entries, required this.onTap});

  final List<_CatalogEntry> entries;
  final ValueChanged<int> onTap;

  @override
  Widget build(BuildContext context) {
    return SizedBox(
      width: 230,
      child: Column(
        crossAxisAlignment: CrossAxisAlignment.stretch,
        children: [
          const Padding(
            padding: EdgeInsets.fromLTRB(16, 16, 16, 8),
            child: Text(
              'Widgets',
              style: TextStyle(fontWeight: FontWeight.w700, fontSize: 15),
            ),
          ),
          Expanded(
            child: ListView.builder(
              padding: const EdgeInsets.only(bottom: 16),
              itemCount: entries.length,
              itemBuilder: (context, i) {
                final entry = entries[i];
                return InkWell(
                  onTap: () => onTap(entry.index),
                  child: Padding(
                    padding: const EdgeInsets.symmetric(
                      horizontal: 16,
                      vertical: 10,
                    ),
                    child: Text(
                      entry.label,
                      maxLines: 1,
                      overflow: TextOverflow.ellipsis,
                      style: const TextStyle(fontSize: 13.5),
                    ),
                  ),
                );
              },
            ),
          ),
        ],
      ),
    );
  }
}

/// Top bar: device-type switch (mobile/tablet/web) + color palette picker.
class _Toolbar extends StatelessWidget {
  const _Toolbar({
    required this.device,
    required this.onDeviceChanged,
    required this.paletteIndex,
    required this.onPaletteChanged,
  });

  final _DeviceType device;
  final ValueChanged<_DeviceType> onDeviceChanged;
  final int paletteIndex;
  final ValueChanged<int> onPaletteChanged;

  @override
  Widget build(BuildContext context) {
    return Container(
      color: Colors.white,
      padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 10),
      child: Wrap(
        crossAxisAlignment: WrapCrossAlignment.center,
        spacing: 24,
        runSpacing: 12,
        children: [
          _DeviceSwitch(device: device, onChanged: onDeviceChanged),
          _PaletteSwitch(selected: paletteIndex, onChanged: onPaletteChanged),
        ],
      ),
    );
  }
}

class _DeviceSwitch extends StatelessWidget {
  const _DeviceSwitch({required this.device, required this.onChanged});

  final _DeviceType device;
  final ValueChanged<_DeviceType> onChanged;

  static const _options = [
    (_DeviceType.mobile, Icons.phone_iphone, 'Mobile'),
    (_DeviceType.tablet, Icons.tablet_mac, 'Tablet'),
    (_DeviceType.web, Icons.language, 'Web'),
  ];

  @override
  Widget build(BuildContext context) {
    return Container(
      padding: const EdgeInsets.all(4),
      decoration: BoxDecoration(
        color: const Color(0xFFF3F4F6),
        borderRadius: BorderRadius.circular(10),
      ),
      child: Row(
        mainAxisSize: MainAxisSize.min,
        children: _options.map((opt) {
          final selected = opt.$1 == device;
          return InkWell(
            borderRadius: BorderRadius.circular(8),
            onTap: () => onChanged(opt.$1),
            child: AnimatedContainer(
              duration: const Duration(milliseconds: 150),
              padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
              decoration: BoxDecoration(
                color: selected ? Colors.white : Colors.transparent,
                borderRadius: BorderRadius.circular(8),
                boxShadow: selected
                    ? [
                        BoxShadow(
                          color: Colors.black.withValues(alpha: 0.08),
                          blurRadius: 6,
                        ),
                      ]
                    : null,
              ),
              child: Row(
                mainAxisSize: MainAxisSize.min,
                children: [
                  Icon(
                    opt.$2,
                    size: 18,
                    color: selected
                        ? AppColors.primary
                        : const Color(0xFF6B7280),
                  ),
                  const SizedBox(width: 6),
                  Text(
                    opt.$3,
                    style: TextStyle(
                      fontSize: 13,
                      fontWeight: FontWeight.w600,
                      color: selected
                          ? AppColors.primary
                          : const Color(0xFF6B7280),
                    ),
                  ),
                ],
              ),
            ),
          );
        }).toList(),
      ),
    );
  }
}

class _PaletteSwitch extends StatelessWidget {
  const _PaletteSwitch({required this.selected, required this.onChanged});

  final int selected;
  final ValueChanged<int> onChanged;

  @override
  Widget build(BuildContext context) {
    return Row(
      mainAxisSize: MainAxisSize.min,
      children: [
        const Text(
          'Colors',
          style: TextStyle(
            fontSize: 13,
            fontWeight: FontWeight.w600,
            color: Color(0xFF6B7280),
          ),
        ),
        const SizedBox(width: 10),
        for (var i = 0; i < _palettes.length; i++)
          Padding(
            padding: const EdgeInsets.only(right: 8),
            child: Tooltip(
              message: _palettes[i].name,
              child: InkWell(
                customBorder: const CircleBorder(),
                onTap: () => onChanged(i),
                child: AnimatedContainer(
                  duration: const Duration(milliseconds: 150),
                  width: 28,
                  height: 28,
                  decoration: BoxDecoration(
                    color: _palettes[i].swatch,
                    shape: BoxShape.circle,
                    border: Border.all(
                      color: i == selected ? Colors.black87 : Colors.white,
                      width: i == selected ? 2.5 : 2,
                    ),
                    boxShadow: [
                      BoxShadow(
                        color: Colors.black.withValues(alpha: 0.15),
                        blurRadius: 4,
                      ),
                    ],
                  ),
                  child: i == selected
                      ? const Icon(Icons.check, size: 14, color: Colors.white)
                      : null,
                ),
              ),
            ),
          ),
      ],
    );
  }
}

/// Renders [widgets] inside a device frame that matches [device].
class _PreviewArea extends StatelessWidget {
  const _PreviewArea({
    required this.device,
    required this.itemKeys,
    required this.widgets,
  });

  final _DeviceType device;
  final List<GlobalKey> itemKeys;
  final List<Widget> widgets;

  // A plain (non-lazy) Column: the catalog is short enough (~20 items) that
  // eagerly building everything is cheap, and it guarantees every item's
  // GlobalKey has a mounted context so the sidebar can always scroll to it —
  // a lazy ListView.builder would leave far-off items unbuilt (null
  // context) until scrolled near, breaking jump-to-item navigation.
  Widget _content(EdgeInsets padding) {
    return SingleChildScrollView(
      padding: padding,
      child: Column(
        children: [
          for (var i = 0; i < widgets.length; i++)
            KeyedSubtree(key: itemKeys[i], child: widgets[i]),
        ],
      ),
    );
  }

  @override
  Widget build(BuildContext context) {
    switch (device) {
      case _DeviceType.web:
        return Padding(
          padding: const EdgeInsets.all(24),
          child: _BrowserChrome(child: _content(EdgeInsets.zero)),
        );
      case _DeviceType.mobile:
        return SingleChildScrollView(
          padding: const EdgeInsets.symmetric(vertical: 24),
          child: Center(
            child: _Bezel(
              width: 390,
              height: 844,
              radius: 44,
              notch: true,
              child: _content(const EdgeInsets.only(top: 34)),
            ),
          ),
        );
      case _DeviceType.tablet:
        return SingleChildScrollView(
          padding: const EdgeInsets.symmetric(vertical: 24),
          child: Center(
            child: _Bezel(
              width: 820,
              height: 1100,
              radius: 28,
              notch: false,
              child: _content(const EdgeInsets.only(top: 16)),
            ),
          ),
        );
    }
  }
}

/// Scopes [child] to its own [Navigator] and a [MediaQuery] reporting [size]
/// as the viewport, so widgets that push full-screen routes or show
/// dialogs/bottom sheets (STORY, MODAL, variant sheets, the SLIDER zoom
/// gallery, ...) stay confined to the device frame instead of covering the
/// whole demo window.
class _DeviceScope extends StatelessWidget {
  const _DeviceScope({required this.size, required this.child});

  final Size size;
  final Widget child;

  @override
  Widget build(BuildContext context) {
    return MediaQuery(
      data: MediaQuery.of(context).copyWith(size: size),
      child: Navigator(
        onGenerateRoute: (settings) => MaterialPageRoute(builder: (_) => child),
      ),
    );
  }
}

/// Phone/tablet bezel: dark rounded frame with an optional notch, clipping
/// the scrollable widget catalog inside it.
class _Bezel extends StatelessWidget {
  const _Bezel({
    required this.width,
    required this.height,
    required this.radius,
    required this.notch,
    required this.child,
  });

  final double width;
  final double height;
  final double radius;
  final bool notch;
  final Widget child;

  @override
  Widget build(BuildContext context) {
    return Container(
      width: width,
      height: height,
      padding: const EdgeInsets.all(12),
      decoration: BoxDecoration(
        color: const Color(0xFF111318),
        borderRadius: BorderRadius.circular(radius),
        boxShadow: [
          BoxShadow(
            color: Colors.black.withValues(alpha: 0.25),
            blurRadius: 30,
            offset: const Offset(0, 12),
          ),
        ],
      ),
      child: ClipRRect(
        borderRadius: BorderRadius.circular(radius - 12),
        child: Stack(
          children: [
            Positioned.fill(
              child: Container(
                color: Colors.white,
                child: _DeviceScope(size: Size(width, height), child: child),
              ),
            ),
            if (notch)
              Positioned(
                top: 8,
                left: 0,
                right: 0,
                child: Center(
                  child: Container(
                    width: 120,
                    height: 26,
                    decoration: BoxDecoration(
                      color: const Color(0xFF111318),
                      borderRadius: BorderRadius.circular(20),
                    ),
                  ),
                ),
              ),
          ],
        ),
      ),
    );
  }
}

/// Browser-window chrome (title bar dots + address pill) used for the "Web"
/// device mode.
class _BrowserChrome extends StatelessWidget {
  const _BrowserChrome({required this.child});

  final Widget child;

  @override
  Widget build(BuildContext context) {
    return Container(
      constraints: const BoxConstraints(maxWidth: 1100),
      decoration: BoxDecoration(
        color: Colors.white,
        borderRadius: BorderRadius.circular(12),
        border: Border.all(color: const Color(0xFFE2E2E2)),
        boxShadow: [
          BoxShadow(
            color: Colors.black.withValues(alpha: 0.08),
            blurRadius: 24,
            offset: const Offset(0, 8),
          ),
        ],
      ),
      clipBehavior: Clip.antiAlias,
      child: Column(
        children: [
          Container(
            height: 40,
            color: const Color(0xFFF3F4F6),
            padding: const EdgeInsets.symmetric(horizontal: 12),
            child: Row(
              children: [
                _dot(const Color(0xFFFF5F57)),
                const SizedBox(width: 6),
                _dot(const Color(0xFFFFBD2E)),
                const SizedBox(width: 6),
                _dot(const Color(0xFF28C840)),
                const SizedBox(width: 16),
                Expanded(
                  child: Container(
                    height: 24,
                    alignment: Alignment.centerLeft,
                    padding: const EdgeInsets.symmetric(horizontal: 10),
                    decoration: BoxDecoration(
                      color: Colors.white,
                      borderRadius: BorderRadius.circular(6),
                      border: Border.all(color: const Color(0xFFE2E2E2)),
                    ),
                    child: const Text(
                      'serenay-ecommerce-widgets.dev',
                      style: TextStyle(fontSize: 12, color: Color(0xFF6B7280)),
                    ),
                  ),
                ),
              ],
            ),
          ),
          Expanded(
            child: LayoutBuilder(
              builder: (context, constraints) => _DeviceScope(
                size: Size(constraints.maxWidth, constraints.maxHeight),
                child: child,
              ),
            ),
          ),
        ],
      ),
    );
  }

  Widget _dot(Color c) => Container(
    width: 12,
    height: 12,
    decoration: BoxDecoration(color: c, shape: BoxShape.circle),
  );
}

List<ProductCardData> _mockProducts() => List.generate(
  6,
  (i) => ProductCardData(
    id: i + 1,
    image: 'https://picsum.photos/seed/product$i/400/400',
    title: 'Product ${i + 1}',
    subtitle: 'Brand ${i % 3}',
    price: 99.9 + i * 10,
    priceOld: i.isEven ? 149.9 + i * 10 : null,
    discount: i.isEven ? '${20 + i}' : null,
    isFavorited: i == 1,
    variants: i == 0
        ? const [
            ProductVariant(id: 'v1', name: 'Red'),
            ProductVariant(id: 'v2', name: 'Blue'),
          ]
        : const [],
    measureOptions: i == 0
        ? const [
            ProductMeasureOption(id: 's', title: 'S', priceText: '99.90 ₺'),
            ProductMeasureOption(id: 'm', title: 'M', priceText: '99.90 ₺'),
            ProductMeasureOption(id: 'l', title: 'L', priceText: '109.90 ₺'),
          ]
        : const [],
    saleDisabled: i == 5,
    saleDisabledReason: i == 5 ? 'Out of stock' : null,
    preOrder: i == 3,
  ),
);

final _mockScreenJson = {
  'data': [
    {
      'type': 'TEXT',
      'params': {
        'text': 'Serenay E-commerce Widgets',
        'style': 'section',
        'subtitle': 'Full widget catalog',
      },
    },
    {
      'type': 'SEARCH',
      'params': {
        'hint_text': 'Search products, brands...',
        'url': 'https://picsum.photos/seed/searchbg/800/400',
        'height_percent': 0.35,
      },
    },
    {
      'type': 'MIXEDCAROUSEL',
      'params': {
        'height_percent': 1.0,
        'items': [
          {
            'item_type': 'image',
            'bg_color': '#222222',
            'title': 'Lowest Price of the Year',
            'title_color': '#FFFFFF',
            'url': 'https://picsum.photos/seed/mix1/800/800',
            'type': 'category',
            'id': 1,
          },
          {
            'item_type': 'products',
            'bg_color': '#FFF7EC',
            'title': 'Just For You',
            'title_color': '#000000',
            'description':
                'Hand-picked products based on your browsing history',
            'description_color': '#000000',
            'category_id': 1,
          },
        ],
      },
    },
    {'type': 'DIVIDER', 'params': {}},
    {
      'type': 'STORY',
      'params': {
        'list': [
          {
            'thumbnail': 'https://picsum.photos/seed/story1/100/100',
            'urls': ['https://picsum.photos/seed/story1a/800/1400'],
            'contain': 'View Product',
            'type': 'product',
            'product_id_or_url': 1,
          },
          {
            'thumbnail': 'https://picsum.photos/seed/story2/100/100',
            'urls': ['https://picsum.photos/seed/story2a/800/1400'],
          },
        ],
      },
    },
    {'type': 'DIVIDER', 'params': {}},
    {
      'type': 'TEXT',
      'params': {'text': 'Featured', 'style': 'section'},
    },
    {
      'type': 'CAROUSEL',
      'params': {'category_id': 1, 'limit': 10},
    },
    {'type': 'DIVIDER', 'params': {}},
    {
      'type': 'FLASHSALE',
      'params': {
        'title': 'Flash Sale',
        'subtitle': 'Grab it before time runs out',
        'end_time': null,
      },
    },
    {'type': 'DIVIDER', 'params': {}},
    {
      'type': 'IMAGE',
      'params': {
        'url': 'https://picsum.photos/seed/banner/800/300',
        'type': 'category',
        'id': 2,
        'height_percent': 0.3,
        'radius': 12,
        'padding': 16,
      },
    },
    {
      'type': 'IMAGELIST',
      'params': {
        'list': [
          {
            'url': 'https://picsum.photos/seed/il1/400/200',
            'type': 'category',
            'id': 1,
          },
          {
            'url': 'https://picsum.photos/seed/il2/400/200',
            'type': 'category',
            'id': 2,
          },
        ],
      },
    },
    {
      'type': 'IMAGECAROUSEL',
      'params': {
        'id': 2,
        'height_percent': 0.35,
        'item_count': 2,
        'bg_image': '',
      },
    },
    {'type': 'DIVIDER', 'params': {}},
    {
      'type': 'TEXT',
      'params': {'text': 'All Products', 'style': 'section'},
    },
    {
      'type': 'SLIDER',
      'params': {'id': 1, 'height_percent': 0.35},
    },
    {
      'type': 'GRID',
      'params': {'category_id': 1},
    },
    {'type': 'DIVIDER', 'params': {}},
    {
      'type': 'PRODUCTCARD',
      'params': {'category_id': 1},
    },
    {'type': 'DIVIDER', 'params': {}},

    {'type': 'DIVIDER', 'params': {}},
    {
      'type': 'VISITEDPRODUCTS',
      'params': {'limit': 5},
    },
    {'type': 'DIVIDER', 'params': {}},
    {
      'type': 'TIMEIMAGE',
      'params': {
        'url': 'https://picsum.photos/seed/timeimage/800/400',
        'title': 'Campaign Ends In',
        'title_position': 'bottom',
        'title_color': '#FFFFFF',
      },
    },
    {
      'type': 'YOUTUBE',
      'params': {'url': 'https://www.youtube.com/watch?v=dQw4w9WgXcQ'},
    },
    {
      'type': 'VIDEOLIST',
      'params': {'id': 1},
    },
    {'type': 'FASTREGISTER', 'params': {}},
    {
      'type': 'MODAL',
      'params': {
        'url': 'https://picsum.photos/seed/modalwelcome/600/800',
        'type': 'category',
        'id': 1,
      },
    },
    {'type': 'UNKNOWN_FUTURE_TYPE', 'params': {}},
  ],
};
2
likes
0
points
494
downloads

Publisher

verified publisherserenay.net.tr

Weekly Downloads

A backend-driven widget system for e-commerce screens. Renders TEXT, IMAGE, CAROUSEL, GRID, PRODUCTCARD, FLASHSALE and more from JSON, with nav/data/cart injected by the host app.

Repository (GitHub)
View/report issues

Topics

#widget #ecommerce #ui

License

unknown (license)

Dependencies

cupertino_icons, flutter, url_launcher, video_player, youtube_player_flutter

More

Packages that depend on serenay_ecommerce_widgets