1134 lines
		
	
	
		
			38 KiB
		
	
	
	
		
			Dart
		
	
	
	
	
	
			
		
		
	
	
			1134 lines
		
	
	
		
			38 KiB
		
	
	
	
		
			Dart
		
	
	
	
	
	
import 'package:flutter/foundation.dart';
 | 
						|
import 'package:flutter/material.dart';
 | 
						|
import 'package:flutter/services.dart';
 | 
						|
import 'package:flutter_riverpod/flutter_riverpod.dart';
 | 
						|
import 'package:dio/dio.dart';
 | 
						|
import 'package:gap/gap.dart';
 | 
						|
import 'package:island/pods/network.dart';
 | 
						|
import 'package:island/talker.dart';
 | 
						|
import 'package:island/widgets/alert.dart';
 | 
						|
import 'package:island/models/poll.dart';
 | 
						|
import 'package:island/widgets/app_scaffold.dart';
 | 
						|
import 'package:styled_widget/styled_widget.dart';
 | 
						|
import 'package:uuid/uuid.dart';
 | 
						|
import 'package:easy_localization/easy_localization.dart';
 | 
						|
 | 
						|
class PollEditorState {
 | 
						|
  String? id; // for editing
 | 
						|
  String? title;
 | 
						|
  String? description;
 | 
						|
  DateTime? endedAt;
 | 
						|
  List<SnPollQuestion> questions;
 | 
						|
 | 
						|
  PollEditorState({
 | 
						|
    this.id,
 | 
						|
    this.title,
 | 
						|
    this.description,
 | 
						|
    this.endedAt,
 | 
						|
    List<SnPollQuestion>? questions,
 | 
						|
  }) : questions = questions ?? const [];
 | 
						|
}
 | 
						|
 | 
						|
/// Riverpod Notifier state
 | 
						|
