Initial commit
This commit is contained in:
@@ -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);
|
||||
Reference in New Issue
Block a user