Initial commit
This commit is contained in:
@@ -0,0 +1,157 @@
|
||||
import React, { createContext, useContext, useState, useEffect } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { CCUser, CCUserMetadata, authService } from '../services/auth/authService';
|
||||
import { logger } from '../debugConfig';
|
||||
import { supabase } from '../supabaseClient';
|
||||
|
||||
export interface AuthContextType {
|
||||
user: CCUser | null;
|
||||
user_role: string | null;
|
||||
loading: boolean;
|
||||
error: Error | null;
|
||||
signIn: (email: string, password: string) => Promise<void>;
|
||||
signOut: () => Promise<void>;
|
||||
clearError: () => void;
|
||||
}
|
||||
|
||||
export const AuthContext = createContext<AuthContextType>({
|
||||
user: null,
|
||||
user_role: null,
|
||||
loading: true,
|
||||
error: null,
|
||||
signIn: async () => {},
|
||||
signOut: async () => {},
|
||||
clearError: () => {}
|
||||
});
|
||||
|
||||
export function AuthProvider({ children }: { children: React.ReactNode }) {
|
||||
const navigate = useNavigate();
|
||||
const [user, setUser] = useState<CCUser | null>(null);
|
||||
const [user_role, setUserRole] = useState<string | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState<Error | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
const loadUser = async () => {
|
||||
try {
|
||||
const { data: { user } } = await supabase.auth.getUser();
|
||||
|
||||
if (user) {
|
||||
const metadata = user.user_metadata as CCUserMetadata;
|
||||
setUser({
|
||||
id: user.id,
|
||||
email: user.email,
|
||||
user_type: metadata.user_type || '',
|
||||
username: metadata.username || '',
|
||||
display_name: metadata.display_name || '',
|
||||
user_db_name: `cc.users.${metadata.user_type}.${metadata.username}`,
|
||||
school_db_name: 'cc.institutes.development.default',
|
||||
created_at: user.created_at,
|
||||
updated_at: user.updated_at
|
||||
});
|
||||
setUserRole(metadata.user_role || null);
|
||||
} else {
|
||||
setUser(null);
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error('auth-context', '❌ Failed to load user', { error });
|
||||
setError(error instanceof Error ? error : new Error('Failed to load user'));
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
loadUser();
|
||||
|
||||
const { data: { subscription } } = supabase.auth.onAuthStateChange(async (event, session) => {
|
||||
if (event === 'SIGNED_IN' && session?.user) {
|
||||
const metadata = session.user.user_metadata as CCUserMetadata;
|
||||
setUser({
|
||||
id: session.user.id,
|
||||
email: session.user.email,
|
||||
user_type: metadata.user_type || '',
|
||||
username: metadata.username || '',
|
||||
display_name: metadata.display_name || '',
|
||||
user_db_name: `cc.users.${metadata.user_type}.${metadata.username}`,
|
||||
school_db_name: 'cc.institutes.development.default',
|
||||
created_at: session.user.created_at,
|
||||
updated_at: session.user.updated_at
|
||||
});
|
||||
} else if (event === 'SIGNED_OUT') {
|
||||
setUser(null);
|
||||
}
|
||||
});
|
||||
|
||||
return () => {
|
||||
subscription.unsubscribe();
|
||||
};
|
||||
}, []);
|
||||
|
||||
const signIn = async (email: string, password: string) => {
|
||||
try {
|
||||
setLoading(true);
|
||||
const { data, error: signInError } = await supabase.auth.signInWithPassword({
|
||||
email,
|
||||
password
|
||||
});
|
||||
|
||||
if (signInError) throw signInError;
|
||||
|
||||
if (data.user) {
|
||||
const metadata = data.user.user_metadata as CCUserMetadata;
|
||||
setUser({
|
||||
id: data.user.id,
|
||||
email: data.user.email,
|
||||
user_type: metadata.user_type || '',
|
||||
username: metadata.username || '',
|
||||
display_name: metadata.display_name || '',
|
||||
user_db_name: `cc.users.${metadata.user_type}.${metadata.username}`,
|
||||
school_db_name: 'cc.institutes.development.default',
|
||||
created_at: data.user.created_at,
|
||||
updated_at: data.user.updated_at
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error('auth-context', '❌ Sign in failed', { error });
|
||||
setError(error instanceof Error ? error : new Error('Sign in failed'));
|
||||
throw error;
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const signOut = async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
await authService.logout();
|
||||
setUser(null);
|
||||
navigate('/');
|
||||
} catch (error) {
|
||||
logger.error('auth-context', '❌ Sign out failed', { error });
|
||||
setError(error instanceof Error ? error : new Error('Sign out failed'));
|
||||
throw error;
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const clearError = () => setError(null);
|
||||
|
||||
return (
|
||||
<AuthContext.Provider
|
||||
value={{
|
||||
user,
|
||||
user_role,
|
||||
loading,
|
||||
error,
|
||||
signIn,
|
||||
signOut,
|
||||
clearError
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</AuthContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
export const useAuth = () => useContext(AuthContext);
|
||||
@@ -0,0 +1,92 @@
|
||||
import React, { createContext, useContext, useState, useEffect, ReactNode } from 'react';
|
||||
import { useAuth } from './AuthContext';
|
||||
import { useUser } from './UserContext';
|
||||
import { SchoolNeoDBService } from '../services/graph/schoolNeoDBService';
|
||||
import { CCSchoolNodeProps } from '../utils/tldraw/cc-base/cc-graph/cc-graph-types';
|
||||
import { logger } from '../debugConfig';
|
||||
|
||||
export interface NeoInstituteContextType {
|
||||
schoolNode: CCSchoolNodeProps | null;
|
||||
isLoading: boolean;
|
||||
isInitialized: boolean;
|
||||
error: string | null;
|
||||
}
|
||||
|
||||
const NeoInstituteContext = createContext<NeoInstituteContextType>({
|
||||
schoolNode: null,
|
||||
isLoading: true,
|
||||
isInitialized: false,
|
||||
error: null
|
||||
});
|
||||
|
||||
export const NeoInstituteProvider: React.FC<{ children: ReactNode }> = ({ children }) => {
|
||||
const { user } = useAuth();
|
||||
const { profile, isInitialized: isUserInitialized } = useUser();
|
||||
|
||||
const [schoolNode, setSchoolNode] = useState<CCSchoolNodeProps | null>(null);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [isInitialized, setIsInitialized] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
// Wait for user profile to be ready
|
||||
if (!isUserInitialized) {
|
||||
logger.debug('neo-institute-context', '⏳ Waiting for user initialization...');
|
||||
return;
|
||||
}
|
||||
|
||||
// If no profile or no worker database, mark as initialized with no data
|
||||
if (!profile || !profile.school_db_name) {
|
||||
setIsLoading(false);
|
||||
setIsInitialized(true);
|
||||
return;
|
||||
}
|
||||
|
||||
const loadSchoolNode = async () => {
|
||||
try {
|
||||
setIsLoading(true);
|
||||
logger.debug('neo-institute-context', '🔄 Loading school node', {
|
||||
schoolDbName: profile.school_db_name,
|
||||
userEmail: user?.email
|
||||
});
|
||||
|
||||
const node = await SchoolNeoDBService.getSchoolNode(profile.school_db_name);
|
||||
if (node) {
|
||||
setSchoolNode(node);
|
||||
logger.debug('neo-institute-context', '✅ School node loaded', {
|
||||
schoolId: node.unique_id,
|
||||
dbName: profile.school_db_name
|
||||
});
|
||||
} else {
|
||||
logger.warn('neo-institute-context', '⚠️ No school node found');
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
const errorMessage = error instanceof Error ? error.message : 'Failed to load school node';
|
||||
logger.error('neo-institute-context', '❌ Failed to load school node', {
|
||||
error: errorMessage,
|
||||
schoolDbName: profile.school_db_name
|
||||
});
|
||||
setError(errorMessage);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
setIsInitialized(true);
|
||||
}
|
||||
};
|
||||
|
||||
loadSchoolNode();
|
||||
}, [user?.email, profile, isUserInitialized]);
|
||||
|
||||
return (
|
||||
<NeoInstituteContext.Provider value={{
|
||||
schoolNode,
|
||||
isLoading,
|
||||
isInitialized,
|
||||
error
|
||||
}}>
|
||||
{children}
|
||||
</NeoInstituteContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
export const useNeoInstitute = () => useContext(NeoInstituteContext);
|
||||
@@ -0,0 +1,624 @@
|
||||
import React, { createContext, useContext, useState, useEffect, ReactNode } from 'react';
|
||||
import { useAuth } from './AuthContext';
|
||||
import { useUser } from './UserContext';
|
||||
import { logger } from '../debugConfig';
|
||||
import { CCUserNodeProps, CCCalendarNodeProps, CCUserTeacherTimetableNodeProps } from '../utils/tldraw/cc-base/cc-graph/cc-graph-types';
|
||||
import { CalendarStructure, WorkerStructure } from '../types/navigation';
|
||||
import { useNavigationStore } from '../stores/navigationStore';
|
||||
|
||||
// Core Node Types
|
||||
export interface CalendarNode {
|
||||
id: string;
|
||||
label: string;
|
||||
title: string;
|
||||
tldraw_snapshot: string;
|
||||
type?: CCCalendarNodeProps['__primarylabel__'];
|
||||
nodeData?: CCCalendarNodeProps;
|
||||
}
|
||||
|
||||
export interface WorkerNode {
|
||||
id: string;
|
||||
label: string;
|
||||
title: string;
|
||||
tldraw_snapshot: string;
|
||||
type?: CCUserTeacherTimetableNodeProps['__primarylabel__'];
|
||||
nodeData?: CCUserTeacherTimetableNodeProps;
|
||||
}
|
||||
|
||||
// Calendar Structure Types
|
||||
export interface CalendarDay {
|
||||
id: string;
|
||||
date: string;
|
||||
title: string;
|
||||
}
|
||||
|
||||
export interface CalendarWeek {
|
||||
id: string;
|
||||
title: string;
|
||||
days: { id: string }[];
|
||||
startDate: string;
|
||||
endDate: string;
|
||||
}
|
||||
|
||||
export interface CalendarMonth {
|
||||
id: string;
|
||||
title: string;
|
||||
days: { id: string }[];
|
||||
weeks: { id: string }[];
|
||||
year: string;
|
||||
month: string;
|
||||
}
|
||||
|
||||
export interface CalendarYear {
|
||||
id: string;
|
||||
title: string;
|
||||
months: { id: string }[];
|
||||
year: string;
|
||||
}
|
||||
|
||||
// Worker Structure Types
|
||||
export interface TimetableEntry {
|
||||
id: string;
|
||||
title: string;
|
||||
type: string;
|
||||
startTime: string;
|
||||
endTime: string;
|
||||
}
|
||||
|
||||
export interface ClassEntry {
|
||||
id: string;
|
||||
title: string;
|
||||
type: string;
|
||||
}
|
||||
|
||||
export interface LessonEntry {
|
||||
id: string;
|
||||
title: string;
|
||||
type: string;
|
||||
}
|
||||
|
||||
interface NeoUserContextType {
|
||||
userNode: CCUserNodeProps | null;
|
||||
calendarNode: CalendarNode | null;
|
||||
workerNode: WorkerNode | null;
|
||||
userDbName: string | null;
|
||||
workerDbName: string | null;
|
||||
isLoading: boolean;
|
||||
isInitialized: boolean;
|
||||
error: string | null;
|
||||
|
||||
// Calendar Navigation
|
||||
navigateToDay: (id: string) => Promise<void>;
|
||||
navigateToWeek: (id: string) => Promise<void>;
|
||||
navigateToMonth: (id: string) => Promise<void>;
|
||||
navigateToYear: (id: string) => Promise<void>;
|
||||
currentCalendarNode: CalendarNode | null;
|
||||
calendarStructure: CalendarStructure | null;
|
||||
|
||||
// Worker Navigation
|
||||
navigateToTimetable: (id: string) => Promise<void>;
|
||||
navigateToJournal: (id: string) => Promise<void>;
|
||||
navigateToPlanner: (id: string) => Promise<void>;
|
||||
navigateToClass: (id: string) => Promise<void>;
|
||||
navigateToLesson: (id: string) => Promise<void>;
|
||||
currentWorkerNode: WorkerNode | null;
|
||||
workerStructure: WorkerStructure | null;
|
||||
}
|
||||
|
||||
const NeoUserContext = createContext<NeoUserContextType>({
|
||||
userNode: null,
|
||||
calendarNode: null,
|
||||
workerNode: null,
|
||||
userDbName: null,
|
||||
workerDbName: null,
|
||||
isLoading: false,
|
||||
isInitialized: false,
|
||||
error: null,
|
||||
navigateToDay: async () => {},
|
||||
navigateToWeek: async () => {},
|
||||
navigateToMonth: async () => {},
|
||||
navigateToYear: async () => {},
|
||||
navigateToTimetable: async () => {},
|
||||
navigateToJournal: async () => {},
|
||||
navigateToPlanner: async () => {},
|
||||
navigateToClass: async () => {},
|
||||
navigateToLesson: async () => {},
|
||||
currentCalendarNode: null,
|
||||
currentWorkerNode: null,
|
||||
calendarStructure: null,
|
||||
workerStructure: null
|
||||
});
|
||||
|
||||
export const NeoUserProvider: React.FC<{ children: ReactNode }> = ({ children }) => {
|
||||
const { user } = useAuth();
|
||||
const { profile, isInitialized: isUserInitialized } = useUser();
|
||||
const navigationStore = useNavigationStore();
|
||||
|
||||
const [userNode, setUserNode] = useState<CCUserNodeProps | null>(null);
|
||||
const [calendarNode] = useState<CalendarNode | null>(null);
|
||||
const [workerNode] = useState<WorkerNode | null>(null);
|
||||
const [currentCalendarNode, setCurrentCalendarNode] = useState<CalendarNode | null>(null);
|
||||
const [currentWorkerNode, setCurrentWorkerNode] = useState<WorkerNode | null>(null);
|
||||
const [calendarStructure] = useState<CalendarStructure | null>(null);
|
||||
const [workerStructure] = useState<WorkerStructure | null>(null);
|
||||
const [userDbName, setUserDbName] = useState<string | null>(null);
|
||||
const [workerDbName, setWorkerDbName] = useState<string | null>(null);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [isInitialized, setIsInitialized] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
// Use ref for initialization tracking to prevent re-renders
|
||||
const initializationRef = React.useRef({
|
||||
hasStarted: false,
|
||||
isComplete: false
|
||||
});
|
||||
|
||||
// Add base properties for node data
|
||||
const getBaseNodeProps = () => ({
|
||||
title: '',
|
||||
w: 200,
|
||||
h: 200,
|
||||
headerColor: '#000000',
|
||||
backgroundColor: '#ffffff',
|
||||
isLocked: false,
|
||||
__primarylabel__: 'UserTeacherTimetable',
|
||||
unique_id: '',
|
||||
tldraw_snapshot: '',
|
||||
created: new Date().toISOString(),
|
||||
merged: new Date().toISOString(),
|
||||
state: {
|
||||
parentId: null,
|
||||
isPageChild: false,
|
||||
hasChildren: false,
|
||||
bindings: [],
|
||||
},
|
||||
defaultComponent: true,
|
||||
});
|
||||
|
||||
// Initialize context when dependencies are ready
|
||||
useEffect(() => {
|
||||
if (!isUserInitialized || !profile || isInitialized || initializationRef.current.hasStarted) {
|
||||
return;
|
||||
}
|
||||
|
||||
const initializeContext = async () => {
|
||||
try {
|
||||
initializationRef.current.hasStarted = true;
|
||||
setIsLoading(true);
|
||||
setError(null);
|
||||
|
||||
// Set database names
|
||||
const userDb = profile.user_db_name || (user?.email ?
|
||||
`cc.users.${user.email.replace('@', 'at').replace(/\./g, 'dot')}` : null);
|
||||
|
||||
if (!userDb) {
|
||||
throw new Error('No user database name available');
|
||||
}
|
||||
|
||||
// Initialize user node in profile context
|
||||
logger.debug('neo-user-context', '🔄 Starting context initialization');
|
||||
|
||||
// Initialize user node
|
||||
await navigationStore.switchContext({
|
||||
main: 'profile',
|
||||
base: 'profile',
|
||||
extended: 'overview'
|
||||
}, userDb, profile.school_db_name);
|
||||
|
||||
const userNavigationNode = navigationStore.context.node;
|
||||
if (userNavigationNode?.data) {
|
||||
const userNodeData: CCUserNodeProps = {
|
||||
...getBaseNodeProps(),
|
||||
__primarylabel__: 'User',
|
||||
unique_id: userNavigationNode.id,
|
||||
tldraw_snapshot: userNavigationNode.tldraw_snapshot || '',
|
||||
title: String(userNavigationNode.data?.user_name || 'User'),
|
||||
user_name: String(userNavigationNode.data?.user_name || 'User'),
|
||||
user_email: user?.email || '',
|
||||
user_type: 'User',
|
||||
user_id: userNavigationNode.id,
|
||||
worker_node_data: JSON.stringify(userNavigationNode.data || {})
|
||||
};
|
||||
setUserNode(userNodeData);
|
||||
}
|
||||
|
||||
// Set final state
|
||||
setUserDbName(userDb);
|
||||
setWorkerDbName(profile.school_db_name);
|
||||
setIsInitialized(true);
|
||||
setIsLoading(false);
|
||||
initializationRef.current.isComplete = true;
|
||||
|
||||
logger.debug('neo-user-context', '✅ Context initialization complete');
|
||||
} catch (error) {
|
||||
const errorMessage = error instanceof Error ? error.message : 'Failed to initialize user context';
|
||||
logger.error('neo-user-context', '❌ Failed to initialize context', { error: errorMessage });
|
||||
setError(errorMessage);
|
||||
setIsLoading(false);
|
||||
setIsInitialized(true);
|
||||
initializationRef.current.isComplete = true;
|
||||
}
|
||||
};
|
||||
|
||||
initializeContext();
|
||||
}, [user?.email, profile, isUserInitialized, navigationStore, isInitialized]);
|
||||
|
||||
// Calendar Navigation Functions
|
||||
const navigateToDay = async (id: string) => {
|
||||
if (!userDbName) return;
|
||||
setIsLoading(true);
|
||||
try {
|
||||
await navigationStore.switchContext({
|
||||
base: 'calendar',
|
||||
extended: 'day'
|
||||
}, userDbName, workerDbName);
|
||||
|
||||
const node = navigationStore.context.node;
|
||||
if (node?.data) {
|
||||
const nodeData: CCCalendarNodeProps = {
|
||||
...getBaseNodeProps(),
|
||||
__primarylabel__: 'CalendarDay',
|
||||
unique_id: id || node.id,
|
||||
tldraw_snapshot: node.tldraw_snapshot || '',
|
||||
title: node.label,
|
||||
name: node.label,
|
||||
calendar_type: 'day',
|
||||
calendar_name: node.label,
|
||||
start_date: new Date().toISOString(),
|
||||
end_date: new Date().toISOString()
|
||||
};
|
||||
|
||||
setCurrentCalendarNode({
|
||||
id: id || node.id,
|
||||
label: node.label,
|
||||
title: node.label,
|
||||
tldraw_snapshot: node.tldraw_snapshot || '',
|
||||
type: 'CalendarDay',
|
||||
nodeData
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
setError(error instanceof Error ? error.message : 'Failed to navigate to day');
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const navigateToWeek = async (id: string) => {
|
||||
if (!userDbName) return;
|
||||
setIsLoading(true);
|
||||
try {
|
||||
await navigationStore.switchContext({
|
||||
base: 'calendar',
|
||||
extended: 'week'
|
||||
}, userDbName, workerDbName);
|
||||
|
||||
const node = navigationStore.context.node;
|
||||
if (node?.data) {
|
||||
const nodeData: CCCalendarNodeProps = {
|
||||
...getBaseNodeProps(),
|
||||
__primarylabel__: 'CalendarWeek',
|
||||
unique_id: id || node.id,
|
||||
tldraw_snapshot: node.tldraw_snapshot || '',
|
||||
title: node.label,
|
||||
name: node.label,
|
||||
calendar_type: 'week',
|
||||
calendar_name: node.label,
|
||||
start_date: new Date().toISOString(),
|
||||
end_date: new Date().toISOString()
|
||||
};
|
||||
|
||||
setCurrentCalendarNode({
|
||||
id: id || node.id,
|
||||
label: node.label,
|
||||
title: node.label,
|
||||
tldraw_snapshot: node.tldraw_snapshot || '',
|
||||
type: 'CalendarWeek',
|
||||
nodeData
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
setError(error instanceof Error ? error.message : 'Failed to navigate to week');
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const navigateToMonth = async (id: string) => {
|
||||
if (!userDbName) return;
|
||||
setIsLoading(true);
|
||||
try {
|
||||
await navigationStore.switchContext({
|
||||
base: 'calendar',
|
||||
extended: 'month'
|
||||
}, userDbName, workerDbName);
|
||||
|
||||
const node = navigationStore.context.node;
|
||||
if (node?.data) {
|
||||
const nodeData: CCCalendarNodeProps = {
|
||||
...getBaseNodeProps(),
|
||||
__primarylabel__: 'CalendarMonth',
|
||||
unique_id: id || node.id,
|
||||
tldraw_snapshot: node.tldraw_snapshot || '',
|
||||
title: node.label,
|
||||
name: node.label,
|
||||
calendar_type: 'month',
|
||||
calendar_name: node.label,
|
||||
start_date: new Date().toISOString(),
|
||||
end_date: new Date().toISOString()
|
||||
};
|
||||
|
||||
setCurrentCalendarNode({
|
||||
id: id || node.id,
|
||||
label: node.label,
|
||||
title: node.label,
|
||||
tldraw_snapshot: node.tldraw_snapshot || '',
|
||||
type: 'CalendarMonth',
|
||||
nodeData
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
setError(error instanceof Error ? error.message : 'Failed to navigate to month');
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const navigateToYear = async (id: string) => {
|
||||
if (!userDbName) return;
|
||||
setIsLoading(true);
|
||||
try {
|
||||
await navigationStore.switchContext({
|
||||
base: 'calendar',
|
||||
extended: 'year'
|
||||
}, userDbName, workerDbName);
|
||||
|
||||
const node = navigationStore.context.node;
|
||||
if (node?.data) {
|
||||
const nodeData: CCCalendarNodeProps = {
|
||||
...getBaseNodeProps(),
|
||||
__primarylabel__: 'CalendarYear',
|
||||
unique_id: id || node.id,
|
||||
tldraw_snapshot: node.tldraw_snapshot || '',
|
||||
title: node.label,
|
||||
name: node.label,
|
||||
calendar_type: 'year',
|
||||
calendar_name: node.label,
|
||||
start_date: new Date().toISOString(),
|
||||
end_date: new Date().toISOString()
|
||||
};
|
||||
|
||||
setCurrentCalendarNode({
|
||||
id: id || node.id,
|
||||
label: node.label,
|
||||
title: node.label,
|
||||
tldraw_snapshot: node.tldraw_snapshot || '',
|
||||
type: 'CalendarYear',
|
||||
nodeData
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
setError(error instanceof Error ? error.message : 'Failed to navigate to year');
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
// Worker Navigation Functions
|
||||
const navigateToTimetable = async (id: string) => {
|
||||
if (!userDbName) return;
|
||||
setIsLoading(true);
|
||||
try {
|
||||
await navigationStore.switchContext({
|
||||
base: 'teaching',
|
||||
extended: 'timetable'
|
||||
}, userDbName, workerDbName);
|
||||
|
||||
const node = navigationStore.context.node;
|
||||
if (node?.data) {
|
||||
const nodeData: CCUserTeacherTimetableNodeProps = {
|
||||
...getBaseNodeProps(),
|
||||
__primarylabel__: 'UserTeacherTimetable',
|
||||
unique_id: id || node.id,
|
||||
tldraw_snapshot: node.tldraw_snapshot || '',
|
||||
title: node.label,
|
||||
school_db_name: workerDbName || '',
|
||||
school_timetable_id: id || node.id
|
||||
};
|
||||
|
||||
setCurrentWorkerNode({
|
||||
id: id || node.id,
|
||||
label: node.label,
|
||||
title: node.label,
|
||||
tldraw_snapshot: node.tldraw_snapshot || '',
|
||||
type: 'UserTeacherTimetable',
|
||||
nodeData
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
setError(error instanceof Error ? error.message : 'Failed to navigate to timetable');
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const navigateToJournal = async (id: string) => {
|
||||
if (!userDbName) return;
|
||||
setIsLoading(true);
|
||||
try {
|
||||
await navigationStore.switchContext({
|
||||
base: 'teaching',
|
||||
extended: 'journal'
|
||||
}, userDbName, workerDbName);
|
||||
|
||||
const node = navigationStore.context.node;
|
||||
if (node?.data) {
|
||||
const nodeData: CCUserTeacherTimetableNodeProps = {
|
||||
...getBaseNodeProps(),
|
||||
__primarylabel__: 'UserTeacherTimetable',
|
||||
unique_id: id || node.id,
|
||||
tldraw_snapshot: node.tldraw_snapshot || '',
|
||||
title: node.label,
|
||||
school_db_name: workerDbName || '',
|
||||
school_timetable_id: id || node.id
|
||||
};
|
||||
|
||||
setCurrentWorkerNode({
|
||||
id: id || node.id,
|
||||
label: node.label,
|
||||
title: node.label,
|
||||
tldraw_snapshot: node.tldraw_snapshot || '',
|
||||
type: 'UserTeacherTimetable',
|
||||
nodeData
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
setError(error instanceof Error ? error.message : 'Failed to navigate to journal');
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const navigateToPlanner = async (id: string) => {
|
||||
if (!userDbName) return;
|
||||
setIsLoading(true);
|
||||
try {
|
||||
await navigationStore.switchContext({
|
||||
base: 'teaching',
|
||||
extended: 'planner'
|
||||
}, userDbName, workerDbName);
|
||||
|
||||
const node = navigationStore.context.node;
|
||||
if (node?.data) {
|
||||
const nodeData: CCUserTeacherTimetableNodeProps = {
|
||||
...getBaseNodeProps(),
|
||||
__primarylabel__: 'UserTeacherTimetable',
|
||||
unique_id: id || node.id,
|
||||
tldraw_snapshot: node.tldraw_snapshot || '',
|
||||
title: node.label,
|
||||
school_db_name: workerDbName || '',
|
||||
school_timetable_id: id || node.id
|
||||
};
|
||||
|
||||
setCurrentWorkerNode({
|
||||
id: id || node.id,
|
||||
label: node.label,
|
||||
title: node.label,
|
||||
tldraw_snapshot: node.tldraw_snapshot || '',
|
||||
type: 'UserTeacherTimetable',
|
||||
nodeData
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
setError(error instanceof Error ? error.message : 'Failed to navigate to planner');
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const navigateToClass = async (id: string) => {
|
||||
if (!userDbName) return;
|
||||
setIsLoading(true);
|
||||
try {
|
||||
await navigationStore.switchContext({
|
||||
base: 'teaching',
|
||||
extended: 'classes'
|
||||
}, userDbName, workerDbName);
|
||||
await navigationStore.navigate(id, userDbName);
|
||||
|
||||
const node = navigationStore.context.node;
|
||||
if (node?.data) {
|
||||
const nodeData: CCUserTeacherTimetableNodeProps = {
|
||||
...getBaseNodeProps(),
|
||||
__primarylabel__: 'UserTeacherTimetable',
|
||||
unique_id: node.id,
|
||||
tldraw_snapshot: node.tldraw_snapshot || '',
|
||||
title: node.label,
|
||||
school_db_name: workerDbName || '',
|
||||
school_timetable_id: node.id
|
||||
};
|
||||
|
||||
setCurrentWorkerNode({
|
||||
id: node.id,
|
||||
label: node.label,
|
||||
title: node.label,
|
||||
tldraw_snapshot: node.tldraw_snapshot || '',
|
||||
type: 'UserTeacherTimetable',
|
||||
nodeData
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
setError(error instanceof Error ? error.message : 'Failed to navigate to class');
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const navigateToLesson = async (id: string) => {
|
||||
if (!userDbName) return;
|
||||
setIsLoading(true);
|
||||
try {
|
||||
await navigationStore.switchContext({
|
||||
base: 'teaching',
|
||||
extended: 'lessons'
|
||||
}, userDbName, workerDbName);
|
||||
await navigationStore.navigate(id, userDbName);
|
||||
|
||||
const node = navigationStore.context.node;
|
||||
if (node?.data) {
|
||||
const nodeData: CCUserTeacherTimetableNodeProps = {
|
||||
...getBaseNodeProps(),
|
||||
__primarylabel__: 'UserTeacherTimetable',
|
||||
unique_id: node.id,
|
||||
tldraw_snapshot: node.tldraw_snapshot || '',
|
||||
title: node.label,
|
||||
school_db_name: workerDbName || '',
|
||||
school_timetable_id: node.id
|
||||
};
|
||||
|
||||
setCurrentWorkerNode({
|
||||
id: node.id,
|
||||
label: node.label,
|
||||
title: node.label,
|
||||
tldraw_snapshot: node.tldraw_snapshot || '',
|
||||
type: 'UserTeacherTimetable',
|
||||
nodeData
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
setError(error instanceof Error ? error.message : 'Failed to navigate to lesson');
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<NeoUserContext.Provider value={{
|
||||
userNode,
|
||||
calendarNode,
|
||||
workerNode,
|
||||
userDbName,
|
||||
workerDbName,
|
||||
isLoading,
|
||||
isInitialized,
|
||||
error,
|
||||
navigateToDay,
|
||||
navigateToWeek,
|
||||
navigateToMonth,
|
||||
navigateToYear,
|
||||
navigateToTimetable,
|
||||
navigateToJournal,
|
||||
navigateToPlanner,
|
||||
navigateToClass,
|
||||
navigateToLesson,
|
||||
currentCalendarNode,
|
||||
currentWorkerNode,
|
||||
calendarStructure,
|
||||
workerStructure
|
||||
}}>
|
||||
{children}
|
||||
</NeoUserContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
export const useNeoUser = () => useContext(NeoUserContext);
|
||||
@@ -0,0 +1,221 @@
|
||||
import React, { ReactNode, createContext, useContext, useState, useCallback } from 'react';
|
||||
import { TLUserPreferences, TLEditorSnapshot, TLStore, getSnapshot, loadSnapshot, Editor } from '@tldraw/tldraw';
|
||||
import { storageService, StorageKeys } from '../services/auth/localStorageService';
|
||||
import { LoadingState } from '../services/tldraw/snapshotService';
|
||||
import { SharedStoreService } from '../services/tldraw/sharedStoreService';
|
||||
import { logger } from '../debugConfig';
|
||||
import { PresentationService } from '../services/tldraw/presentationService';
|
||||
|
||||
interface TLDrawContextType {
|
||||
tldrawPreferences: TLUserPreferences | null;
|
||||
tldrawUserFilePath: string | null;
|
||||
localSnapshot: Partial<TLEditorSnapshot> | null;
|
||||
presentationMode: boolean;
|
||||
sharedStore: SharedStoreService | null;
|
||||
connectionStatus: 'online' | 'offline' | 'error';
|
||||
presentationService: PresentationService | null;
|
||||
setTldrawPreferences: (preferences: TLUserPreferences | null) => void;
|
||||
setTldrawUserFilePath: (path: string | null) => void;
|
||||
handleLocalSnapshot: (
|
||||
action: string,
|
||||
store: TLStore,
|
||||
setLoadingState: (state: LoadingState) => void
|
||||
) => Promise<void>;
|
||||
togglePresentationMode: (editor?: Editor) => void;
|
||||
initializePreferences: (userId: string) => void;
|
||||
setSharedStore: (store: SharedStoreService | null) => void;
|
||||
setConnectionStatus: (status: 'online' | 'offline' | 'error') => void;
|
||||
}
|
||||
|
||||
const TLDrawContext = createContext<TLDrawContextType>({
|
||||
tldrawPreferences: null,
|
||||
tldrawUserFilePath: null,
|
||||
localSnapshot: null,
|
||||
presentationMode: false,
|
||||
sharedStore: null,
|
||||
connectionStatus: 'online',
|
||||
presentationService: null,
|
||||
setTldrawPreferences: () => {},
|
||||
setTldrawUserFilePath: () => {},
|
||||
handleLocalSnapshot: async () => {},
|
||||
togglePresentationMode: () => {},
|
||||
initializePreferences: () => {},
|
||||
setSharedStore: () => {},
|
||||
setConnectionStatus: () => {}
|
||||
});
|
||||
|
||||
export const TLDrawProvider: React.FC<{ children: ReactNode }> = ({ children }) => {
|
||||
const [tldrawPreferences, setTldrawPreferencesState] = useState<TLUserPreferences | null>(
|
||||
storageService.get(StorageKeys.TLDRAW_PREFERENCES)
|
||||
);
|
||||
const [tldrawUserFilePath, setTldrawUserFilePathState] = useState<string | null>(
|
||||
storageService.get(StorageKeys.TLDRAW_FILE_PATH)
|
||||
);
|
||||
const [localSnapshot, setLocalSnapshot] = useState<Partial<TLEditorSnapshot> | null>(
|
||||
storageService.get(StorageKeys.LOCAL_SNAPSHOT)
|
||||
);
|
||||
const [presentationMode, setPresentationMode] = useState<boolean>(
|
||||
storageService.get(StorageKeys.PRESENTATION_MODE) || false
|
||||
);
|
||||
const [sharedStore, setSharedStore] = useState<SharedStoreService | null>(null);
|
||||
const [connectionStatus, setConnectionStatus] = useState<'online' | 'offline' | 'error'>('online');
|
||||
const [presentationService, setPresentationService] = useState<PresentationService | null>(null);
|
||||
|
||||
const initializePreferences = useCallback((userId: string) => {
|
||||
logger.debug('tldraw-context', '🔄 Initializing TLDraw preferences');
|
||||
const storedPrefs = storageService.get(StorageKeys.TLDRAW_PREFERENCES);
|
||||
|
||||
if (storedPrefs) {
|
||||
logger.debug('tldraw-context', '📥 Found stored preferences');
|
||||
setTldrawPreferencesState(storedPrefs);
|
||||
return;
|
||||
}
|
||||
|
||||
// Create default preferences if none exist
|
||||
const defaultPrefs: TLUserPreferences = {
|
||||
id: userId,
|
||||
name: 'User',
|
||||
color: `hsl(${Math.random() * 360}, 70%, 50%)`,
|
||||
locale: 'en',
|
||||
colorScheme: 'system',
|
||||
isSnapMode: false,
|
||||
isWrapMode: false,
|
||||
isDynamicSizeMode: false,
|
||||
isPasteAtCursorMode: false,
|
||||
animationSpeed: 1,
|
||||
edgeScrollSpeed: 1
|
||||
};
|
||||
|
||||
logger.debug('tldraw-context', '📝 Creating default preferences');
|
||||
storageService.set(StorageKeys.TLDRAW_PREFERENCES, defaultPrefs);
|
||||
setTldrawPreferencesState(defaultPrefs);
|
||||
}, []);
|
||||
|
||||
const setTldrawPreferences = useCallback((preferences: TLUserPreferences | null) => {
|
||||
logger.debug('tldraw-context', '🔄 Setting TLDraw preferences', { preferences });
|
||||
if (preferences) {
|
||||
storageService.set(StorageKeys.TLDRAW_PREFERENCES, preferences);
|
||||
} else {
|
||||
storageService.remove(StorageKeys.TLDRAW_PREFERENCES);
|
||||
}
|
||||
setTldrawPreferencesState(preferences);
|
||||
}, []);
|
||||
|
||||
const setTldrawUserFilePath = (path: string | null) => {
|
||||
logger.debug('tldraw-context', '🔄 Setting TLDraw user file path');
|
||||
if (path) {
|
||||
storageService.set(StorageKeys.TLDRAW_FILE_PATH, path);
|
||||
} else {
|
||||
storageService.remove(StorageKeys.TLDRAW_FILE_PATH);
|
||||
}
|
||||
setTldrawUserFilePathState(path);
|
||||
};
|
||||
|
||||
const handleLocalSnapshot = useCallback(async (
|
||||
action: string,
|
||||
store: TLStore,
|
||||
setLoadingState: (state: LoadingState) => void
|
||||
): Promise<void> => {
|
||||
if (!store) {
|
||||
setLoadingState({ status: 'error', error: 'Store not initialized' });
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
if (sharedStore) {
|
||||
if (action === 'put') {
|
||||
const snapshot = getSnapshot(store);
|
||||
await sharedStore.saveSnapshot(snapshot, setLoadingState);
|
||||
} else if (action === 'get') {
|
||||
const savedSnapshot = storageService.get(StorageKeys.LOCAL_SNAPSHOT);
|
||||
if (savedSnapshot) {
|
||||
await sharedStore.loadSnapshot(savedSnapshot, setLoadingState);
|
||||
}
|
||||
}
|
||||
}
|
||||
else if (action === 'put') {
|
||||
logger.debug('tldraw-context', '💾 Putting snapshot into local storage');
|
||||
const snapshot = getSnapshot(store);
|
||||
logger.debug('tldraw-context', '📦 Snapshot:', snapshot);
|
||||
setLocalSnapshot(snapshot);
|
||||
storageService.set(StorageKeys.LOCAL_SNAPSHOT, snapshot);
|
||||
setLoadingState({ status: 'ready', error: '' });
|
||||
}
|
||||
else if (action === 'get') {
|
||||
logger.debug('tldraw-context', '📂 Getting snapshot from local storage');
|
||||
setLoadingState({ status: 'loading', error: '' });
|
||||
const savedSnapshot = storageService.get(StorageKeys.LOCAL_SNAPSHOT);
|
||||
|
||||
if (savedSnapshot && savedSnapshot.document && savedSnapshot.session) {
|
||||
try {
|
||||
logger.debug('tldraw-context', '📥 Loading snapshot into editor');
|
||||
loadSnapshot(store, savedSnapshot);
|
||||
setLoadingState({ status: 'ready', error: '' });
|
||||
} catch (error) {
|
||||
logger.error('tldraw-context', '❌ Failed to load snapshot:', error);
|
||||
store.clear();
|
||||
setLoadingState({ status: 'error', error: 'Failed to load snapshot' });
|
||||
}
|
||||
} else {
|
||||
logger.debug('tldraw-context', '⚠️ No valid snapshot found in local storage');
|
||||
setLoadingState({ status: 'ready', error: '' });
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error('tldraw-context', '❌ Error handling local snapshot:', error);
|
||||
setLoadingState({
|
||||
status: 'error',
|
||||
error: error instanceof Error ? error.message : 'Unknown error'
|
||||
});
|
||||
}
|
||||
}, [sharedStore]);
|
||||
|
||||
const togglePresentationMode = useCallback((editor?: Editor) => {
|
||||
logger.debug('tldraw-context', '🔄 Toggling presentation mode');
|
||||
|
||||
setPresentationMode(prev => {
|
||||
const newValue = !prev;
|
||||
storageService.set(StorageKeys.PRESENTATION_MODE, newValue);
|
||||
|
||||
if (newValue && editor) {
|
||||
// Starting presentation mode
|
||||
logger.info('presentation', '🎥 Initializing presentation service');
|
||||
const service = new PresentationService(editor);
|
||||
setPresentationService(service);
|
||||
service.startPresentationMode();
|
||||
} else if (!newValue && presentationService) {
|
||||
// Stopping presentation mode
|
||||
logger.info('presentation', '🛑 Stopping presentation service');
|
||||
presentationService.stopPresentationMode();
|
||||
setPresentationService(null);
|
||||
}
|
||||
|
||||
return newValue;
|
||||
});
|
||||
}, [presentationService]);
|
||||
|
||||
return (
|
||||
<TLDrawContext.Provider
|
||||
value={{
|
||||
tldrawPreferences,
|
||||
tldrawUserFilePath,
|
||||
localSnapshot,
|
||||
presentationMode,
|
||||
sharedStore,
|
||||
connectionStatus,
|
||||
presentationService,
|
||||
setTldrawPreferences,
|
||||
setTldrawUserFilePath,
|
||||
handleLocalSnapshot,
|
||||
togglePresentationMode,
|
||||
initializePreferences,
|
||||
setSharedStore,
|
||||
setConnectionStatus
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</TLDrawContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
export const useTLDraw = () => useContext(TLDrawContext);
|
||||
@@ -0,0 +1,191 @@
|
||||
import React, { createContext, useContext, useState, useEffect } from 'react';
|
||||
import { supabase } from '../supabaseClient';
|
||||
import { logger } from '../debugConfig';
|
||||
import { CCUser, CCUserMetadata } from '../services/auth/authService';
|
||||
import { UserPreferences } from '../services/auth/profileService';
|
||||
import { DatabaseNameService } from '../services/graph/databaseNameService';
|
||||
|
||||
export interface UserContextType {
|
||||
user: CCUser | null;
|
||||
loading: boolean;
|
||||
error: Error | null;
|
||||
profile: CCUser | null;
|
||||
preferences: UserPreferences;
|
||||
isMobile: boolean;
|
||||
isInitialized: boolean;
|
||||
updateProfile: (updates: Partial<CCUser>) => Promise<void>;
|
||||
updatePreferences: (updates: Partial<UserPreferences>) => Promise<void>;
|
||||
clearError: () => void;
|
||||
}
|
||||
|
||||
export const UserContext = createContext<UserContextType>({
|
||||
user: null,
|
||||
loading: true,
|
||||
error: null,
|
||||
profile: null,
|
||||
preferences: {},
|
||||
isMobile: false,
|
||||
isInitialized: false,
|
||||
updateProfile: async () => {},
|
||||
updatePreferences: async () => {},
|
||||
clearError: () => {}
|
||||
});
|
||||
|
||||
export function UserProvider({ children }: { children: React.ReactNode }) {
|
||||
const [user] = useState<CCUser | null>(null);
|
||||
const [profile, setProfile] = useState<CCUser | null>(null);
|
||||
const [preferences, setPreferences] = useState<UserPreferences>({});
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [isInitialized, setIsInitialized] = useState(false);
|
||||
const [error, setError] = useState<Error | null>(null);
|
||||
const [isMobile] = useState(window.innerWidth <= 768);
|
||||
|
||||
useEffect(() => {
|
||||
const loadUserProfile = async () => {
|
||||
try {
|
||||
const { data: { user } } = await supabase.auth.getUser();
|
||||
|
||||
if (!user) {
|
||||
setProfile(null);
|
||||
setLoading(false);
|
||||
setIsInitialized(true);
|
||||
return;
|
||||
}
|
||||
|
||||
const { data, error } = await supabase
|
||||
.from('profiles')
|
||||
.select('*')
|
||||
.eq('id', user.id)
|
||||
.single();
|
||||
|
||||
if (error) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
const metadata = user.user_metadata as CCUserMetadata;
|
||||
const userDbName = DatabaseNameService.getUserPrivateDB(metadata.user_type || '', metadata.username || '');
|
||||
const schoolDbName = DatabaseNameService.getDevelopmentSchoolDB();
|
||||
|
||||
const userProfile: CCUser = {
|
||||
id: user.id,
|
||||
email: user.email,
|
||||
user_type: metadata.user_type || '',
|
||||
username: metadata.username || '',
|
||||
display_name: metadata.display_name || '',
|
||||
user_db_name: userDbName,
|
||||
school_db_name: schoolDbName,
|
||||
created_at: user.created_at,
|
||||
updated_at: user.updated_at
|
||||
};
|
||||
|
||||
setProfile(userProfile);
|
||||
|
||||
logger.debug('user-context', '✅ User profile loaded', {
|
||||
userId: userProfile.id,
|
||||
userType: userProfile.user_type,
|
||||
username: userProfile.username,
|
||||
userDbName: userProfile.user_db_name,
|
||||
schoolDbName: userProfile.school_db_name
|
||||
});
|
||||
|
||||
// Load preferences from profile data
|
||||
setPreferences({
|
||||
theme: data.theme || 'system',
|
||||
notifications: data.notifications_enabled || false
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
logger.error('user-context', '❌ Failed to load user profile', { error });
|
||||
setError(error instanceof Error ? error : new Error('Failed to load user profile'));
|
||||
} finally {
|
||||
setLoading(false);
|
||||
setIsInitialized(true);
|
||||
}
|
||||
};
|
||||
|
||||
loadUserProfile();
|
||||
}, []);
|
||||
|
||||
const updateProfile = async (updates: Partial<CCUser>) => {
|
||||
if (!user?.id || !profile) {
|
||||
return;
|
||||
}
|
||||
|
||||
setLoading(true);
|
||||
try {
|
||||
const { error } = await supabase
|
||||
.from('profiles')
|
||||
.update({
|
||||
...updates,
|
||||
updated_at: new Date().toISOString()
|
||||
})
|
||||
.eq('id', user.id);
|
||||
|
||||
if (error) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
setProfile(prev => prev ? { ...prev, ...updates } : null);
|
||||
logger.info('user-context', '✅ Profile updated successfully');
|
||||
} catch (error) {
|
||||
logger.error('user-context', '❌ Failed to update profile', { error });
|
||||
setError(error instanceof Error ? error : new Error('Failed to update profile'));
|
||||
throw error;
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const updatePreferences = async (updates: Partial<UserPreferences>) => {
|
||||
if (!user?.id) {
|
||||
return;
|
||||
}
|
||||
|
||||
setLoading(true);
|
||||
try {
|
||||
const newPreferences = { ...preferences, ...updates };
|
||||
setPreferences(newPreferences);
|
||||
|
||||
const { error } = await supabase
|
||||
.from('profiles')
|
||||
.update({
|
||||
preferences: newPreferences,
|
||||
updated_at: new Date().toISOString()
|
||||
})
|
||||
.eq('id', user.id);
|
||||
|
||||
if (error) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
logger.info('user-context', '✅ Preferences updated successfully');
|
||||
} catch (error) {
|
||||
logger.error('user-context', '❌ Failed to update preferences', { error });
|
||||
setError(error instanceof Error ? error : new Error('Failed to update preferences'));
|
||||
throw error;
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<UserContext.Provider
|
||||
value={{
|
||||
user: profile,
|
||||
loading,
|
||||
error,
|
||||
profile,
|
||||
preferences,
|
||||
isMobile,
|
||||
isInitialized,
|
||||
updateProfile,
|
||||
updatePreferences,
|
||||
clearError: () => setError(null)
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</UserContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
export const useUser = () => useContext(UserContext);
|
||||
Reference in New Issue
Block a user