class PollEditor extends Notifier<PollEditorState> {
 | 
						|
  @override
 | 
						|
  PollEditorState build() {
 | 
						|
    return PollEditorState();
 | 
						|
  }
 | 
						|
 | 
						|
  void setTitle(String? value) {
 | 
						|
    state = PollEditorState(
 | 
						|
      id: state.id,
 | 
						|
      title: value,
 | 
						|
      description: state.description,
 | 
						|
      endedAt: state.endedAt,
 | 
						|
      questions: [...state.questions],
 | 
						|
    );
 | 
						|
  }
 | 
						|
 | 
						|
  void setDescription(String? value) {
 | 
						|
    state = PollEditorState(
 | 
						|
      id: state.id,
 | 
						|
      title: state.title,
 | 
						|
      description: value,
 | 
						|
      endedAt: state.endedAt,
 | 
						|
      questions: [...state.questions],
 | 
						|
    );
 | 
						|
  }
 | 
						|
 | 
						|
  void setEndedAt(DateTime? value) {
 | 
						|
    state = PollEditorState(
 | 
						|
      id: state.id,
 | 
						|
      title: state.title,
 | 
						|
      description: state.description,
 | 
						|
      endedAt: value,
 | 
						|
      questions: [...state.questions],
 | 
						|
    );
 | 
						|
  }
 | 
						|
 | 
						|
  Future<void> setEditingId(BuildContext context, String? id) async {
 | 
						|
    if (id == null || id.isEmpty) return;
 | 
						|
 | 
						|
    showLoadingModal(context);
 | 
						|
    final dio = ref.read(apiClientProvider);
 | 
						|
    try {
 | 
						|
      final res = await dio.get('/sphere/polls/$id');
 | 
						|
 | 
						|
      // Handle both plain object and wrapped response formats.
 | 
						|
      final dynamic payload = res.data;
 | 
						|
      final Map<String, dynamic> json =
 | 
						|
          payload is Map && payload['data'] is Map<String, dynamic>
 | 
						|
              ? Map<String, dynamic>.from(payload['data'] as Map)
 | 
						|
              : Map<String, dynamic>.from(payload as Map);
 | 
						|
 | 
						|
      final poll = SnPoll.fromJson(json);
 | 
						|
 | 
						|
      state = PollEditorState(
 | 
						|
        id: poll.id,
 | 
						|
        title: poll.title,
 | 
						|
        description: poll.description,
 | 
						|
        endedAt: poll.endedAt,
 | 
						|
        questions: poll.questions,
 | 
						|
      );
 | 
						|
    } on DioException catch (e) {
 | 
						|
      talker.error('Failed to load poll $id: ${e.message}');
 | 
						|
      // Keep state with id set; UI may handle error display.
 | 
						|
    } catch (e) {
 | 
						|
      talker.error('Unexpected error loading poll $id: $e');
 | 
						|
    } finally {
 | 
						|
      if (context.mounted) hideLoadingModal(context);
 | 
						|
    }
 | 
						|
  }
 | 
						|
 | 
						|
  void addQuestion(SnPollQuestionType type) {
 | 
						|
    final nextOrder = state.questions.length;
 | 
						|
    final isOptionsType = _isOptionsType(type);
 | 
						|
    final q = SnPollQuestion(
 | 
						|
      id: const Uuid().v4(),
 | 
						|
      type: type,
 | 
						|
      options:
 | 
						|
          isOptionsType
 | 
						|
              ? [
 | 
						|
                SnPollOption(
 | 
						|
                  id: const Uuid().v4(),
 | 
						|
                  label: 'pollOptionDefaultLabel'.tr(),
 | 
						|
                  order: 0,
 | 
						|
                ),
 | 
						|
              ]
 | 
						|
              : null,
 | 
						|
      title: '',
 | 
						|
      description: null,
 | 
						|
      order: nextOrder,
 | 
						|
      isRequired: false,
 | 
						|
    );
 | 
						|
    state = PollEditorState(
 | 
						|
      id: state.id,
 | 
						|
      title: state.title,
 | 
						|
      description: state.description,
 | 
						|
      endedAt: state.endedAt,
 | 
						|
      questions: [...state.questions, q],
 | 
						|
    );
 | 
						|
  }
 | 
						|
 | 
						|
  void removeQuestion(int index) {
 | 
						|
    if (index < 0 || index >= state.questions.length) return;
 | 
						|
    final updated = [...state.questions]..removeAt(index);
 | 
						|
    for (var i = 0; i < updated.length; i++) {
 | 
						|
      updated[i] = updated[i].copyWith(order: i);
 | 
						|
    }
 | 
						|
    state = PollEditorState(
 | 
						|
      id: state.id,
 | 
						|
      title: state.title,
 | 
						|
      description: state.description,
 | 
						|
      endedAt: state.endedAt,
 | 
						|
      questions: updated,
 | 
						|
    );
 | 
						|
  }
 | 
						|
 | 
						|
  void moveQuestionUp(int index) {
 | 
						|
    if (index <= 0 || index >= state.questions.length) return;
 | 
						|
    final updated = [...state.questions];
 | 
						|
    final tmp = updated[index - 1];
 | 
						|
    updated[index - 1] = updated[index];
 | 
						|
    updated[index] = tmp;
 | 
						|
    for (var i = 0; i < updated.length; i++) {
 | 
						|
      updated[i] = updated[i].copyWith(order: i);
 | 
						|
    }
 | 
						|
    state = PollEditorState(
 | 
						|
      id: state.id,
 | 
						|
      title: state.title,
 | 
						|
      description: state.description,
 | 
						|
      endedAt: state.endedAt,
 | 
						|
      questions: updated,
 | 
						|
    );
 | 
						|
  }
 | 
						|
 | 
						|
  void moveQuestionDown(int index) {
 | 
						|
    if (index < 0 || index >= state.questions.length - 1) return;
 | 
						|
    final updated = [...state.questions];
 | 
						|
    final tmp = updated[index + 1];
 | 
						|
    updated[index + 1] = updated[index];
 | 
						|
    updated[index] = tmp;
 | 
						|
    for (var i = 0; i < updated.length; i++) {
 | 
						|
      updated[i] = updated[i].copyWith(order: i);
 | 
						|
    }
 | 
						|
    state = PollEditorState(
 | 
						|
      id: state.id,
 | 
						|
      title: state.title,
 | 
						|
      description: state.description,
 | 
						|
      endedAt: state.endedAt,
 | 
						|
      questions: updated,
 | 
						|
    );
 | 
						|
  }
 | 
						|
 | 
						|
  void setQuestionType(int index, SnPollQuestionType type) {
 | 
						|
    if (index < 0 || index >= state.questions.length) return;
 | 
						|
    final q = state.questions[index];
 | 
						|
    final isOptionsType = _isOptionsType(type);
 | 
						|
    final newOptions =
 | 
						|
        isOptionsType
 | 
						|
            ? (q.options?.isNotEmpty == true
 | 
						|
                ? q.options
 | 
						|
                : [
 | 
						|
                  SnPollOption(
 | 
						|
                    id: const Uuid().v4(),
 | 
						|
                    label: 'pollOptionDefaultLabel'.tr(),
 | 
						|
                    order: 0,
 | 
						|
                  ),
 | 
						|
                ])
 | 
						|
            : null;
 | 
						|
    _updateQuestion(index, q.copyWith(type: type, options: newOptions));
 | 
						|
  }
 | 
						|
 | 
						|
  void setQuestionTitle(int index, String title) {
 | 
						|
    if (index < 0 || index >= state.questions.length) return;
 | 
						|
    final q = state.questions[index];
 | 
						|
    _updateQuestion(index, q.copyWith(title: title));
 | 
						|
  }
 | 
						|
 | 
						|
  void setQuestionDescription(int index, String? description) {
 | 
						|
    if (index < 0 || index >= state.questions.length) return;
 | 
						|
    final q = state.questions[index];
 | 
						|
    _updateQuestion(index, q.copyWith(description: description));
 | 
						|
  }
 | 
						|
 | 
						|
  void setQuestionRequired(int index, bool value) {
 | 
						|
    if (index < 0 || index >= state.questions.length) return;
 | 
						|
    final q = state.questions[index];
 | 
						|
    _updateQuestion(index, q.copyWith(isRequired: value));
 | 
						|
  }
 | 
						|
 | 
						|
  void addOption(int qIndex) {
 | 
						|
    if (qIndex < 0 || qIndex >= state.questions.length) return;
 | 
						|
    final q = state.questions[qIndex];
 | 
						|
    if (!_isOptionsType(q.type)) return;
 | 
						|
    final opts = <SnPollOption>[...(q.options ?? [])];
 | 
						|
    final nextOrder = opts.length;
 | 
						|
    opts.add(
 | 
						|
      SnPollOption(
 | 
						|
        id: const Uuid().v4(),
 | 
						|
        label: 'Option ${nextOrder + 1}',
 | 
						|
        order: nextOrder,
 | 
						|
      ),
 | 
						|
    );
 | 
						|
    _updateQuestion(qIndex, q.copyWith(options: opts));
 | 
						|
  }
 | 
						|
 | 
						|
  void removeOption(int qIndex, int optIndex) {
 | 
						|
    if (qIndex < 0 || qIndex >= state.questions.length) return;
 | 
						|
    final q = state.questions[qIndex];
 | 
						|
    if (!_isOptionsType(q.type)) return;
 | 
						|
    final opts = <SnPollOption>[...(q.options ?? [])];
 | 
						|
    if (optIndex < 0 || optIndex >= opts.length) return;
 | 
						|
    opts.removeAt(optIndex);
 | 
						|
    for (var i = 0; i < opts.length; i++) {
 | 
						|
      opts[i] = opts[i].copyWith(order: i);
 | 
						|
    }
 | 
						|
    _updateQuestion(qIndex, q.copyWith(options: opts));
 | 
						|
  }
 | 
						|
 | 
						|
  List<SnPollOption> _moveOptionByDelta(
 | 
						|
    List<SnPollOption> original,
 | 
						|
    int idx,
 | 
						|
    int delta,
 | 
						|
  ) {
 | 
						|
    if (idx + delta < 0 || idx + delta >= original.length) {
 | 
						|
      return original;
 | 
						|
    }
 | 
						|
    final clone = List<SnPollOption>.from(original);
 | 
						|
    clone.insert(idx + delta, clone.removeAt(idx));
 | 
						|
    for (var i = 0; i < clone.length; i++) {
 | 
						|
      clone[i] = clone[i].copyWith(order: i);
 | 
						|
    }
 | 
						|
    return clone;
 | 
						|
  }
 | 
						|
 | 
						|
  void moveOptionUp(int qIndex, int optIndex) {
 | 
						|
    if (qIndex < 0 || qIndex >= state.questions.length) return;
 | 
						|
    final q = state.questions[qIndex];
 | 
						|
    if (!_isOptionsType(q.type)) return;
 | 
						|
    final original = q.options ?? const <SnPollOption>[];
 | 
						|
    if (optIndex <= 0 || optIndex >= original.length) return;
 | 
						|
 | 
						|
    final reordered = _moveOptionByDelta(original, optIndex, -1);
 | 
						|
    if (!identical(reordered, original)) {
 | 
						|
      _updateQuestion(qIndex, q.copyWith(options: reordered));
 | 
						|
    }
 | 
						|
  }
 | 
						|
 | 
						|
  void moveOptionDown(int qIndex, int optIndex) {
 | 
						|
    if (qIndex < 0 || qIndex >= state.questions.length) return;
 | 
						|
    final q = state.questions[qIndex];
 | 
						|
    if (!_isOptionsType(q.type)) return;
 | 
						|
    final original = q.options ?? const <SnPollOption>[];
 | 
						|
    if (optIndex < 0 || optIndex >= original.length - 1) return;
 | 
						|
 | 
						|
    final reordered = _moveOptionByDelta(original, optIndex, 1);
 | 
						|
    if (!identical(reordered, original)) {
 | 
						|
      _updateQuestion(qIndex, q.copyWith(options: reordered));
 | 
						|
    }
 | 
						|
  }
 | 
						|
 | 
						|
  void setOptionLabel(int qIndex, int optIndex, String label) {
 | 
						|
    final q = state.questions[qIndex];
 | 
						|
    if (!_isOptionsType(q.type)) return;
 | 
						|
    final opts = <SnPollOption>[...(q.options ?? [])];
 | 
						|
    if (optIndex < 0 || optIndex >= opts.length) return;
 | 
						|
    opts[optIndex] = opts[optIndex].copyWith(label: label);
 | 
						|
    _updateQuestion(qIndex, q.copyWith(options: opts));
 | 
						|
  }
 | 
						|
 | 
						|
  void setOptionDescription(int qIndex, int optIndex, String? description) {
 | 
						|
    final q = state.questions[qIndex];
 | 
						|
    if (!_isOptionsType(q.type)) return;
 | 
						|
    final opts = <SnPollOption>[...(q.options ?? [])];
 | 
						|
    if (optIndex < 0 || optIndex >= opts.length) return;
 | 
						|
    opts[optIndex] = opts[optIndex].copyWith(description: description);
 | 
						|
    _updateQuestion(qIndex, q.copyWith(options: opts));
 | 
						|
  }
 | 
						|
 | 
						|
  bool _isOptionsType(SnPollQuestionType type) {
 | 
						|
    return type == SnPollQuestionType.singleChoice ||
 | 
						|
        type == SnPollQuestionType.multipleChoice;
 | 
						|
  }
 | 
						|
 | 
						|
  void _updateQuestion(int index, SnPollQuestion newQ) {
 | 
						|
    final list = <SnPollQuestion>[...state.questions];
 | 
						|
    list[index] = newQ;
 | 
						|
    state = PollEditorState(
 | 
						|
      id: state.id,
 | 
						|
      title: state.title,
 | 
						|
      description: state.description,
 | 
						|
      endedAt: state.endedAt,
 | 
						|
      questions: list,
 | 
						|
    );
 | 
						|
  }
 | 
						|
}
 | 
						|
 | 
						|
