feat(front-hall): implement three-mode layout with quick links panel
Phase 2 of Organizr Migration - Front Hall restructure: - Add FrontHallState provider with three modes (dashboard, iframe, settings) - Create QuickLinksPanel widget with categorized links and overflow menus - Add IframeView with platform-aware implementation (web iframe, mobile fallback) - Extract DashboardContent from FrontHallPage - Add QuickLinkSettingsContent placeholder for Phase 4 link editor - Implement QuickLink entity and data layer with Core API datasource - Add default quick links fallback when API unavailable - Update UI_LAYOUT.md with Front Hall panel configurations 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.5
parent
4f8d0f521f
commit
40db92d9d5
@@ -0,0 +1,93 @@
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||
import 'package:tatlock_ui/core/api/api_client.dart';
|
||||
import 'package:tatlock_ui/features/front_hall/data/models/quick_link_model.dart';
|
||||
import 'package:tatlock_ui/features/front_hall/domain/entities/quick_link.dart';
|
||||
|
||||
part 'quick_links_datasource.g.dart';
|
||||
|
||||
/// Data source for quick link operations.
|
||||
///
|
||||
/// Provides CRUD operations for Front Hall quick links via Core API.
|
||||
class QuickLinksDatasource {
|
||||
QuickLinksDatasource(this._dio);
|
||||
|
||||
final Dio _dio;
|
||||
|
||||
static const _basePath = '/front-hall/quick-links';
|
||||
|
||||
/// Gets all quick links.
|
||||
Future<List<QuickLink>> getQuickLinks() async {
|
||||
final response = await _dio.get<List<dynamic>>(_basePath);
|
||||
final data = response.data ?? [];
|
||||
|
||||
return data
|
||||
.map((json) => QuickLinkModel.fromJson(json as Map<String, dynamic>))
|
||||
.map((model) => model.toEntity())
|
||||
.toList();
|
||||
}
|
||||
|
||||
/// Gets a single quick link by ID.
|
||||
Future<QuickLink> getQuickLink(String id) async {
|
||||
final response = await _dio.get<Map<String, dynamic>>('$_basePath/$id');
|
||||
final data = response.data;
|
||||
|
||||
if (data == null) {
|
||||
throw Exception('Quick link not found: $id');
|
||||
}
|
||||
|
||||
return QuickLinkModel.fromJson(data).toEntity();
|
||||
}
|
||||
|
||||
/// Creates a new quick link.
|
||||
Future<QuickLink> createQuickLink(QuickLink link) async {
|
||||
final model = QuickLinkModel.fromEntity(link);
|
||||
final response = await _dio.post<Map<String, dynamic>>(
|
||||
_basePath,
|
||||
data: model.toJson(),
|
||||
);
|
||||
final data = response.data;
|
||||
|
||||
if (data == null) {
|
||||
throw Exception('Failed to create quick link');
|
||||
}
|
||||
|
||||
return QuickLinkModel.fromJson(data).toEntity();
|
||||
}
|
||||
|
||||
/// Updates an existing quick link.
|
||||
Future<QuickLink> updateQuickLink(QuickLink link) async {
|
||||
final model = QuickLinkModel.fromEntity(link);
|
||||
final response = await _dio.put<Map<String, dynamic>>(
|
||||
'$_basePath/${link.id}',
|
||||
data: model.toJson(),
|
||||
);
|
||||
final data = response.data;
|
||||
|
||||
if (data == null) {
|
||||
throw Exception('Failed to update quick link');
|
||||
}
|
||||
|
||||
return QuickLinkModel.fromJson(data).toEntity();
|
||||
}
|
||||
|
||||
/// Deletes a quick link.
|
||||
Future<void> deleteQuickLink(String id) async {
|
||||
await _dio.delete<void>('$_basePath/$id');
|
||||
}
|
||||
|
||||
/// Reorders quick links by updating sort orders.
|
||||
Future<void> reorderQuickLinks(List<String> orderedIds) async {
|
||||
await _dio.put<void>(
|
||||
'$_basePath/reorder',
|
||||
data: {'ids': orderedIds},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Provides the quick links datasource.
|
||||
@riverpod
|
||||
QuickLinksDatasource quickLinksDatasource(Ref ref) {
|
||||
final dio = ref.watch(coreApiClientProvider);
|
||||
return QuickLinksDatasource(dio);
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||
import 'package:tatlock_ui/features/front_hall/domain/entities/quick_link.dart';
|
||||
|
||||
part 'quick_link_model.freezed.dart';
|
||||
part 'quick_link_model.g.dart';
|
||||
|
||||
/// Quick link data model for API serialization.
|
||||
@freezed
|
||||
sealed class QuickLinkModel with _$QuickLinkModel {
|
||||
const factory QuickLinkModel({
|
||||
required String id,
|
||||
required String name,
|
||||
required String url,
|
||||
@JsonKey(name: 'icon_name') required String iconName,
|
||||
String? category,
|
||||
@Default('iframe') String type,
|
||||
@JsonKey(name: 'sort_order') @Default(0) int sortOrder,
|
||||
@JsonKey(name: 'is_active') @Default(true) bool isActive,
|
||||
}) = _QuickLinkModel;
|
||||
|
||||
const QuickLinkModel._();
|
||||
|
||||
factory QuickLinkModel.fromJson(Map<String, dynamic> json) =>
|
||||
_$QuickLinkModelFromJson(json);
|
||||
|
||||
/// Converts to domain entity.
|
||||
QuickLink toEntity() {
|
||||
return QuickLink(
|
||||
id: id,
|
||||
name: name,
|
||||
url: url,
|
||||
iconName: iconName,
|
||||
category: category,
|
||||
type: _parseType(type),
|
||||
sortOrder: sortOrder,
|
||||
isActive: isActive,
|
||||
);
|
||||
}
|
||||
|
||||
/// Creates model from domain entity.
|
||||
factory QuickLinkModel.fromEntity(QuickLink entity) {
|
||||
return QuickLinkModel(
|
||||
id: entity.id,
|
||||
name: entity.name,
|
||||
url: entity.url,
|
||||
iconName: entity.iconName,
|
||||
category: entity.category,
|
||||
type: entity.type == QuickLinkType.iframe ? 'iframe' : 'new_tab',
|
||||
sortOrder: entity.sortOrder,
|
||||
isActive: entity.isActive,
|
||||
);
|
||||
}
|
||||
|
||||
QuickLinkType _parseType(String type) {
|
||||
return switch (type.toLowerCase()) {
|
||||
'iframe' => QuickLinkType.iframe,
|
||||
'new_tab' || 'newtab' => QuickLinkType.newTab,
|
||||
_ => QuickLinkType.iframe,
|
||||
};
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user