feat: deploy infrastructure + disk/drive, calendar, presentation, workspaces, onboarding, demo user

This commit is contained in:
2026-05-19 16:26:26 +03:00
parent 688d043dad
commit 3529c39b22
304 changed files with 18826 additions and 1176 deletions
+176
View File
@@ -0,0 +1,176 @@
import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../core/api/client.dart';
import '../core/api/endpoints.dart';
import '../core/api/exceptions.dart';
import '../models/auth.dart';
enum AuthStatus { initial, authenticated, unauthenticated, loading }
class AuthState {
final AuthStatus status;
final User? user;
final String? error;
final bool needsTwoFactor;
AuthState({
this.status = AuthStatus.initial,
this.user,
this.error,
this.needsTwoFactor = false,
});
AuthState copyWith({
AuthStatus? status,
User? user,
String? error,
bool? needsTwoFactor,
}) =>
AuthState(
status: status ?? this.status,
user: user ?? this.user,
error: error,
needsTwoFactor: needsTwoFactor ?? this.needsTwoFactor,
);
}
class AuthNotifier extends StateNotifier<AuthState> {
final ApiClient _api;
AuthNotifier(this._api) : super(AuthState()) {
_checkAuth();
}
Future<void> _checkAuth() async {
final token = await _api.getAccessToken();
if (token != null) {
state = state.copyWith(status: AuthStatus.authenticated);
} else {
state = state.copyWith(status: AuthStatus.unauthenticated);
}
}
Future<void> login(String email, String password) async {
state = state.copyWith(status: AuthStatus.loading, error: null);
try {
final response = await _api.post(Endpoints.login, data: {
'email': email,
'password': password,
});
final authResponse = AuthResponse.fromJson(
response.data as Map<String, dynamic>);
if (authResponse.user.isTwoFactorEnabled) {
state = state.copyWith(
status: AuthStatus.unauthenticated,
needsTwoFactor: true,
);
return;
}
await _api.saveTokens(
authResponse.accessToken, authResponse.refreshToken);
state = state.copyWith(
status: AuthStatus.authenticated,
user: authResponse.user,
);
} on ApiException catch (e) {
state = state.copyWith(
status: AuthStatus.unauthenticated,
error: e.message,
);
}
}
Future<void> register(String email, String password, String name) async {
state = state.copyWith(status: AuthStatus.loading, error: null);
try {
final response = await _api.post(Endpoints.register, data: {
'email': email,
'password': password,
'name': name,
});
final authResponse = AuthResponse.fromJson(
response.data as Map<String, dynamic>);
await _api.saveTokens(
authResponse.accessToken, authResponse.refreshToken);
state = state.copyWith(
status: AuthStatus.authenticated,
user: authResponse.user,
);
} on ApiException catch (e) {
state = state.copyWith(
status: AuthStatus.unauthenticated,
error: e.message,
);
}
}
Future<void> verifyTwoFactor(String code) async {
state = state.copyWith(status: AuthStatus.loading, error: null);
try {
final response = await _api.post(Endpoints.twoFactor, data: {
'code': code,
});
final authResponse = AuthResponse.fromJson(
response.data as Map<String, dynamic>);
await _api.saveTokens(
authResponse.accessToken, authResponse.refreshToken);
state = state.copyWith(
status: AuthStatus.authenticated,
user: authResponse.user,
needsTwoFactor: false,
);
} on ApiException catch (e) {
state = state.copyWith(
status: AuthStatus.unauthenticated,
error: e.message,
);
}
}
Future<void> logout() async {
await _api.clearTokens();
state = AuthState(status: AuthStatus.unauthenticated);
}
Future<void> forgotPassword(String email) async {
state = state.copyWith(status: AuthStatus.loading, error: null);
try {
await _api.post(Endpoints.forgotPassword, data: {'email': email});
state = state.copyWith(status: AuthStatus.unauthenticated);
} on ApiException catch (e) {
state = state.copyWith(
status: AuthStatus.unauthenticated, error: e.message);
}
}
Future<void> resetPassword(String token, String password) async {
state = state.copyWith(status: AuthStatus.loading, error: null);
try {
await _api.post(Endpoints.resetPassword, data: {
'token': token,
'password': password,
});
state = state.copyWith(status: AuthStatus.unauthenticated);
} on ApiException catch (e) {
state = state.copyWith(
status: AuthStatus.unauthenticated, error: e.message);
}
}
Future<String?> getOAuthUrl(String provider) async {
try {
final response = await _api.get('${Endpoints.oauth}/$provider');
return response.data['url'] as String?;
} catch (_) {
return null;
}
}
void clearError() {
state = state.copyWith(error: null);
}
}
final authProvider = StateNotifierProvider<AuthNotifier, AuthState>((ref) {
final api = ref.read(apiClientProvider);
return AuthNotifier(api);
});
+83
View File
@@ -0,0 +1,83 @@
import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../api/disk.dart';
import '../core/api/client.dart';
class DiskState {
final List<String> availableProviders;
final List<Map<String, dynamic>> connectedProviders;
final bool isLoading;
final String? error;
DiskState({
this.availableProviders = const [],
this.connectedProviders = const [],
this.isLoading = false,
this.error,
});
DiskState copyWith({
List<String>? availableProviders,
List<Map<String, dynamic>>? connectedProviders,
bool? isLoading,
String? error,
}) =>
DiskState(
availableProviders: availableProviders ?? this.availableProviders,
connectedProviders: connectedProviders ?? this.connectedProviders,
isLoading: isLoading ?? this.isLoading,
error: error,
);
bool isConnected(String provider) =>
connectedProviders.any((p) => p['provider'] == provider);
}
class DiskNotifier extends StateNotifier<DiskState> {
final DiskApi _api;
DiskNotifier(this._api) : super(DiskState());
Future<void> load() async {
state = state.copyWith(isLoading: true, error: null);
try {
final providers = await _api.getProviders();
final status = await _api.getStatus();
final connected = (status['connected'] as List<dynamic>? ?? [])
.map((e) => e as Map<String, dynamic>)
.toList();
state = state.copyWith(
availableProviders: providers,
connectedProviders: connected,
isLoading: false,
);
} catch (e) {
state = state.copyWith(isLoading: false, error: e.toString());
}
}
Future<bool> connect(String provider, String code) async {
final ok = await _api.connect(provider, code);
if (ok) await load();
return ok;
}
Future<bool> disconnect(String provider) async {
final ok = await _api.disconnect(provider);
if (ok) {
state = state.copyWith(
connectedProviders: state.connectedProviders
.where((p) => p['provider'] != provider)
.toList(),
);
}
return ok;
}
Future<String?> getOAuthUrl(String provider) =>
_api.getOAuthUrl(provider);
}
final diskProvider = StateNotifierProvider<DiskNotifier, DiskState>((ref) {
final api = ref.read(apiClientProvider);
return DiskNotifier(DiskApi(api));
});
+269
View File
@@ -0,0 +1,269 @@
import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../core/api/client.dart';
import '../core/api/endpoints.dart';
import '../core/api/exceptions.dart';
import '../models/idea.dart';
import '../models/collaboration.dart';
class IdeasState {
final List<Idea> ideas;
final Idea? selectedIdea;
final bool isLoading;
final String? error;
final String viewMode;
IdeasState({
this.ideas = const [],
this.selectedIdea,
this.isLoading = false,
this.error,
this.viewMode = 'list',
});
IdeasState copyWith({
List<Idea>? ideas,
Idea? selectedIdea,
bool? isLoading,
String? error,
String? viewMode,
}) =>
IdeasState(
ideas: ideas ?? this.ideas,
selectedIdea: selectedIdea ?? this.selectedIdea,
isLoading: isLoading ?? this.isLoading,
error: error,
viewMode: viewMode ?? this.viewMode,
);
}
class IdeasNotifier extends StateNotifier<IdeasState> {
final ApiClient _api;
IdeasNotifier(this._api) : super(IdeasState());
Future<void> loadIdeas({String? workspaceId}) async {
state = state.copyWith(isLoading: true, error: null);
try {
final params = <String, dynamic>{};
if (workspaceId != null) params['workspace_id'] = workspaceId;
final response = await _api.get(Endpoints.ideas, params: params);
final list = (response.data as List<dynamic>)
.map((e) => Idea.fromJson(e as Map<String, dynamic>))
.toList();
state = state.copyWith(ideas: list, isLoading: false);
} on ApiException catch (e) {
state = state.copyWith(isLoading: false, error: e.message);
}
}
Future<void> loadIdea(String id) async {
state = state.copyWith(isLoading: true, error: null);
try {
final response = await _api.get(Endpoints.idea(id));
final idea =
Idea.fromJson(response.data as Map<String, dynamic>);
state = state.copyWith(selectedIdea: idea, isLoading: false);
} on ApiException catch (e) {
state = state.copyWith(isLoading: false, error: e.message);
}
}
Future<bool> createIdea(CreateIdeaRequest request) async {
try {
final response = await _api.post(Endpoints.ideas, data: request.toJson());
final idea =
Idea.fromJson(response.data as Map<String, dynamic>);
state = state.copyWith(
ideas: [idea, ...state.ideas],
selectedIdea: idea,
);
return true;
} on ApiException catch (e) {
state = state.copyWith(error: e.message);
return false;
}
}
Future<bool> updateIdea(String id, UpdateIdeaRequest request) async {
try {
final response =
await _api.patch(Endpoints.idea(id), data: request.toJson());
final updated =
Idea.fromJson(response.data as Map<String, dynamic>);
state = state.copyWith(
ideas: state.ideas.map((i) => i.id == id ? updated : i).toList(),
selectedIdea: updated,
);
return true;
} on ApiException catch (e) {
state = state.copyWith(error: e.message);
return false;
}
}
Future<bool> deleteIdea(String id) async {
try {
await _api.delete(Endpoints.idea(id));
state = state.copyWith(
ideas: state.ideas.where((i) => i.id != id).toList(),
selectedIdea: null,
);
return true;
} on ApiException catch (e) {
state = state.copyWith(error: e.message);
return false;
}
}
Future<bool> updateFunnel(String ideaId, String status) async {
try {
final response = await _api.post(Endpoints.ideaFunnel(ideaId), data: {
'status': status,
});
final updated =
Idea.fromJson(response.data as Map<String, dynamic>);
state = state.copyWith(
ideas: state.ideas.map((i) => i.id == ideaId ? updated : i).toList(),
selectedIdea: updated,
);
return true;
} on ApiException catch (e) {
state = state.copyWith(error: e.message);
return false;
}
}
void setViewMode(String mode) {
state = state.copyWith(viewMode: mode);
}
void clearError() {
state = state.copyWith(error: null);
}
}
final ideasProvider =
StateNotifierProvider<IdeasNotifier, IdeasState>((ref) {
final api = ref.read(apiClientProvider);
return IdeasNotifier(api);
});
class IdeaCommentsState {
final List<Comment> comments;
final bool isLoading;
final String? error;
IdeaCommentsState({
this.comments = const [],
this.isLoading = false,
this.error,
});
IdeaCommentsState copyWith({
List<Comment>? comments,
bool? isLoading,
String? error,
}) =>
IdeaCommentsState(
comments: comments ?? this.comments,
isLoading: isLoading ?? this.isLoading,
error: error,
);
}
class IdeaCommentsNotifier extends StateNotifier<IdeaCommentsState> {
final ApiClient _api;
final String ideaId;
IdeaCommentsNotifier(this._api, this.ideaId) : super(IdeaCommentsState());
Future<void> loadComments() async {
state = state.copyWith(isLoading: true);
try {
final response = await _api.get(Endpoints.ideaComments(ideaId));
final list = (response.data as List<dynamic>)
.map((e) => Comment.fromJson(e as Map<String, dynamic>))
.toList();
state = state.copyWith(comments: list, isLoading: false);
} on ApiException catch (e) {
state = state.copyWith(isLoading: false, error: e.message);
}
}
Future<bool> addComment(String body, {String? parentId}) async {
try {
final response = await _api.post(Endpoints.ideaComments(ideaId), data: {
'body': body,
if (parentId != null) 'parent_id': parentId,
});
final comment =
Comment.fromJson(response.data as Map<String, dynamic>);
state = state.copyWith(comments: [...state.comments, comment]);
return true;
} on ApiException catch (e) {
state = state.copyWith(error: e.message);
return false;
}
}
Future<void> toggleVote(String ideaId) async {
try {
await _api.post(Endpoints.ideaVote(ideaId));
} on ApiException catch (e) {
state = state.copyWith(error: e.message);
}
}
}
final ideaCommentsProvider =
StateNotifierProvider.family<IdeaCommentsNotifier, IdeaCommentsState, String>(
(ref, ideaId) {
final api = ref.read(apiClientProvider);
return IdeaCommentsNotifier(api, ideaId);
});
class IdeaActivityState {
final List<Activity> activities;
final bool isLoading;
IdeaActivityState({
this.activities = const [],
this.isLoading = false,
});
IdeaActivityState copyWith({
List<Activity>? activities,
bool? isLoading,
}) =>
IdeaActivityState(
activities: activities ?? this.activities,
isLoading: isLoading ?? this.isLoading,
);
}
class IdeaActivityNotifier extends StateNotifier<IdeaActivityState> {
final ApiClient _api;
final String ideaId;
IdeaActivityNotifier(this._api, this.ideaId) : super(IdeaActivityState());
Future<void> loadActivities() async {
state = state.copyWith(isLoading: true);
try {
final response = await _api.get(Endpoints.ideaActivity(ideaId));
final list = (response.data as List<dynamic>)
.map((e) => Activity.fromJson(e as Map<String, dynamic>))
.toList();
state = state.copyWith(activities: list, isLoading: false);
} catch (_) {
state = state.copyWith(isLoading: false);
}
}
}
final ideaActivityProvider =
StateNotifierProvider.family<IdeaActivityNotifier, IdeaActivityState, String>(
(ref, ideaId) {
final api = ref.read(apiClientProvider);
return IdeaActivityNotifier(api, ideaId);
});
@@ -0,0 +1,120 @@
import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../core/api/client.dart';
import '../core/api/endpoints.dart';
import '../core/api/exceptions.dart';
import '../models/collaboration.dart';
class NotificationState {
final List<AppNotification> notifications;
final int unreadCount;
final bool isLoading;
final String? error;
NotificationState({
this.notifications = const [],
this.unreadCount = 0,
this.isLoading = false,
this.error,
});
NotificationState copyWith({
List<AppNotification>? notifications,
int? unreadCount,
bool? isLoading,
String? error,
}) =>
NotificationState(
notifications: notifications ?? this.notifications,
unreadCount: unreadCount ?? this.unreadCount,
isLoading: isLoading ?? this.isLoading,
error: error,
);
}
class NotificationNotifier extends StateNotifier<NotificationState> {
final ApiClient _api;
NotificationNotifier(this._api) : super(NotificationState());
Future<void> loadNotifications() async {
state = state.copyWith(isLoading: true);
try {
final response = await _api.get(Endpoints.notifications);
final list = (response.data as List<dynamic>)
.map((e) =>
AppNotification.fromJson(e as Map<String, dynamic>))
.toList();
state = state.copyWith(notifications: list, isLoading: false);
_updateUnreadCount();
} on ApiException catch (e) {
state = state.copyWith(isLoading: false, error: e.message);
}
}
Future<void> loadUnreadCount() async {
try {
final response =
await _api.get(Endpoints.notificationsUnreadCount);
state = state.copyWith(
unreadCount: response.data['count'] as int? ?? 0);
} catch (_) {}
}
Future<void> markAsRead(String id) async {
try {
await _api.patch(Endpoints.notification(id));
state = state.copyWith(
notifications: state.notifications.map((n) {
if (n.id == id) {
return AppNotification(
id: n.id,
userId: n.userId,
title: n.title,
body: n.body,
isRead: true,
ideaId: n.ideaId,
createdAt: n.createdAt,
);
}
return n;
}).toList(),
);
_updateUnreadCount();
} catch (_) {}
}
Future<void> markAllAsRead() async {
try {
await _api.post(Endpoints.notificationsReadAll);
state = state.copyWith(
notifications: state.notifications
.map((n) => AppNotification(
id: n.id,
userId: n.userId,
title: n.title,
body: n.body,
isRead: true,
ideaId: n.ideaId,
createdAt: n.createdAt,
))
.toList(),
unreadCount: 0,
);
} catch (_) {}
}
void _updateUnreadCount() {
state = state.copyWith(
unreadCount: state.notifications.where((n) => !n.isRead).length,
);
}
}
final notificationProvider = StateNotifierProvider<NotificationNotifier, NotificationState>((ref) {
final api = ref.read(apiClientProvider);
return NotificationNotifier(api);
});
final unreadCountProvider = Provider<int>((ref) {
return ref.watch(notificationProvider).unreadCount;
});
+33
View File
@@ -0,0 +1,33 @@
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
final themeModeProvider =
StateNotifierProvider<ThemeModeNotifier, ThemeMode>((ref) {
return ThemeModeNotifier();
});
class ThemeModeNotifier extends StateNotifier<ThemeMode> {
ThemeModeNotifier() : super(ThemeMode.system) {
_load();
}
final _storage = const FlutterSecureStorage();
static const _key = 'theme_mode';
Future<void> _load() async {
final value = await _storage.read(key: _key);
if (value == 'light') state = ThemeMode.light;
if (value == 'dark') state = ThemeMode.dark;
}
Future<void> setTheme(ThemeMode mode) async {
state = mode;
final value = mode == ThemeMode.light
? 'light'
: mode == ThemeMode.dark
? 'dark'
: 'system';
await _storage.write(key: _key, value: value);
}
}
@@ -0,0 +1,124 @@
import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../core/api/client.dart';
import '../core/api/endpoints.dart';
import '../core/api/exceptions.dart';
import '../models/workspace.dart';
class WorkspaceState {
final List<Workspace> workspaces;
final Workspace? selectedWorkspace;
final List<WorkspaceMember> members;
final bool isLoading;
final String? error;
WorkspaceState({
this.workspaces = const [],
this.selectedWorkspace,
this.members = const [],
this.isLoading = false,
this.error,
});
WorkspaceState copyWith({
List<Workspace>? workspaces,
Workspace? selectedWorkspace,
List<WorkspaceMember>? members,
bool? isLoading,
String? error,
}) =>
WorkspaceState(
workspaces: workspaces ?? this.workspaces,
selectedWorkspace: selectedWorkspace ?? this.selectedWorkspace,
members: members ?? this.members,
isLoading: isLoading ?? this.isLoading,
error: error,
);
}
class WorkspaceNotifier extends StateNotifier<WorkspaceState> {
final ApiClient _api;
WorkspaceNotifier(this._api) : super(WorkspaceState());
Future<void> loadWorkspaces() async {
state = state.copyWith(isLoading: true, error: null);
try {
final response = await _api.get(Endpoints.workspaces);
final list = (response.data as List<dynamic>)
.map((e) => Workspace.fromJson(e as Map<String, dynamic>))
.toList();
state = state.copyWith(workspaces: list, isLoading: false);
} on ApiException catch (e) {
state = state.copyWith(isLoading: false, error: e.message);
}
}
Future<Workspace?> createWorkspace(CreateWorkspaceRequest request) async {
try {
final response =
await _api.post(Endpoints.workspaces, data: request.toJson());
final ws =
Workspace.fromJson(response.data as Map<String, dynamic>);
state = state.copyWith(workspaces: [...state.workspaces, ws]);
return ws;
} on ApiException catch (e) {
state = state.copyWith(error: e.message);
return null;
}
}
Future<void> selectWorkspace(Workspace workspace) async {
state = state.copyWith(selectedWorkspace: workspace);
await loadMembers(workspace.id);
}
Future<void> loadMembers(String workspaceId) async {
try {
final response =
await _api.get(Endpoints.workspaceMembers(workspaceId));
final list = (response.data as List<dynamic>)
.map((e) => WorkspaceMember.fromJson(e as Map<String, dynamic>))
.toList();
state = state.copyWith(members: list);
} on ApiException catch (e) {
state = state.copyWith(error: e.message);
}
}
Future<bool> addMember(String workspaceId, String email, String role) async {
try {
await _api.post(Endpoints.workspaceMembers(workspaceId), data: {
'email': email,
'role': role,
});
await loadMembers(workspaceId);
return true;
} on ApiException catch (e) {
state = state.copyWith(error: e.message);
return false;
}
}
Future<bool> removeMember(String workspaceId, String userId) async {
try {
await _api.delete(Endpoints.workspaceMember(workspaceId, userId));
state = state.copyWith(
members: state.members.where((m) => m.userId != userId).toList(),
);
return true;
} on ApiException catch (e) {
state = state.copyWith(error: e.message);
return false;
}
}
void clearError() {
state = state.copyWith(error: null);
}
}
final workspaceProvider =
StateNotifierProvider<WorkspaceNotifier, WorkspaceState>((ref) {
final api = ref.read(apiClientProvider);
return WorkspaceNotifier(api);
});