/// The poll editor screen.
 | 
						|
/// Note: This is UI only; wire API later. Requires riverpod_generator and build_runner to generate .g.dart.
 | 
						|
final pollEditorProvider = NotifierProvider<PollEditor, PollEditorState>(
 | 
						|
  PollEditor.new,
 | 
						|
);
 | 
						|
 | 
						|
class PollEditorScreen extends ConsumerWidget {
 | 
						|
  const PollEditorScreen({
 | 
						|
    super.key,
 | 
						|
    this.initialPollId,
 | 
						|
    this.initialPublisher,
 | 
						|
  });
 | 
						|
 | 
						|
  // Submit helpers declared before build to avoid forward reference issues
 | 
						|
 | 
						|
  Future<void> _submitPoll(BuildContext context, WidgetRef ref) async {
 | 
						|
    final model = ref.watch(pollEditorProvider);
 | 
						|
    final dio = ref.read(apiClientProvider);
 | 
						|
 | 
						|
    // Build payload
 | 
						|
    final body = {
 | 
						|
      'title': model.title,
 | 
						|
      'description': model.description,
 | 
						|
      'endedAt': model.endedAt?.toUtc().toIso8601String(),
 | 
						|
      'questions':
 | 
						|
          model.questions
 | 
						|
              .map(
 | 
						|
                (q) => {
 | 
						|
                  'type': q.type.index,
 | 
						|
                  'options':
 | 
						|
                      q.options
 | 
						|
                          ?.map(
 | 
						|
                            (o) => {
 | 
						|
                              'label': o.label,
 | 
						|
                              'description': o.description,
 | 
						|
                              'order': o.order,
 | 
						|
                            },
 | 
						|
                          )
 | 
						|
                          .toList(),
 | 
						|
                  'title': q.title,
 | 
						|
                  'description': q.description,
 | 
						|
                  'order': q.order,
 | 
						|
                  'isRequired': q.isRequired,
 | 
						|
                },
 | 
						|
              )
 | 
						|
              .toList(),
 | 
						|
    };
 | 
						|
 | 
						|
    try {
 | 
						|
      final isUpdate = model.id != null && model.id!.isNotEmpty;
 | 
						|
      final String path =
 | 
						|
          isUpdate ? '/sphere/polls/${model.id}' : '/sphere/polls';
 | 
						|
      final Response res =
 | 
						|
          await (isUpdate
 | 
						|
              ? dio.patch(
 | 
						|
                path,
 | 
						|
                queryParameters: {'pub': initialPublisher},
 | 
						|
                data: body,
 | 
						|
              )
 | 
						|
              : dio.post(
 | 
						|
                path,
 | 
						|
                queryParameters: {'pub': initialPublisher},
 | 
						|
                data: body,
 | 
						|
              ));
 | 
						|
 | 
						|
      showSnackBar(isUpdate ? 'pollUpdated'.tr() : 'pollCreated'.tr());
 | 
						|
 | 
						|
      if (!context.mounted) return;
 | 
						|
      Navigator.of(context).maybePop(res.data);
 | 
						|
    } catch (e) {
 | 
						|
      showErrorAlert(e);
 | 
						|
    }
 | 
						|
  }
 | 
						|
 | 
						|
  // If editing, provide existing poll id and preselected publisher name (optional)
 | 
						|
  final String? initialPollId;
 | 
						|
  final String? initialPublisher;
 | 
						|
 | 
						|
  @override
 | 
						|
  Widget build(BuildContext context, WidgetRef ref) {
 | 
						|
    final model = ref.watch(pollEditorProvider);
 | 
						|
    final notifier = ref.watch(pollEditorProvider.notifier);
 | 
						|
 | 
						|
    // initialize editing state if provided
 | 
						|
    if (initialPollId != null && model.id != initialPollId) {
 | 
						|
      Future(() {
 | 
						|
        if (context.mounted) notifier.setEditingId(context, initialPollId);
 | 
						|
      });
 | 
						|
    }
 | 
						|
 | 
						|
    return AppScaffold(
 | 
						|
      isNoBackground: false,
 | 
						|
      appBar: AppBar(
 | 
						|
        title: Text(model.id == null ? 'pollCreate'.tr() : 'pollEdit'.tr()),
 | 
						|
        actions: [
 | 
						|
          if (kDebugMode)
 | 
						|
            IconButton(
 | 
						|
              tooltip: 'pollPreviewJsonDebug'.tr(),
 | 
						|
              onPressed: () {
 | 
						|
                _showDebugPreview(context, model);
 | 
						|
              },
 | 
						|
              icon: const Icon(Icons.visibility_outlined),
 | 
						|
            ),
 | 
						|
          const Gap(8),
 | 
						|
        ],
 | 
						|
      ),
 | 
						|
      body: Column(
 | 
						|
        children: [
 | 
						|
          Expanded(
 | 
						|
            child: ConstrainedBox(
 | 
						|
              constraints: BoxConstraints(maxWidth: 640),
 | 
						|
              child:
 | 
						|
                  Form(
 | 
						|
                    key: ValueKey(model.id),
 | 
						|
                    child: ListView(
 | 
						|
                      padding: const EdgeInsets.all(16),
 | 
						|
                      children: [
 | 
						|
                        TextFormField(
 | 
						|
                          initialValue: model.title ?? '',
 | 
						|
                          decoration: InputDecoration(
 | 
						|
                            labelText: 'postTitle'.tr(),
 | 
						|
                            border: OutlineInputBorder(
 | 
						|
                              borderRadius: BorderRadius.all(
 | 
						|
                                Radius.circular(16),
 | 
						|
                              ),
 | 
						|
                            ),
 | 
						|
                          ),
 | 
						|
                          textInputAction: TextInputAction.next,
 | 
						|
                          maxLength: 256,
 | 
						|
                          onChanged: notifier.setTitle,
 | 
						|
                          onTapOutside:
 | 
						|
                              (_) =>
 | 
						|
                                  FocusManager.instance.primaryFocus?.unfocus(),
 | 
						|
                          validator: (v) {
 | 
						|
                            if (v == null || v.trim().isEmpty) {
 | 
						|
                              return 'pollTitleRequired'.tr();
 | 
						|
                            }
 | 
						|
                            return null;
 | 
						|
                          },
 | 
						|
                        ),
 | 
						|
                        const Gap(12),
 | 
						|
                        TextFormField(
 | 
						|
                          initialValue: model.description ?? '',
 | 
						|
                          decoration: InputDecoration(
 | 
						|
                            labelText: 'description'.tr(),
 | 
						|
                            alignLabelWithHint: true,
 | 
						|
                            border: OutlineInputBorder(
 | 
						|
                              borderRadius: BorderRadius.all(
 | 
						|
                                Radius.circular(16),
 | 
						|
                              ),
 | 
						|
                            ),
 | 
						|
                          ),
 | 
						|
                          maxLines: 3,
 | 
						|
                          maxLength: 4096,
 | 
						|
                          onChanged: notifier.setDescription,
 | 
						|
                          onTapOutside:
 | 
						|
                              (_) =>
 | 
						|
                                  FocusManager.instance.primaryFocus?.unfocus(),
 | 
						|
                        ),
 | 
						|
                        const Gap(12),
 | 
						|
                        _EndDatePicker(
 | 
						|
                          value: model.endedAt,
 | 
						|
                          onChanged: notifier.setEndedAt,
 | 
						|
                        ),
 | 
						|
                        const Gap(24),
 | 
						|
                        Row(
 | 
						|
                          children: [
 | 
						|
                            Text(
 | 
						|
                              'questions'.tr(),
 | 
						|
                              style: Theme.of(context).textTheme.titleLarge,
 | 
						|
                            ),
 | 
						|
                            const Spacer(),
 | 
						|
                            MenuAnchor(
 | 
						|
                              builder: (context, controller, child) {
 | 
						|
                                return FilledButton.icon(
 | 
						|
                                  onPressed: () {
 | 
						|
                                    controller.isOpen
 | 
						|
                                        ? controller.close()
 | 
						|
                                        : controller.open();
 | 
						|
                                  },
 | 
						|
                                  icon: const Icon(Icons.add),
 | 
						|
                                  label: Text('pollAddQuestion'.tr()),
 | 
						|
                                );
 | 
						|
                              },
 | 
						|
                              menuChildren:
 | 
						|
                                  SnPollQuestionType.values
 | 
						|
                                      .map(
 | 
						|
                                        (t) => MenuItemButton(
 | 
						|
                                          leadingIcon: Icon(_iconForType(t)),
 | 
						|
                                          onPressed:
 | 
						|
                                              () => notifier.addQuestion(t),
 | 
						|
                                          child: Text(_labelForType(t)),
 | 
						|
                                        ),
 | 
						|
                                      )
 | 
						|
                                      .toList(),
 | 
						|
                            ),
 | 
						|
                          ],
 | 
						|
                        ),
 | 
						|
                        const Gap(8),
 | 
						|
                        if (model.questions.isEmpty)
 | 
						|
                          _EmptyState(
 | 
						|
                            title: 'pollNoQuestionsYet'.tr(),
 | 
						|
                            subtitle: 'pollNoQuestionsHint'.tr(),
 | 
						|
                          )
 | 
						|
                        else
 | 
						|
                          ReorderableListView.builder(
 | 
						|
                            shrinkWrap: true,
 | 
						|
                            physics: const NeverScrollableScrollPhysics(),
 | 
						|
                            itemCount: model.questions.length,
 | 
						|
                            onReorder: (oldIndex, newIndex) {
 | 
						|
                              // Convert to stepwise moves using provided functions
 | 
						|
                              if (newIndex > oldIndex) newIndex -= 1;
 | 
						|
                              final steps = newIndex - oldIndex;
 | 
						|
                              if (steps == 0) return;
 | 
						|
                              if (steps > 0) {
 | 
						|
                                for (int i = 0; i < steps; i++) {
 | 
						|
                                  notifier.moveQuestionDown(oldIndex + i);
 | 
						|
                                }
 | 
						|
                              } else {
 | 
						|
                                for (int i = 0; i > steps; i--) {
 | 
						|
                                  notifier.moveQuestionUp(oldIndex + i);
 | 
						|
                                }
 | 
						|
                              }
 | 
						|
                            },
 | 
						|
                            buildDefaultDragHandles: false,
 | 
						|
                            itemBuilder: (context, index) {
 | 
						|
                              final q = model.questions[index];
 | 
						|
                              return Card(
 | 
						|
                                key: ValueKey('q_$index'),
 | 
						|
                                margin: const EdgeInsets.symmetric(vertical: 8),
 | 
						|
                                clipBehavior: Clip.antiAlias,
 | 
						|
                                child: Column(
 | 
						|
                                  children: [
 | 
						|
                                    _QuestionHeader(
 | 
						|
                                      index: index,
 | 
						|
                                      question: q,
 | 
						|
                                      onMoveUp:
 | 
						|
                                          index > 0
 | 
						|
                                              ? () =>
 | 
						|
                                                  notifier.moveQuestionUp(index)
 | 
						|
                                              : null,
 | 
						|
                                      onMoveDown:
 | 
						|
                                          index < model.questions.length - 1
 | 
						|
                                              ? () => notifier.moveQuestionDown(
 | 
						|
                                                index,
 | 
						|
                                              )
 | 
						|
                                              : null,
 | 
						|
                                      onDelete:
 | 
						|
                                          () => notifier.removeQuestion(index),
 | 
						|
                                    ),
 | 
						|
                                    const Divider(height: 1),
 | 
						|
                                    Padding(
 | 
						|
                                      padding: const EdgeInsets.all(16),
 | 
						|
                                      child: _QuestionEditor(
 | 
						|
                                        index: index,
 | 
						|
                                        question: q,
 | 
						|
                                      ),
 | 
						|
                                    ),
 | 
						|
                                  ],
 | 
						|
                                ),
 | 
						|
                              );
 | 
						|
                            },
 | 
						|
                          ),
 | 
						|
                        const Gap(96),
 | 
						|
                      ],
 | 
						|
                    ),
 | 
						|
                  ).center(),
 | 
						|
            ),
 | 
						|
          ),
 | 
						|
          Material(
 | 
						|
            elevation: 2,
 | 
						|
            color: Theme.of(context).colorScheme.surfaceContainer,
 | 
						|
            child:
 | 
						|
                ConstrainedBox(
 | 
						|
                  constraints: BoxConstraints(maxWidth: 640),
 | 
						|
                  child: Row(
 | 
						|
                    children: [
 | 
						|
                      OutlinedButton.icon(
 | 
						|
                        onPressed: () {
 | 
						|
                          Navigator.of(context).maybePop();
 | 
						|
                        },
 | 
						|
                        icon: const Icon(Icons.close),
 | 
						|
                        label: Text('cancel'.tr()),
 | 
						|
                      ),
 | 
						|
                      const Spacer(),
 | 
						|
                      FilledButton.icon(
 | 
						|
                        onPressed: () {
 | 
						|
                          _submitPoll(context, ref);
 | 
						|
                        },
 | 
						|
                        icon: const Icon(Icons.cloud_upload_outlined),
 | 
						|
                        label: Text(
 | 
						|
                          model.id == null ? 'create'.tr() : 'update'.tr(),
 | 
						|
                        ),
 | 
						|
                      ),
 | 
						|
                    ],
 | 
						|
                  ).padding(
 | 
						|
                    horizontal: 24,
 | 
						|
                    top: 16,
 | 
						|
                    bottom: MediaQuery.of(context).padding.bottom + 16,
 | 
						|
                  ),
 | 
						|
                ).center(),
 | 
						|
          ),
 | 
						|
        ],
 | 
						|
      ),
 | 
						|
    );
 | 
						|
  }
 | 
						|
 | 
						|
  void _showDebugPreview(BuildContext context, PollEditorState model) {
 | 
						|
    final buf = StringBuffer();
 | 
						|
    buf.writeln('{');
 | 
						|
    buf.writeln('  "title": ${_jsonStr(model.title)},');
 | 
						|
    buf.writeln('  "description": ${_jsonStr(model.description)},');
 | 
						|
    buf.writeln('  "endedAt": ${_jsonStr(model.endedAt?.toIso8601String())},');
 | 
						|
    buf.writeln('  "questions": [');
 | 
						|
    for (var i = 0; i < model.questions.length; i++) {
 | 
						|
      final q = model.questions[i];
 | 
						|
      buf.writeln('    {');
 | 
						|
      buf.writeln('      "type": "${q.type.name}",');
 | 
						|
      buf.writeln('      "title": ${_jsonStr(q.title)},');
 | 
						|
      buf.writeln('      "description": ${_jsonStr(q.description)},');
 | 
						|
      buf.writeln('      "order": ${q.order},');
 | 
						|
      buf.writeln('      "isRequired": ${q.isRequired},');
 | 
						|
      if (q.options != null) {
 | 
						|
        buf.writeln('      "options": [');
 | 
						|
        for (var j = 0; j < q.options!.length; j++) {
 | 
						|
          final o = q.options![j];
 | 
						|
          buf.writeln(
 | 
						|
            '        { "label": ${_jsonStr(o.label)}, "description": ${_jsonStr(o.description)}, "order": ${o.order} }${j == q.options!.length - 1 ? '' : ','}',
 | 
						|
          );
 | 
						|
        }
 | 
						|
        buf.writeln('      ]');
 | 
						|
      } else {
 | 
						|
        buf.writeln('      "options": null');
 | 
						|
      }
 | 
						|
      buf.writeln('    }${i == model.questions.length - 1 ? '' : ','}');
 | 
						|
    }
 | 
						|
    buf.writeln('  ]');
 | 
						|
    buf.writeln('}');
 | 
						|
    showDialog(
 | 
						|
      context: context,
 | 
						|
      builder:
 | 
						|
          (_) => AlertDialog(
 | 
						|
            title: Text('pollDebugPreview'.tr()),
 | 
						|
            content: SingleChildScrollView(
 | 
						|
              child: SelectableText(buf.toString()),
 | 
						|
            ),
 | 
						|
            actions: [
 | 
						|
              TextButton(
 | 
						|
                onPressed: () => Navigator.of(context).pop(),
 | 
						|
                child: Text('close'.tr()),
 | 
						|
              ),
 | 
						|
            ],
 | 
						|
          ),
 | 
						|
    );
 | 
						|
  }
 | 
						|
}
 | 
						|
 | 
						|
