Initial commit

This commit is contained in:
2025-07-11 13:21:49 +00:00
commit 8a7ab3ac24
262 changed files with 28219 additions and 0 deletions
+157
View File
@@ -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);
+92
View File
@@ -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);
+624
View File
@@ -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);
+221
View File
@@ -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);
+191
View File
@@ -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);