String _jsonStr(String? v) =>
 | 
						|
    v == null ? 'null' : '"${v.replaceAll('"', '\\"')}"';
 | 
						|
 | 
						|
IconData _iconForType(SnPollQuestionType t) {
 | 
						|
  switch (t) {
 | 
						|
    case SnPollQuestionType.singleChoice:
 | 
						|
      return Icons.radio_button_checked;
 | 
						|
    case SnPollQuestionType.multipleChoice:
 | 
						|
      return Icons.check_box;
 | 
						|
    case SnPollQuestionType.freeText:
 | 
						|
      return Icons.short_text;
 | 
						|
    case SnPollQuestionType.yesNo:
 | 
						|
      return Icons.toggle_on;
 | 
						|
    case SnPollQuestionType.rating:
 | 
						|
      return Icons.star_rate;
 | 
						|
  }
 | 
						|
}
 | 
						|
 | 
						|
String _labelForType(SnPollQuestionType t) {
 | 
						|
  switch (t) {
 | 
						|
    case SnPollQuestionType.singleChoice:
 | 
						|
      return 'pollQuestionTypeSingleChoice'.tr();
 | 
						|
    case SnPollQuestionType.multipleChoice:
 | 
						|
      return 'pollQuestionTypeMultipleChoice'.tr();
 | 
						|
    case SnPollQuestionType.freeText:
 | 
						|
      return 'pollQuestionTypeFreeText'.tr();
 | 
						|
    case SnPollQuestionType.yesNo:
 | 
						|
      return 'pollQuestionTypeYesNo'.tr();
 | 
						|
    case SnPollQuestionType.rating:
 | 
						|
      return 'pollQuestionTypeRating'.tr();
 | 
						|
  }
 | 
						|
}
 | 
						|
 | 
						|
/// End date and time picker row
 | 
						|
class _EndDatePicker extends StatelessWidget {
 | 
						|
  const _EndDatePicker({required this.value, required this.onChanged});
 | 
						|
 | 
						|
  final DateTime? value;
 | 
						|
  final ValueChanged<DateTime?> onChanged;
 | 
						|
 | 
						|
  @override
 | 
						|
  Widget build(BuildContext context) {
 | 
						|
    return Row(
 | 
						|
      children: [
 | 
						|
        Expanded(
 | 
						|
          child: InputDecorator(
 | 
						|
            decoration: InputDecoration(
 | 
						|
              labelText: 'pollEndDateOptional'.tr(),
 | 
						|
              border: OutlineInputBorder(
 | 
						|
                borderRadius: BorderRadius.all(Radius.circular(16)),
 | 
						|
              ),
 | 
						|
            ),
 | 
						|
            child: Wrap(
 | 
						|
              spacing: 8,
 | 
						|
              crossAxisAlignment: WrapCrossAlignment.center,
 | 
						|
              children: [
 | 
						|
                Icon(Icons.event, color: Theme.of(context).colorScheme.primary),
 | 
						|
                Text(
 | 
						|
                  value == null
 | 
						|
                      ? 'notSet'.tr()
 | 
						|
                      : MaterialLocalizations.of(
 | 
						|
                        context,
 | 
						|
                      ).formatFullDate(value!),
 | 
						|
                ),
 | 
						|
                if (value != null) ...[
 | 
						|
                  const Text('—'),
 | 
						|
                  Text(
 | 
						|
                    MaterialLocalizations.of(context).formatTimeOfDay(
 | 
						|
                      TimeOfDay.fromDateTime(value!),
 | 
						|
                      alwaysUse24HourFormat: true,
 | 
						|
                    ),
 | 
						|
                  ),
 | 
						|
                ],
 | 
						|
                const Gap(8),
 | 
						|
                TextButton(
 | 
						|
                  onPressed: () async {
 | 
						|
                    final now = DateTime.now();
 | 
						|
                    final initial = value ?? now.add(const Duration(days: 1));
 | 
						|
                    final pickedDate = await showDatePicker(
 | 
						|
                      context: context,
 | 
						|
                      initialDate: initial,
 | 
						|
                      firstDate: now,
 | 
						|
                      lastDate: now.add(const Duration(days: 3650)),
 | 
						|
                    );
 | 
						|
                    if (pickedDate == null) return;
 | 
						|
                    if (!context.mounted) return;
 | 
						|
                    final pickedTime = await showTimePicker(
 | 
						|
                      context: context,
 | 
						|
                      initialTime: TimeOfDay.fromDateTime(initial),
 | 
						|
                      builder: (ctx, child) {
 | 
						|
                        return MediaQuery(
 | 
						|
                          data: MediaQuery.of(
 | 
						|
                            ctx,
 | 
						|
                          ).copyWith(alwaysUse24HourFormat: true),
 | 
						|
                          child: child!,
 | 
						|
                        );
 | 
						|
                      },
 | 
						|
                    );
 | 
						|
                    final dt = DateTime(
 | 
						|
                      pickedDate.year,
 | 
						|
                      pickedDate.month,
 | 
						|
                      pickedDate.day,
 | 
						|
                      pickedTime?.hour ?? 0,
 | 
						|
                      pickedTime?.minute ?? 0,
 | 
						|
                    );
 | 
						|
                    onChanged(dt);
 | 
						|
                  },
 | 
						|
                  child: Text('pick'.tr()),
 | 
						|
                ),
 | 
						|
                if (value != null)
 | 
						|
                  TextButton(
 | 
						|
                    onPressed: () => onChanged(null),
 | 
						|
                    child: Text('clear'.tr()),
 | 
						|
                  ),
 | 
						|
              ],
 | 
						|
            ),
 | 
						|
          ),
 | 
						|
        ),
 | 
						|
      ],
 | 
						|
    );
 | 
						|
  }
 | 
						|
}
 | 
						|
 | 
						|
/// Question card header with actions
 | 
						|
class _QuestionHeader extends StatelessWidget {
 | 
						|
  const _QuestionHeader({
 | 
						|
    required this.index,
 | 
						|
    required this.question,
 | 
						|
    this.onMoveUp,
 | 
						|
    this.onMoveDown,
 | 
						|
    this.onDelete,
 | 
						|
  });
 | 
						|
 | 
						|
  final int index;
 | 
						|
  final SnPollQuestion question;
 | 
						|
  final VoidCallback? onMoveUp;
 | 
						|
  final VoidCallback? onMoveDown;
 | 
						|
  final VoidCallback? onDelete;
 | 
						|
 | 
						|
  @override
 | 
						|
  Widget build(BuildContext context) {
 | 
						|
    return ListTile(
 | 
						|
      leading: ReorderableDragStartListener(
 | 
						|
        index: index,
 | 
						|
        child: const Icon(Icons.drag_handle),
 | 
						|
      ),
 | 
						|
      title: Text(
 | 
						|
        question.title.isEmpty ? 'pollUntitledQuestion'.tr() : question.title,
 | 
						|
        maxLines: 1,
 | 
						|
        overflow: TextOverflow.ellipsis,
 | 
						|
      ),
 | 
						|
      subtitle: Text(_labelForType(question.type)),
 | 
						|
      trailing: Wrap(
 | 
						|
        spacing: 4,
 | 
						|
        children: [
 | 
						|
          IconButton(
 | 
						|
            tooltip: 'moveUp'.tr(),
 | 
						|
            onPressed: onMoveUp,
 | 
						|
            icon: const Icon(Icons.arrow_upward),
 | 
						|
          ),
 | 
						|
          IconButton(
 | 
						|
            tooltip: 'moveDown'.tr(),
 | 
						|
            onPressed: onMoveDown,
 | 
						|
            icon: const Icon(Icons.arrow_downward),
 | 
						|
          ),
 | 
						|
          IconButton(
 | 
						|
            tooltip: 'delete'.tr(),
 | 
						|
            onPressed: onDelete,
 | 
						|
            icon: const Icon(Icons.delete_outline),
 | 
						|
            color: Theme.of(context).colorScheme.error,
 | 
						|
          ),
 | 
						|
        ],
 | 
						|
      ),
 | 
						|
    );
 | 
						|
  }
 | 
						|
}
 | 
						|
 | 
						|
/// Question details editor
 | 
						|
class _QuestionEditor extends ConsumerWidget {
 | 
						|
  const _QuestionEditor({required this.index, required this.question});
 | 
						|
 | 
						|
  final int index;
 | 
						|
  final SnPollQuestion question;
 | 
						|
 | 
						|
  @override
 | 
						|
  Widget build(BuildContext context, WidgetRef ref) {
 | 
						|
    final notifier = ref.watch(pollEditorProvider.notifier);
 | 
						|
 | 
						|
    return Column(
 | 
						|
      crossAxisAlignment: CrossAxisAlignment.start,
 | 
						|
      children: [
 | 
						|
        Wrap(
 | 
						|
          spacing: 12,
 | 
						|
          runSpacing: 12,
 | 
						|
          crossAxisAlignment: WrapCrossAlignment.center,
 | 
						|
          children: [
 | 
						|
            _QuestionTypePicker(
 | 
						|
              value: question.type,
 | 
						|
              onChanged: (t) => notifier.setQuestionType(index, t),
 | 
						|
            ),
 | 
						|
            FilterChip(
 | 
						|
              label: Text('required'.tr()),
 | 
						|
              selected: question.isRequired,
 | 
						|
              onSelected: (v) => notifier.setQuestionRequired(index, v),
 | 
						|
              avatar: Icon(
 | 
						|
                question.isRequired
 | 
						|
                    ? Icons.check_circle
 | 
						|
                    : Icons.radio_button_unchecked,
 | 
						|
              ),
 | 
						|
            ),
 | 
						|
          ],
 | 
						|
        ),
 | 
						|
        const Gap(12),
 | 
						|
        TextFormField(
 | 
						|
          initialValue: question.title,
 | 
						|
          decoration: InputDecoration(
 | 
						|
            labelText: 'pollQuestionTitle'.tr(),
 | 
						|
            border: OutlineInputBorder(
 | 
						|
              borderRadius: BorderRadius.all(Radius.circular(16)),
 | 
						|
            ),
 | 
						|
          ),
 | 
						|
          textInputAction: TextInputAction.next,
 | 
						|
          maxLength: 1024,
 | 
						|
          onChanged: (v) => notifier.setQuestionTitle(index, v),
 | 
						|
          onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(),
 | 
						|
          validator: (v) {
 | 
						|
            if (v == null || v.trim().isEmpty) {
 | 
						|
              return 'pollQuestionTitleRequired'.tr();
 | 
						|
            }
 | 
						|
            return null;
 | 
						|
          },
 | 
						|
        ),
 | 
						|
        const Gap(12),
 | 
						|
        TextFormField(
 | 
						|
          initialValue: question.description ?? '',
 | 
						|
          decoration: InputDecoration(
 | 
						|
            labelText: 'pollQuestionDescriptionOptional'.tr(),
 | 
						|
            border: OutlineInputBorder(
 | 
						|
              borderRadius: BorderRadius.all(Radius.circular(16)),
 | 
						|
            ),
 | 
						|
          ),
 | 
						|
          maxLines: 2,
 | 
						|
          maxLength: 4096,
 | 
						|
          onChanged:
 | 
						|
              (v) =>
 | 
						|
                  notifier.setQuestionDescription(index, v.isEmpty ? null : v),
 | 
						|
          onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(),
 | 
						|
        ),
 | 
						|
        if (question.options != null) ...[
 | 
						|
          const Gap(16),
 | 
						|
          Text('options'.tr(), style: Theme.of(context).textTheme.titleMedium),
 | 
						|
          const Gap(8),
 | 
						|
          _OptionsEditor(index: index, options: question.options!),
 | 
						|
          const Gap(4),
 | 
						|
          Align(
 | 
						|
            alignment: Alignment.centerLeft,
 | 
						|
            child: OutlinedButton.icon(
 | 
						|
              onPressed: () => notifier.addOption(index),
 | 
						|
              icon: const Icon(Icons.add),
 | 
						|
              label: Text('pollAddOption'.tr()),
 | 
						|
            ),
 | 
						|
          ),
 | 
						|
        ],
 | 
						|
        if (question.options == null &&
 | 
						|
            (question.type == SnPollQuestionType.freeText ||
 | 
						|
                question.type == SnPollQuestionType.rating ||
 | 
						|
                question.type == SnPollQuestionType.yesNo)) ...[
 | 
						|
          const Gap(16),
 | 
						|
          _TextAnswerPreview(long: false),
 | 
						|
        ],
 | 
						|
      ],
 | 
						|
    );
 | 
						|
  }
 | 
						|
}
 | 
						|
 | 
						|
class _QuestionTypePicker extends StatelessWidget {
 | 
						|
  const _QuestionTypePicker({required this.value, required this.onChanged});
 | 
						|
 | 
						|
  final SnPollQuestionType value;
 | 
						|
  final ValueChanged<SnPollQuestionType> onChanged;
 | 
						|
 | 
						|
  @override
 | 
						|
  Widget build(BuildContext context) {
 | 
						|
    return DropdownButtonFormField<SnPollQuestionType>(
 | 
						|
      value: value,
 | 
						|
      decoration: InputDecoration(
 | 
						|
        labelText: 'Type'.tr(),
 | 
						|
        border: OutlineInputBorder(
 | 
						|
          borderRadius: BorderRadius.all(Radius.circular(16)),
 | 
						|
        ),
 | 
						|
      ),
 | 
						|
      items:
 | 
						|
          SnPollQuestionType.values
 | 
						|
              .map(
 | 
						|
                (t) => DropdownMenuItem(
 | 
						|
                  value: t,
 | 
						|
                  child: Row(
 | 
						|
                    children: [
 | 
						|
                      Icon(_iconForType(t)),
 | 
						|
                      const Gap(8),
 | 
						|
                      Text(_labelForType(t)),
 | 
						|
                    ],
 | 
						|
                  ),
 | 
						|
                ),
 | 
						|
              )
 | 
						|
              .toList(),
 | 
						|
      onChanged: (t) {
 | 
						|
        if (t != null) onChanged(t);
 | 
						|
      },
 | 
						|
    );
 | 
						|
  }
 | 
						|
}
 | 
						|
 | 
						|
class _OptionsEditor extends ConsumerWidget {
 | 
						|
  const _OptionsEditor({required this.index, required this.options});
 | 
						|
 | 
						|
  final int index;
 | 
						|
  final List<SnPollOption> options;
 | 
						|
 | 
						|
  @override
 | 
						|
  Widget build(BuildContext context, WidgetRef ref) {
 | 
						|
    final notifier = ref.watch(pollEditorProvider.notifier);
 | 
						|
 | 
						|
    return Column(
 | 
						|
      children: [
 | 
						|
        for (var i = 0; i < options.length; i++)
 | 
						|
          Padding(
 | 
						|
            padding: const EdgeInsets.symmetric(vertical: 6),
 | 
						|
            child: Row(
 | 
						|
              crossAxisAlignment: CrossAxisAlignment.start,
 | 
						|
              children: [
 | 
						|
                Expanded(
 | 
						|
                  child: TextFormField(
 | 
						|
                    key: ValueKey(options[i].id),
 | 
						|
                    initialValue: options[i].label,
 | 
						|
                    decoration: InputDecoration(
 | 
						|
                      labelText: 'pollOptionLabel'.tr(),
 | 
						|
                      border: OutlineInputBorder(
 | 
						|
                        borderRadius: BorderRadius.all(Radius.circular(16)),
 | 
						|
                      ),
 | 
						|
                    ),
 | 
						|
                    onChanged: (v) => notifier.setOptionLabel(index, i, v),
 | 
						|
                    onTapOutside:
 | 
						|
                        (_) => FocusManager.instance.primaryFocus?.unfocus(),
 | 
						|
                    inputFormatters: [LengthLimitingTextInputFormatter(1024)],
 | 
						|
                  ),
 | 
						|
                ),
 | 
						|
                const Gap(8),
 | 
						|
                SizedBox(
 | 
						|
                  width: 40,
 | 
						|
                  child: IconButton(
 | 
						|
                    tooltip: 'moveUp'.tr(),
 | 
						|
                    onPressed:
 | 
						|
                        i > 0 ? () => notifier.moveOptionUp(index, i) : null,
 | 
						|
                    icon: const Icon(Icons.arrow_upward),
 | 
						|
                  ),
 | 
						|
                ),
 | 
						|
                SizedBox(
 | 
						|
                  width: 40,
 | 
						|
                  child: IconButton(
 | 
						|
                    tooltip: 'moveDown'.tr(),
 | 
						|
                    onPressed:
 | 
						|
                        i < options.length - 1
 | 
						|
                            ? () => notifier.moveOptionDown(index, i)
 | 
						|
                            : null,
 | 
						|
                    icon: const Icon(Icons.arrow_downward),
 | 
						|
                  ),
 | 
						|
                ),
 | 
						|
                SizedBox(
 | 
						|
                  width: 40,
 | 
						|
                  child: IconButton(
 | 
						|
                    tooltip: 'delete'.tr(),
 | 
						|
                    onPressed: () => notifier.removeOption(index, i),
 | 
						|
                    icon: const Icon(Icons.close),
 | 
						|
                  ),
 | 
						|
                ),
 | 
						|
              ],
 | 
						|
            ),
 | 
						|
          ),
 | 
						|
      ],
 | 
						|
    );
 | 
						|
  }
 | 
						|
}
 | 
						|
 | 
						|
class _TextAnswerPreview extends StatelessWidget {
 | 
						|
  const _TextAnswerPreview({required this.long});
 | 
						|
 | 
						|
  final bool long;
 | 
						|
 | 
						|
  @override
 | 
						|
  Widget build(BuildContext context) {
 | 
						|
    return TextField(
 | 
						|
      enabled: false,
 | 
						|
      maxLines: long ? 4 : 1,
 | 
						|
      decoration: InputDecoration(
 | 
						|
        labelText:
 | 
						|
            long
 | 
						|
                ? 'pollLongTextAnswerPreview'.tr()
 | 
						|
                : 'pollShortTextAnswerPreview'.tr(),
 | 
						|
        border: const OutlineInputBorder(
 | 
						|
          borderRadius: BorderRadius.all(Radius.circular(16)),
 | 
						|
        ),
 | 
						|
      ),
 | 
						|
    );
 | 
						|
  }
 | 
						|
}
 | 
						|
 | 
						|
class _EmptyState extends StatelessWidget {
 | 
						|
  const _EmptyState({required this.title, required this.subtitle});
 | 
						|
 | 
						|
  final String title;
 | 
						|
  final String subtitle;
 | 
						|
 | 
						|
  @override
 | 
						|
  Widget build(BuildContext context) {
 | 
						|
    return Container(
 | 
						|
      padding: const EdgeInsets.all(24),
 | 
						|
      decoration: BoxDecoration(
 | 
						|
        border: Border.all(color: Theme.of(context).dividerColor),
 | 
						|
        borderRadius: BorderRadius.circular(12),
 | 
						|
      ),
 | 
						|
      child: Row(
 | 
						|
        children: [
 | 
						|
          Icon(
 | 
						|
            Icons.help_outline,
 | 
						|
            color: Theme.of(context).colorScheme.primary,
 | 
						|
          ),
 | 
						|
          const Gap(12),
 | 
						|
          Expanded(
 | 
						|
            child: Column(
 | 
						|
              crossAxisAlignment: CrossAxisAlignment.start,
 | 
						|
              children: [
 | 
						|
                Text(
 | 
						|
                  'pollNoQuestionsYet'.tr(),
 | 
						|
                  style: Theme.of(context).textTheme.titleMedium,
 | 
						|
                ),
 | 
						|
                const Gap(4),
 | 
						|
                Text(
 | 
						|
                  'pollNoQuestionsHint'.tr(),
 | 
						|
                  style: Theme.of(context).textTheme.bodyMedium,
 | 
						|
                ),
 | 
						|
              ],
 | 
						|
            ),
 | 
						|
          ),
 | 
						|
        ],
 | 
						|
      ),
 | 
						|
    );
 | 
						|
  }
 | 
						|
}
 |