Initial commit
This commit is contained in:
@@ -0,0 +1,126 @@
|
||||
import { TLUiAssetUrlOverrides } from '@tldraw/tldraw';
|
||||
|
||||
// Custom asset URLs
|
||||
export const customAssets: TLUiAssetUrlOverrides = {
|
||||
icons: {
|
||||
'sticker-icon': '/icons/sticker-tool.svg'
|
||||
}
|
||||
};
|
||||
|
||||
// Blank canvas snapshot template
|
||||
export const blankCanvasSnapshot = {
|
||||
store: {
|
||||
"document:document": {
|
||||
gridSize: 10,
|
||||
name: "",
|
||||
meta: {},
|
||||
id: "document:document",
|
||||
typeName: "document"
|
||||
},
|
||||
"page:page": {
|
||||
meta: {},
|
||||
id: "page:page",
|
||||
name: "Page 1",
|
||||
index: "a1",
|
||||
typeName: "page"
|
||||
}
|
||||
},
|
||||
schema: {
|
||||
schemaVersion: 2 as const,
|
||||
storeVersion: 4,
|
||||
sequences: {
|
||||
"com.tldraw.store":4,
|
||||
"com.tldraw.asset":1,
|
||||
"com.tldraw.camera":1,
|
||||
"com.tldraw.document":2,
|
||||
"com.tldraw.instance":25,
|
||||
"com.tldraw.instance_page_state":5,
|
||||
"com.tldraw.page":1,
|
||||
"com.tldraw.instance_presence":5,
|
||||
"com.tldraw.pointer":1,
|
||||
"com.tldraw.shape":4,
|
||||
"com.tldraw.asset.bookmark":2,
|
||||
"com.tldraw.asset.image":5,
|
||||
"com.tldraw.asset.video":5,
|
||||
"com.tldraw.shape.arrow":5,
|
||||
"com.tldraw.shape.bookmark":2,
|
||||
"com.tldraw.shape.draw":2,
|
||||
"com.tldraw.shape.embed":4,
|
||||
"com.tldraw.shape.frame":0,
|
||||
"com.tldraw.shape.geo":9,
|
||||
"com.tldraw.shape.group":0,
|
||||
"com.tldraw.shape.highlight":1,
|
||||
"com.tldraw.shape.image":4,
|
||||
"com.tldraw.shape.line":5,
|
||||
"com.tldraw.shape.note":8,
|
||||
"com.tldraw.shape.text":2,
|
||||
"com.tldraw.shape.video":2,
|
||||
"com.tldraw.shape.youtube-embed":0,
|
||||
"com.tldraw.shape.calendar":0,
|
||||
"com.tldraw.shape.microphone":1,
|
||||
"com.tldraw.shape.transcriptionText":0,
|
||||
"com.tldraw.shape.slide":0,"com.tldraw.shape.slideshow":0,
|
||||
"com.tldraw.shape.user_node":1,
|
||||
"com.tldraw.shape.developer_node":1,
|
||||
"com.tldraw.shape.student_node":1,
|
||||
"com.tldraw.shape.teacher_node":1,
|
||||
"com.tldraw.shape.calendar_node":1,
|
||||
"com.tldraw.shape.calendar_year_node":1,
|
||||
"com.tldraw.shape.calendar_month_node":1,
|
||||
"com.tldraw.shape.calendar_week_node":1,
|
||||
"com.tldraw.shape.calendar_day_node":1,
|
||||
"com.tldraw.shape.calendar_time_chunk_node":1,
|
||||
"com.tldraw.shape.teacher_timetable_node":1,
|
||||
"com.tldraw.shape.timetable_lesson_node":1,
|
||||
"com.tldraw.shape.planned_lesson_node":1,
|
||||
"com.tldraw.shape.pastoral_structure_node":1,
|
||||
"com.tldraw.shape.year_group_node":1,
|
||||
"com.tldraw.shape.curriculum_structure_node":1,
|
||||
"com.tldraw.shape.key_stage_node":1,
|
||||
"com.tldraw.shape.key_stage_syllabus_node":1,
|
||||
"com.tldraw.shape.year_group_syllabus_node":1,
|
||||
"com.tldraw.shape.subject_node":1,
|
||||
"com.tldraw.shape.topic_node":1,
|
||||
"com.tldraw.shape.topic_lesson_node":1,
|
||||
"com.tldraw.shape.learning_statement_node":1,
|
||||
"com.tldraw.shape.science_lab_node":1,
|
||||
"com.tldraw.shape.school_timetable_node":1,
|
||||
"com.tldraw.shape.academic_year_node":1,
|
||||
"com.tldraw.shape.academic_term_node":1,
|
||||
"com.tldraw.shape.academic_week_node":1,
|
||||
"com.tldraw.shape.academic_day_node":1,
|
||||
"com.tldraw.shape.academic_period_node":1,
|
||||
"com.tldraw.shape.registration_period_node":1,
|
||||
"com.tldraw.shape.school_node":1,
|
||||
"com.tldraw.shape.department_node":1,
|
||||
"com.tldraw.shape.room_node":1,
|
||||
"com.tldraw.shape.subject_class_node":1,
|
||||
"com.tldraw.shape.general_relationship":1,
|
||||
"com.tldraw.binding.arrow":0,
|
||||
"com.tldraw.binding.slide-layout":0
|
||||
},
|
||||
recordVersions: {
|
||||
asset: { version: 1, subTypeKey: "type", subTypeVersions: {} },
|
||||
camera: { version: 1 },
|
||||
document: { version: 2 },
|
||||
instance: { version: 21 },
|
||||
instance_page_state: { version: 5 },
|
||||
page: { version: 1 },
|
||||
shape: { version: 3, subTypeKey: "type", subTypeVersions: {} },
|
||||
instance_presence: { version: 5 },
|
||||
pointer: { version: 1 }
|
||||
}
|
||||
},
|
||||
rootShapeIds: [],
|
||||
bindings: {},
|
||||
assets: {},
|
||||
session: {
|
||||
version: 0,
|
||||
currentPageId: "page:page",
|
||||
pageStates: [{
|
||||
pageId: "page:page",
|
||||
camera: { x: 0, y: 0, z: 1 },
|
||||
selectedShapeIds: []
|
||||
}]
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,14 @@
|
||||
// TLDraw bindings
|
||||
import { TLAnyBindingUtilConstructor } from '@tldraw/tldraw'
|
||||
import { CCSlideLayoutBindingUtil } from './cc-base/cc-slideshow/CCSlideLayoutBindingUtil'
|
||||
import { ccBindingProps } from './cc-base/cc-props'
|
||||
|
||||
// Export CC bindings
|
||||
export { ccBindingProps }
|
||||
|
||||
// Define all binding utils in a single object for easy maintenance
|
||||
export const BindingUtils = {
|
||||
CCSlideLayout: CCSlideLayoutBindingUtil,
|
||||
}
|
||||
|
||||
export const allBindingUtils: TLAnyBindingUtilConstructor[] = Object.values(BindingUtils)
|
||||
@@ -0,0 +1,138 @@
|
||||
import React from 'react'
|
||||
import { BaseBoxShapeUtil, HTMLContainer, toDomPrecision } from '@tldraw/tldraw'
|
||||
import { CCBaseShape } from './cc-types'
|
||||
import { CC_BASE_STYLE_CONSTANTS } from './cc-styles'
|
||||
import { logger } from '../../../debugConfig'
|
||||
|
||||
export interface ToolbarItem {
|
||||
id: string
|
||||
icon: string | React.ReactNode
|
||||
label: string
|
||||
onClick: (e: React.MouseEvent, shape: CCBaseShape) => void
|
||||
isActive?: boolean
|
||||
}
|
||||
|
||||
export abstract class CCBaseShapeUtil<T extends CCBaseShape> extends BaseBoxShapeUtil<T> {
|
||||
abstract renderContent: (shape: T) => React.ReactElement
|
||||
|
||||
indicator(shape: T) {
|
||||
return (
|
||||
<rect
|
||||
width={shape.props.w}
|
||||
height={shape.props.h}
|
||||
fill="none"
|
||||
rx={CC_BASE_STYLE_CONSTANTS.CONTAINER.borderRadius}
|
||||
stroke={CC_BASE_STYLE_CONSTANTS.COLORS.border}
|
||||
strokeWidth={CC_BASE_STYLE_CONSTANTS.CONTAINER.borderWidth}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
getToolbarItems(shape: T): ToolbarItem[] {
|
||||
return []
|
||||
}
|
||||
|
||||
onAfterCreate(shape: T) {
|
||||
logger.info('cc-base-shape-util', 'onAfterCreate', shape)
|
||||
return shape
|
||||
}
|
||||
|
||||
component(shape: T) {
|
||||
const {
|
||||
props: { w, h, isLocked },
|
||||
} = shape
|
||||
const toolbarItems = this.getToolbarItems(shape)
|
||||
|
||||
return (
|
||||
<HTMLContainer
|
||||
id={shape.id}
|
||||
style={{
|
||||
width: toDomPrecision(w),
|
||||
height: toDomPrecision(h),
|
||||
backgroundColor: shape.props.headerColor,
|
||||
borderRadius: CC_BASE_STYLE_CONSTANTS.CONTAINER.borderRadius,
|
||||
boxShadow: CC_BASE_STYLE_CONSTANTS.CONTAINER.boxShadow,
|
||||
overflow: 'hidden',
|
||||
position: 'relative',
|
||||
}}
|
||||
>
|
||||
{/* Header */}
|
||||
<div
|
||||
style={{
|
||||
backgroundColor: shape.props.headerColor,
|
||||
padding: CC_BASE_STYLE_CONSTANTS.HEADER.padding,
|
||||
height: CC_BASE_STYLE_CONSTANTS.HEADER.height,
|
||||
display: 'flex',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
cursor: isLocked ? 'not-allowed' : 'move',
|
||||
pointerEvents: 'all',
|
||||
position: 'relative',
|
||||
zIndex: 1,
|
||||
}}
|
||||
>
|
||||
<span style={{ color: 'white', fontWeight: 'bold' }}>{shape.props.title}</span>
|
||||
<div style={{ display: 'flex', gap: '4px', alignItems: 'center', pointerEvents: 'all' }}>
|
||||
{toolbarItems.map((item) => (
|
||||
<button
|
||||
key={item.id}
|
||||
title={item.label}
|
||||
onClick={(e) => {
|
||||
logger.info('cc-base-shape-util', 'toolbar item clicked', item.id)
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
item.onClick(e, shape)
|
||||
}}
|
||||
onPointerDown={(e) => {
|
||||
logger.info('cc-base-shape-util', 'toolbar item pointer down', item.id)
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
}}
|
||||
style={{
|
||||
background: 'transparent',
|
||||
border: 'none',
|
||||
padding: '4px',
|
||||
cursor: 'pointer',
|
||||
color: 'white',
|
||||
opacity: item.isActive ? 1 : 0.7,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
pointerEvents: 'all',
|
||||
fontSize: '16px',
|
||||
width: '24px',
|
||||
height: '24px',
|
||||
zIndex: 100,
|
||||
userSelect: 'none',
|
||||
position: 'relative',
|
||||
touchAction: 'none',
|
||||
}}
|
||||
>
|
||||
<div style={{ pointerEvents: 'none' }}>
|
||||
{item.icon}
|
||||
</div>
|
||||
</button>
|
||||
))}
|
||||
{isLocked && <span style={{ color: 'white' }}>🔒</span>}
|
||||
</div>
|
||||
</div>
|
||||
{/* Content */}
|
||||
<div
|
||||
style={{
|
||||
position: 'absolute',
|
||||
top: CC_BASE_STYLE_CONSTANTS.HEADER.height,
|
||||
left: 0,
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
overflow: 'auto',
|
||||
padding: CC_BASE_STYLE_CONSTANTS.CONTENT.padding,
|
||||
backgroundColor: shape.props.backgroundColor,
|
||||
}}
|
||||
>
|
||||
{this.renderContent(shape)}
|
||||
</div>
|
||||
</HTMLContainer>
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
import { CCBaseShapeUtil } from '../CCBaseShapeUtil'
|
||||
import { CCBaseShape } from '../cc-types'
|
||||
import { ccShapeProps, getDefaultCCCalendarProps } from '../cc-props'
|
||||
import { ccShapeMigrations } from '../cc-migrations'
|
||||
import { Rectangle2d } from 'tldraw'
|
||||
import { CalendarComponent, CalendarViewType } from './CalendarComponent'
|
||||
import { TeacherTimetableEvent } from '../../../../services/graph/timetableNeoDBService'
|
||||
|
||||
export interface CCCalendarShape extends CCBaseShape {
|
||||
type: 'cc-calendar'
|
||||
props: CCBaseShape['props'] & {
|
||||
date: string
|
||||
selectedDate: string
|
||||
view: CalendarViewType
|
||||
events: TeacherTimetableEvent[]
|
||||
}
|
||||
}
|
||||
|
||||
export class CCCalendarShapeUtil extends CCBaseShapeUtil<CCCalendarShape> {
|
||||
static override type = 'cc-calendar' as const;
|
||||
static override props = ccShapeProps.calendar;
|
||||
static override migrations = ccShapeMigrations.calendar;
|
||||
|
||||
override getDefaultProps(): CCCalendarShape['props'] {
|
||||
return getDefaultCCCalendarProps() as CCCalendarShape['props'];
|
||||
}
|
||||
|
||||
override isAspectRatioLocked = () => true
|
||||
override canResize = () => true
|
||||
override canBind = () => false
|
||||
|
||||
onCreate = (shape: CCCalendarShape) => {
|
||||
// Force a resize after creation to ensure calendar renders correctly
|
||||
setTimeout(() => {
|
||||
const element = document.getElementById(shape.id)
|
||||
if (element) {
|
||||
const calendar = element.querySelector('.fc') as HTMLElement
|
||||
if (calendar) {
|
||||
calendar.style.height = `${shape.props.h}px`
|
||||
}
|
||||
}
|
||||
}, 0)
|
||||
|
||||
return {
|
||||
...shape,
|
||||
props: this.getDefaultProps(),
|
||||
}
|
||||
}
|
||||
|
||||
override renderContent = (shape: CCCalendarShape) => {
|
||||
return <CalendarComponent shape={shape} />
|
||||
}
|
||||
|
||||
override getGeometry(shape: CCCalendarShape) {
|
||||
return new Rectangle2d({
|
||||
width: shape.props.w,
|
||||
height: shape.props.h,
|
||||
isFilled: true,
|
||||
})
|
||||
}
|
||||
|
||||
override onResize = (
|
||||
shape: CCCalendarShape,
|
||||
info: { initialShape: CCCalendarShape; scaleX: number; scaleY: number }
|
||||
) => {
|
||||
const { initialShape, scaleX, scaleY } = info
|
||||
const newW = Math.max(300, Math.round(initialShape.props.w * scaleX))
|
||||
const newH = Math.max(200, Math.round(initialShape.props.h * scaleY))
|
||||
|
||||
return {
|
||||
props: {
|
||||
...shape.props,
|
||||
w: newW,
|
||||
h: newH,
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,274 @@
|
||||
import React, { useCallback, useEffect, useState, useRef } from 'react'
|
||||
import { HTMLContainer, useEditor, useDialogs } from '@tldraw/tldraw'
|
||||
import FullCalendar from '@fullcalendar/react'
|
||||
import { DateSelectArg, ViewMountArg } from '@fullcalendar/core'
|
||||
import { useNeoUser } from '../../../../contexts/NeoUserContext'
|
||||
import { LoadingState } from '../../../../services/tldraw/snapshotService'
|
||||
import { TimetableNeoDBService, TeacherTimetableEvent } from '../../../../services/graph/timetableNeoDBService'
|
||||
import { useCalendarOptions } from './useCalendarOptions'
|
||||
import { openTldrawFile } from './utils'
|
||||
import { CC_BASE_STYLE_CONSTANTS } from '../cc-styles'
|
||||
import { CCCalendarShape } from './CCCalendarShapeUtil'
|
||||
import { ClassFilterDialog, ViewMenuDialog, EventDetailsDialog } from './CalendarDialogs'
|
||||
|
||||
export type CalendarViewType = 'dayGridMonth' | 'timeGridWeek' | 'timeGridDay' | 'listYear' | 'listMonth' | 'listWeek' | 'listDay' | 'timeGridYear' | 'timeGridMonth'
|
||||
|
||||
interface CalendarComponentProps {
|
||||
shape: CCCalendarShape
|
||||
}
|
||||
|
||||
export const CalendarComponent: React.FC<CalendarComponentProps> = ({ shape }) => {
|
||||
const editor = useEditor()
|
||||
const { addDialog } = useDialogs()
|
||||
const { workerNode, isLoading, error, workerDbName } = useNeoUser()
|
||||
const [events, setEvents] = useState<TeacherTimetableEvent[]>(shape.props.events)
|
||||
const calendarRef = useRef<FullCalendar>(null)
|
||||
const lastFetchRef = useRef<number>(0)
|
||||
const timeoutIdRef = useRef<ReturnType<typeof setTimeout>>()
|
||||
const [selectedClasses, setSelectedClasses] = useState<string[]>([])
|
||||
const [subjectClasses, setSubjectClasses] = useState<string[]>([])
|
||||
const [fileLoadingState, setFileLoadingState] = useState<LoadingState>({
|
||||
status: 'ready',
|
||||
error: ''
|
||||
})
|
||||
|
||||
const debouncedUpdateSize = useCallback(() => {
|
||||
clearTimeout(timeoutIdRef.current);
|
||||
timeoutIdRef.current = setTimeout(() => {
|
||||
if (calendarRef.current) {
|
||||
calendarRef.current.getApi().updateSize();
|
||||
}
|
||||
}, 200);
|
||||
}, [calendarRef]);
|
||||
|
||||
const loadEvents = useCallback(async () => {
|
||||
const now = Date.now()
|
||||
if (now - lastFetchRef.current < 60000) {
|
||||
return;
|
||||
}
|
||||
lastFetchRef.current = now
|
||||
|
||||
if ((events && events.length > 0) || isLoading || error || !workerNode?.nodeData) {
|
||||
if (isLoading || error || !workerNode?.nodeData) {
|
||||
console.error('Unable to fetch events: NeoUser context not ready');
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const fetchedEvents = await TimetableNeoDBService.fetchTeacherTimetableEvents(
|
||||
workerNode.nodeData.unique_id,
|
||||
workerDbName || ''
|
||||
);
|
||||
|
||||
setEvents(fetchedEvents);
|
||||
|
||||
const range = TimetableNeoDBService.getEventRange(fetchedEvents);
|
||||
if (calendarRef.current) {
|
||||
const api = calendarRef.current.getApi();
|
||||
if (range.start && range.end) {
|
||||
api.setOption('validRange', {
|
||||
start: range.start,
|
||||
end: range.end
|
||||
});
|
||||
} else {
|
||||
api.setOption('validRange', undefined);
|
||||
}
|
||||
}
|
||||
|
||||
editor.updateShape({
|
||||
id: shape.id,
|
||||
type: 'cc-calendar',
|
||||
props: { ...shape.props, events: fetchedEvents }
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('Error fetching calendar events:', error)
|
||||
}
|
||||
}, [editor, workerNode, workerDbName, isLoading, error, events, shape.id, shape.props])
|
||||
|
||||
useEffect(() => {
|
||||
debouncedUpdateSize();
|
||||
}, [debouncedUpdateSize]);
|
||||
|
||||
useEffect(() => {
|
||||
loadEvents();
|
||||
}, [loadEvents]);
|
||||
|
||||
const handleDateSelect = useCallback((arg: DateSelectArg) => {
|
||||
editor.updateShape<CCCalendarShape>({
|
||||
id: shape.id,
|
||||
type: 'cc-calendar',
|
||||
props: { ...shape.props, selectedDate: arg.start.toISOString() }
|
||||
})
|
||||
}, [editor, shape.id, shape.props])
|
||||
|
||||
const handleViewChange = useCallback((mountArg: ViewMountArg) => {
|
||||
const newView = mountArg.view.type as CCCalendarShape['props']['view']
|
||||
editor.updateShape<CCCalendarShape>({
|
||||
id: shape.id,
|
||||
type: 'cc-calendar',
|
||||
props: { ...shape.props, view: newView }
|
||||
})
|
||||
}, [editor, shape.id, shape.props])
|
||||
|
||||
const handlePointerDown = useCallback((e: React.PointerEvent) => {
|
||||
e.stopPropagation()
|
||||
}, [])
|
||||
|
||||
const handlePointerUp = useCallback((e: React.PointerEvent) => {
|
||||
e.stopPropagation()
|
||||
}, [])
|
||||
|
||||
const handlePointerMove = useCallback((e: React.PointerEvent) => {
|
||||
e.stopPropagation()
|
||||
}, [])
|
||||
|
||||
const handleWheel = useCallback((e: React.WheelEvent) => {
|
||||
e.stopPropagation()
|
||||
}, [])
|
||||
|
||||
const handleClassToggle = (subjectClass: string) => {
|
||||
setSelectedClasses(prev =>
|
||||
prev.includes(subjectClass)
|
||||
? prev.filter(c => c !== subjectClass)
|
||||
: [...prev, subjectClass]
|
||||
);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const classes = Array.from(new Set(events.map(event => event.extendedProps?.subjectClass || '')));
|
||||
setSubjectClasses(classes);
|
||||
setSelectedClasses(classes);
|
||||
}, [events]);
|
||||
|
||||
const filteredEvents = events.filter(event =>
|
||||
selectedClasses.includes(event.extendedProps?.subjectClass || '')
|
||||
);
|
||||
|
||||
// Calculate available height for calendar
|
||||
const getAvailableHeight = useCallback(() => {
|
||||
const totalPadding = CC_BASE_STYLE_CONSTANTS.CONTENT.padding * 2
|
||||
const availableHeight = shape.props.h - CC_BASE_STYLE_CONSTANTS.HEADER.height - totalPadding
|
||||
return Math.max(availableHeight, CC_BASE_STYLE_CONSTANTS.MIN_DIMENSIONS.height)
|
||||
}, [shape.props.h])
|
||||
|
||||
// Calculate available width for calendar
|
||||
const getAvailableWidth = useCallback(() => {
|
||||
const totalPadding = CC_BASE_STYLE_CONSTANTS.CONTENT.padding * 2
|
||||
const availableWidth = shape.props.w - totalPadding
|
||||
return Math.max(availableWidth, CC_BASE_STYLE_CONSTANTS.MIN_DIMENSIONS.width)
|
||||
}, [shape.props.w])
|
||||
|
||||
const showClassFilterDialog = useCallback(() => {
|
||||
addDialog({
|
||||
component: ({ onClose }) => (
|
||||
<ClassFilterDialog
|
||||
onClose={onClose}
|
||||
subjectClasses={subjectClasses}
|
||||
selectedClasses={selectedClasses}
|
||||
onClassToggle={handleClassToggle}
|
||||
events={events}
|
||||
/>
|
||||
),
|
||||
onClose: () => {
|
||||
// Optional cleanup
|
||||
},
|
||||
})
|
||||
}, [addDialog, subjectClasses, selectedClasses, events, handleClassToggle])
|
||||
|
||||
const showViewMenuDialog = useCallback(() => {
|
||||
addDialog({
|
||||
component: ({ onClose }) => (
|
||||
<ViewMenuDialog
|
||||
onSelect={(view) => {
|
||||
calendarRef.current?.getApi().changeView(view)
|
||||
onClose()
|
||||
}}
|
||||
onClose={onClose}
|
||||
/>
|
||||
),
|
||||
onClose: () => {
|
||||
// Optional cleanup
|
||||
},
|
||||
})
|
||||
}, [addDialog, calendarRef])
|
||||
|
||||
const showEventDetailsDialog = useCallback((event: TeacherTimetableEvent) => {
|
||||
addDialog({
|
||||
component: ({ onClose }) => (
|
||||
<EventDetailsDialog
|
||||
onClose={onClose}
|
||||
selectedEvent={event}
|
||||
fileLoadingState={fileLoadingState}
|
||||
editor={editor}
|
||||
workerDbName={workerDbName || ''}
|
||||
onOpenFile={openTldrawFile}
|
||||
setFileLoadingState={setFileLoadingState}
|
||||
/>
|
||||
),
|
||||
onClose: () => {
|
||||
// Optional cleanup
|
||||
},
|
||||
})
|
||||
}, [addDialog, fileLoadingState, editor, workerDbName])
|
||||
|
||||
const calendarOptions = useCalendarOptions({
|
||||
shape: {
|
||||
...shape,
|
||||
props: {
|
||||
...shape.props,
|
||||
h: getAvailableHeight(),
|
||||
}
|
||||
},
|
||||
filteredEvents,
|
||||
handleDateSelect,
|
||||
handleViewChange,
|
||||
toggleClassFilterModal: showClassFilterDialog,
|
||||
setIsViewMenuOpen: () => showViewMenuDialog(),
|
||||
setSelectedEvent: (event: TeacherTimetableEvent | null) => {
|
||||
if (event) showEventDetailsDialog(event)
|
||||
},
|
||||
setIsEventModalOpen: () => {}, // No longer needed
|
||||
});
|
||||
|
||||
// Update calendar size when shape dimensions change
|
||||
useEffect(() => {
|
||||
if (calendarRef.current) {
|
||||
const calendar = calendarRef.current.getApi();
|
||||
calendar.updateSize();
|
||||
}
|
||||
}, [shape.props.w, shape.props.h]);
|
||||
|
||||
return (
|
||||
<HTMLContainer
|
||||
id={shape.id}
|
||||
style={{
|
||||
width: getAvailableWidth(),
|
||||
height: getAvailableHeight(),
|
||||
position: 'relative',
|
||||
overflow: 'hidden',
|
||||
backgroundColor: CC_BASE_STYLE_CONSTANTS.CONTENT.backgroundColor,
|
||||
padding: CC_BASE_STYLE_CONSTANTS.CONTENT.padding,
|
||||
color: 'black',
|
||||
pointerEvents: 'all',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
}}
|
||||
onPointerDown={handlePointerDown}
|
||||
onPointerUp={handlePointerUp}
|
||||
onPointerMove={handlePointerMove}
|
||||
onWheel={handleWheel}
|
||||
>
|
||||
<div style={{
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
overflow: 'hidden'
|
||||
}}>
|
||||
<FullCalendar
|
||||
ref={calendarRef}
|
||||
{...calendarOptions}
|
||||
/>
|
||||
</div>
|
||||
</HTMLContainer>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,184 @@
|
||||
import {
|
||||
TldrawUiButton,
|
||||
TldrawUiButtonLabel,
|
||||
TldrawUiDialogBody,
|
||||
TldrawUiDialogCloseButton,
|
||||
TldrawUiDialogFooter,
|
||||
TldrawUiDialogHeader,
|
||||
TldrawUiDialogTitle,
|
||||
Editor
|
||||
} from '@tldraw/tldraw'
|
||||
import { FaCheck, FaExternalLinkAlt } from 'react-icons/fa'
|
||||
import { TeacherTimetableEvent } from '../../../../services/graph/timetableNeoDBService'
|
||||
import { LoadingState } from '../../../../services/tldraw/snapshotService'
|
||||
|
||||
// View Menu Dialog Component
|
||||
interface ViewMenuDialogProps {
|
||||
onSelect: (view: string) => void
|
||||
onClose: () => void
|
||||
}
|
||||
|
||||
export const ViewMenuDialog = ({ onSelect, onClose }: ViewMenuDialogProps) => {
|
||||
const views = [
|
||||
{ name: 'Day', view: 'timeGridDay' },
|
||||
{ name: 'Week', view: 'timeGridWeek' },
|
||||
{ name: 'Month', view: 'dayGridMonth' },
|
||||
{ name: 'Year', view: 'dayGridYear' },
|
||||
{ name: 'List Day', view: 'listDay' },
|
||||
{ name: 'List Week', view: 'listWeek' },
|
||||
{ name: 'List Month', view: 'listMonth' },
|
||||
{ name: 'List Year', view: 'listYear' },
|
||||
]
|
||||
|
||||
return (
|
||||
<>
|
||||
<TldrawUiDialogHeader>
|
||||
<TldrawUiDialogTitle>Select Calendar View</TldrawUiDialogTitle>
|
||||
<TldrawUiDialogCloseButton />
|
||||
</TldrawUiDialogHeader>
|
||||
<TldrawUiDialogBody style={{ display: 'flex', flexDirection: 'column', gap: '10px' }}>
|
||||
{views.map(({name, view}) => (
|
||||
<TldrawUiButton key={view} type="normal" onClick={() => onSelect(view)}>
|
||||
<TldrawUiButtonLabel>{name}</TldrawUiButtonLabel>
|
||||
</TldrawUiButton>
|
||||
))}
|
||||
</TldrawUiDialogBody>
|
||||
<TldrawUiDialogFooter>
|
||||
<TldrawUiButton type="normal" onClick={onClose}>
|
||||
<TldrawUiButtonLabel>Close</TldrawUiButtonLabel>
|
||||
</TldrawUiButton>
|
||||
</TldrawUiDialogFooter>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
// Class Filter Dialog
|
||||
interface ClassFilterDialogProps {
|
||||
onClose: () => void
|
||||
subjectClasses: string[]
|
||||
selectedClasses: string[]
|
||||
onClassToggle: (subjectClass: string) => void
|
||||
events: TeacherTimetableEvent[]
|
||||
}
|
||||
|
||||
export const ClassFilterDialog = ({
|
||||
onClose,
|
||||
subjectClasses,
|
||||
selectedClasses,
|
||||
onClassToggle,
|
||||
events
|
||||
}: ClassFilterDialogProps) => {
|
||||
const renderClassFilterButton = (subjectClass: string) => {
|
||||
const isSelected = selectedClasses.includes(subjectClass)
|
||||
const color = events.find(e => e.extendedProps?.subjectClass === subjectClass)?.extendedProps?.color || '#000000'
|
||||
|
||||
return (
|
||||
<TldrawUiButton
|
||||
key={subjectClass}
|
||||
type="normal"
|
||||
onClick={() => onClassToggle(subjectClass)}
|
||||
style={{
|
||||
backgroundColor: isSelected ? color : '#ffffff',
|
||||
color: isSelected ? '#ffffff' : '#000000',
|
||||
border: `2px solid ${color}`,
|
||||
}}
|
||||
>
|
||||
<TldrawUiButtonLabel>
|
||||
<div style={{ display: 'flex', alignItems: 'center' }}>
|
||||
<div style={{ marginRight: '8px' }}>
|
||||
{isSelected && <FaCheck />}
|
||||
</div>
|
||||
<span>{subjectClass}</span>
|
||||
</div>
|
||||
</TldrawUiButtonLabel>
|
||||
</TldrawUiButton>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<TldrawUiDialogHeader>
|
||||
<TldrawUiDialogTitle>Filter Classes</TldrawUiDialogTitle>
|
||||
<TldrawUiDialogCloseButton />
|
||||
</TldrawUiDialogHeader>
|
||||
<TldrawUiDialogBody style={{ display: 'flex', flexDirection: 'column', gap: '10px' }}>
|
||||
{subjectClasses.map(renderClassFilterButton)}
|
||||
</TldrawUiDialogBody>
|
||||
<TldrawUiDialogFooter>
|
||||
<TldrawUiButton type="normal" onClick={onClose}>
|
||||
<TldrawUiButtonLabel>Close</TldrawUiButtonLabel>
|
||||
</TldrawUiButton>
|
||||
</TldrawUiDialogFooter>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
// Event Details Dialog
|
||||
interface EventDetailsDialogProps {
|
||||
onClose: () => void
|
||||
selectedEvent: TeacherTimetableEvent | null
|
||||
fileLoadingState: LoadingState
|
||||
editor: Editor
|
||||
workerDbName: string | undefined
|
||||
onOpenFile: (path: string, dbName: string, editor: Editor, setFileLoadingState: (state: LoadingState) => void) => void
|
||||
setFileLoadingState: (state: LoadingState) => void
|
||||
}
|
||||
|
||||
export const EventDetailsDialog = ({
|
||||
onClose,
|
||||
selectedEvent,
|
||||
fileLoadingState,
|
||||
editor,
|
||||
workerDbName,
|
||||
onOpenFile,
|
||||
setFileLoadingState
|
||||
}: EventDetailsDialogProps) => {
|
||||
const handleOpenFile = () => {
|
||||
if (!selectedEvent?.extendedProps?.tldraw_snapshot || !workerDbName) {
|
||||
console.error('❌ Failed to open tldraw file - missing snapshot or db name')
|
||||
return
|
||||
}
|
||||
|
||||
onOpenFile(
|
||||
selectedEvent.extendedProps.tldraw_snapshot,
|
||||
workerDbName,
|
||||
editor,
|
||||
setFileLoadingState
|
||||
)
|
||||
}
|
||||
|
||||
if (!selectedEvent) return null
|
||||
|
||||
return (
|
||||
<>
|
||||
<TldrawUiDialogHeader>
|
||||
<TldrawUiDialogTitle>{selectedEvent.title}</TldrawUiDialogTitle>
|
||||
<TldrawUiDialogCloseButton />
|
||||
</TldrawUiDialogHeader>
|
||||
<TldrawUiDialogBody>
|
||||
<p><strong>Start Time:</strong> {new Date(selectedEvent.start).toLocaleString()}</p>
|
||||
<p><strong>End Time:</strong> {new Date(selectedEvent.end).toLocaleString()}</p>
|
||||
<p><strong>Class:</strong> {selectedEvent.extendedProps?.subjectClass}</p>
|
||||
<p><strong>Period:</strong> {selectedEvent.extendedProps?.periodCode}</p>
|
||||
|
||||
{fileLoadingState.status === 'loading' && <p>Loading file...</p>}
|
||||
{fileLoadingState.status === 'error' && (
|
||||
<p style={{color: 'red'}}>Error: {fileLoadingState.error}</p>
|
||||
)}
|
||||
|
||||
{selectedEvent.extendedProps?.tldraw_snapshot && fileLoadingState.status !== 'loading' && (
|
||||
<TldrawUiButton type="normal" onClick={handleOpenFile}>
|
||||
<TldrawUiButtonLabel>
|
||||
Open Tldraw File <FaExternalLinkAlt style={{ marginLeft: '8px' }} />
|
||||
</TldrawUiButtonLabel>
|
||||
</TldrawUiButton>
|
||||
)}
|
||||
</TldrawUiDialogBody>
|
||||
<TldrawUiDialogFooter>
|
||||
<TldrawUiButton type="normal" onClick={onClose}>
|
||||
<TldrawUiButtonLabel>Close</TldrawUiButtonLabel>
|
||||
</TldrawUiButton>
|
||||
</TldrawUiDialogFooter>
|
||||
</>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,131 @@
|
||||
import { CalendarOptions, EventContentArg, formatDate, DateSelectArg, ViewMountArg } from '@fullcalendar/core'
|
||||
import dayGridPlugin from '@fullcalendar/daygrid'
|
||||
import timeGridPlugin from '@fullcalendar/timegrid'
|
||||
import listPlugin from '@fullcalendar/list'
|
||||
import interactionPlugin from '@fullcalendar/interaction'
|
||||
import { TimetableNeoDBService } from '../../../../services/graph/timetableNeoDBService'
|
||||
import { CC_CALENDAR_STYLE_CONSTANTS } from '../cc-styles'
|
||||
|
||||
// Calendar layout constants
|
||||
export const CALENDAR_LAYOUT = {
|
||||
TOOLBAR_HEIGHT: 5,
|
||||
HEADER_PADDING: 5,
|
||||
CONTENT_PADDING: 5,
|
||||
FOOTER_HEIGHT: 0,
|
||||
// Function to calculate available content height
|
||||
getAvailableHeight: (totalHeight: number) => {
|
||||
return totalHeight - (
|
||||
CALENDAR_LAYOUT.TOOLBAR_HEIGHT +
|
||||
CALENDAR_LAYOUT.HEADER_PADDING * 2 +
|
||||
CALENDAR_LAYOUT.CONTENT_PADDING * 2 +
|
||||
CALENDAR_LAYOUT.FOOTER_HEIGHT
|
||||
);
|
||||
}
|
||||
} as const;
|
||||
|
||||
// Re-export types that other files might need
|
||||
export type { CalendarOptions, EventContentArg, DateSelectArg, ViewMountArg }
|
||||
|
||||
// Export plugins
|
||||
export const getCalendarPlugins = () => [
|
||||
dayGridPlugin,
|
||||
timeGridPlugin,
|
||||
listPlugin,
|
||||
interactionPlugin,
|
||||
]
|
||||
|
||||
// Export event content renderer
|
||||
export const renderEventContent = (arg: EventContentArg) => {
|
||||
const backgroundColor = TimetableNeoDBService.lightenColor(arg.event.extendedProps?.color || '#000000', 100);
|
||||
const textColor = TimetableNeoDBService.getContrastColor(backgroundColor);
|
||||
|
||||
const formatEventTime = (date: Date | null) => {
|
||||
if (!date) {
|
||||
return '';
|
||||
}
|
||||
return formatDate(date, {
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
hour12: true,
|
||||
timeZone: 'local'
|
||||
});
|
||||
};
|
||||
|
||||
const formattedStartTime = formatEventTime(arg.event.start); //DO NOT REMOVE THIS COMMENT
|
||||
const formattedEndTime = formatEventTime(arg.event.end); //DO NOT REMOVE THIS COMMENT
|
||||
const timeAndPeriodCodeString = `${arg.event.extendedProps?.periodCode || ''}`; //DO NOT REMOVE THIS COMMENT
|
||||
|
||||
return {
|
||||
html: `
|
||||
<div class="fc-event-main-frame" style="background-color: ${CC_CALENDAR_STYLE_CONSTANTS.EVENT.mainFrame.backgroundColor}; color: ${textColor}; display: ${CC_CALENDAR_STYLE_CONSTANTS.EVENT.mainFrame.display}; align-items: ${CC_CALENDAR_STYLE_CONSTANTS.EVENT.mainFrame.alignItems}; justify-content: ${CC_CALENDAR_STYLE_CONSTANTS.EVENT.mainFrame.justifyContent}; min-height: ${CC_CALENDAR_STYLE_CONSTANTS.EVENT.mainFrame.minHeight}; padding: ${CC_CALENDAR_STYLE_CONSTANTS.EVENT.mainFrame.padding}; border-radius: ${CC_CALENDAR_STYLE_CONSTANTS.EVENT.mainFrame.borderRadius};">
|
||||
<div class="fc-event-title fc-sticky" style="color: ${textColor}; font-size: ${CC_CALENDAR_STYLE_CONSTANTS.EVENT.title.fontSize}; font-weight: ${CC_CALENDAR_STYLE_CONSTANTS.EVENT.title.fontWeight}; text-align: ${CC_CALENDAR_STYLE_CONSTANTS.EVENT.title.textAlign}; overflow: ${CC_CALENDAR_STYLE_CONSTANTS.EVENT.title.overflow}; text-overflow: ${CC_CALENDAR_STYLE_CONSTANTS.EVENT.title.textOverflow}; white-space: ${CC_CALENDAR_STYLE_CONSTANTS.EVENT.title.whiteSpace}; opacity: ${CC_CALENDAR_STYLE_CONSTANTS.EVENT.title.opacity}; padding: ${CC_CALENDAR_STYLE_CONSTANTS.EVENT.title.padding}; width: ${CC_CALENDAR_STYLE_CONSTANTS.EVENT.title.width}; letter-spacing: ${CC_CALENDAR_STYLE_CONSTANTS.EVENT.title.letterSpacing}; margin: ${CC_CALENDAR_STYLE_CONSTANTS.EVENT.title.margin};">${arg.event.title}</div>
|
||||
</div>
|
||||
`
|
||||
};
|
||||
}
|
||||
|
||||
// Export base calendar options
|
||||
export const getBaseCalendarOptions = (): Partial<CalendarOptions> => ({
|
||||
aspectRatio: undefined,
|
||||
expandRows: true,
|
||||
stickyHeaderDates: true,
|
||||
stickyFooterScrollbar: false,
|
||||
dayHeaders: true,
|
||||
dayHeaderFormat(arg) {
|
||||
return formatDate(arg.date.marker, { weekday: 'long' });
|
||||
},
|
||||
titleFormat: (arg) => {
|
||||
return formatDate(arg.date.marker, { weekday: 'long' });
|
||||
},
|
||||
dayMaxEvents: 10,
|
||||
selectable: true,
|
||||
eventMinHeight: 10,
|
||||
navLinks: true,
|
||||
hiddenDays: [1],
|
||||
weekends: false,
|
||||
slotMinTime: '08:00:00',
|
||||
slotMaxTime: '16:00:00',
|
||||
slotDuration: '00:30:00',
|
||||
slotLabelInterval: '01:00',
|
||||
allDaySlot: false,
|
||||
slotEventOverlap: true,
|
||||
dayMaxEventRows: 1,
|
||||
eventMaxStack: 1,
|
||||
slotLabelFormat: [
|
||||
{
|
||||
formatMatcher: 'best fit',
|
||||
hour: 'numeric',
|
||||
minute: '2-digit',
|
||||
omitZeroMinute: true,
|
||||
meridiem: 'narrow',
|
||||
hour12: false,
|
||||
}
|
||||
],
|
||||
buttonText: {
|
||||
today: 'Today',
|
||||
month: 'Month',
|
||||
week: 'Week',
|
||||
day: 'Day',
|
||||
list: 'List',
|
||||
},
|
||||
buttonIcons: {
|
||||
prev: 'chevron-left',
|
||||
next: 'chevron-right',
|
||||
prevYear: 'chevrons-left',
|
||||
nextYear: 'chevrons-right',
|
||||
},
|
||||
themeSystem: 'standard',
|
||||
eventContent: renderEventContent,
|
||||
eventClassNames: (arg: { event: { extendedProps?: { subjectClass?: string; color?: string } } }) => {
|
||||
return [arg.event.extendedProps?.subjectClass || ''];
|
||||
},
|
||||
eventDidMount: (arg: { event: { extendedProps?: { color?: string }; id: string }; el: HTMLElement }) => {
|
||||
if (arg.event.extendedProps?.color) {
|
||||
const originalColor = arg.event.extendedProps.color;
|
||||
const lightenedColor = TimetableNeoDBService.lightenColor(originalColor, 100);
|
||||
arg.el.style.backgroundColor = lightenedColor;
|
||||
arg.el.style.borderColor = originalColor;
|
||||
arg.el.style.color = TimetableNeoDBService.getContrastColor(lightenedColor);
|
||||
}
|
||||
},
|
||||
})
|
||||
@@ -0,0 +1,62 @@
|
||||
import { TeacherTimetableEvent } from '../../../../services/graph/timetableNeoDBService'
|
||||
import { getBaseCalendarOptions, getCalendarPlugins, CalendarOptions, DateSelectArg, ViewMountArg, CALENDAR_LAYOUT } from './calendarOptions'
|
||||
|
||||
interface UseCalendarOptionsProps {
|
||||
shape: {
|
||||
props: {
|
||||
view: string
|
||||
h: number
|
||||
}
|
||||
}
|
||||
filteredEvents: TeacherTimetableEvent[]
|
||||
handleDateSelect: (arg: DateSelectArg) => void
|
||||
handleViewChange: (arg: ViewMountArg) => void
|
||||
toggleClassFilterModal: () => void
|
||||
setIsViewMenuOpen: (isOpen: boolean) => void
|
||||
setSelectedEvent: (event: TeacherTimetableEvent | null) => void
|
||||
setIsEventModalOpen: (isOpen: boolean) => void
|
||||
}
|
||||
|
||||
export const useCalendarOptions = ({
|
||||
shape,
|
||||
filteredEvents,
|
||||
handleDateSelect,
|
||||
handleViewChange,
|
||||
toggleClassFilterModal,
|
||||
setIsViewMenuOpen,
|
||||
setSelectedEvent,
|
||||
setIsEventModalOpen,
|
||||
}: UseCalendarOptionsProps): CalendarOptions => {
|
||||
const availableHeight = CALENDAR_LAYOUT.getAvailableHeight(shape.props.h);
|
||||
|
||||
return {
|
||||
...getBaseCalendarOptions(),
|
||||
plugins: getCalendarPlugins(),
|
||||
initialView: shape.props.view,
|
||||
events: filteredEvents,
|
||||
select: handleDateSelect,
|
||||
viewDidMount: handleViewChange,
|
||||
height: availableHeight,
|
||||
contentHeight: availableHeight,
|
||||
headerToolbar: {
|
||||
left: 'prev,next today viewToggle',
|
||||
center: 'title',
|
||||
right: 'filterClasses'
|
||||
},
|
||||
customButtons: {
|
||||
filterClasses: {
|
||||
text: 'Classes',
|
||||
click: toggleClassFilterModal,
|
||||
},
|
||||
viewToggle: {
|
||||
text: 'View',
|
||||
click: () => setIsViewMenuOpen(true),
|
||||
},
|
||||
},
|
||||
eventClick: (arg) => {
|
||||
const eventData = arg.event.toPlainObject();
|
||||
setSelectedEvent(eventData as unknown as TeacherTimetableEvent);
|
||||
setIsEventModalOpen(true);
|
||||
},
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,28 @@
|
||||
import { Editor } from '@tldraw/tldraw'
|
||||
import { LoadingState, NavigationSnapshotService } from '../../../../services/tldraw/snapshotService'
|
||||
import logger from '../../../../debugConfig'
|
||||
|
||||
export const openTldrawFile = async (
|
||||
path: string,
|
||||
dbName: string,
|
||||
editor: Editor,
|
||||
setFileLoadingState: (state: LoadingState) => void
|
||||
) => {
|
||||
logger.info('calendar-shape', '📂 Opening tldraw file', {
|
||||
path,
|
||||
db_name: dbName
|
||||
});
|
||||
|
||||
try {
|
||||
await NavigationSnapshotService.loadNodeSnapshotFromDatabase(
|
||||
path,
|
||||
dbName,
|
||||
editor.store,
|
||||
setFileLoadingState
|
||||
);
|
||||
} catch (error) {
|
||||
logger.error('calendar-shape', '❌ Failed to open tldraw file', {
|
||||
error: error instanceof Error ? error.message : 'Unknown error'
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,108 @@
|
||||
import { CC_BASE_STYLE_CONSTANTS, CC_SLIDESHOW_STYLE_CONSTANTS } from './cc-styles'
|
||||
import { TLShapeId } from '@tldraw/tldraw'
|
||||
|
||||
export interface CCShapeConfig {
|
||||
width: number
|
||||
height: number
|
||||
xOffset: number
|
||||
yOffset: number
|
||||
defaultProps: {
|
||||
title: string
|
||||
headerColor: string
|
||||
backgroundColor: string
|
||||
isLocked: boolean
|
||||
[key: string]: string | number | boolean | Date | TLShapeId[] | undefined | null
|
||||
}
|
||||
}
|
||||
|
||||
export const CC_SHAPE_CONFIGS: Record<string, CCShapeConfig> = {
|
||||
'cc-calendar': {
|
||||
width: 400,
|
||||
height: 600,
|
||||
xOffset: 0,
|
||||
yOffset: 0,
|
||||
defaultProps: {
|
||||
title: 'Calendar',
|
||||
headerColor: CC_BASE_STYLE_CONSTANTS.COLORS.primary,
|
||||
backgroundColor: CC_BASE_STYLE_CONSTANTS.CONTENT.backgroundColor,
|
||||
isLocked: false,
|
||||
date: new Date().toISOString(),
|
||||
events: [],
|
||||
view: 'timeGridWeek',
|
||||
}
|
||||
},
|
||||
'cc-settings': {
|
||||
width: 400,
|
||||
height: 500,
|
||||
xOffset: 0,
|
||||
yOffset: 0,
|
||||
defaultProps: {
|
||||
title: 'User Settings',
|
||||
headerColor: CC_BASE_STYLE_CONSTANTS.COLORS.primary,
|
||||
backgroundColor: CC_BASE_STYLE_CONSTANTS.CONTENT.backgroundColor,
|
||||
isLocked: false,
|
||||
}
|
||||
},
|
||||
'cc-live-transcription': {
|
||||
width: 300,
|
||||
height: 400,
|
||||
xOffset: 0,
|
||||
yOffset: 0,
|
||||
defaultProps: {
|
||||
title: 'Live Transcription',
|
||||
headerColor: CC_BASE_STYLE_CONSTANTS.COLORS.primary,
|
||||
backgroundColor: CC_BASE_STYLE_CONSTANTS.CONTENT.backgroundColor,
|
||||
isLocked: false,
|
||||
isRecording: false,
|
||||
segments: [],
|
||||
currentSegment: undefined,
|
||||
lastProcessedSegment: undefined,
|
||||
}
|
||||
},
|
||||
'cc-slideshow': {
|
||||
width: CC_SLIDESHOW_STYLE_CONSTANTS.DEFAULT_SLIDE_WIDTH * 3 + CC_SLIDESHOW_STYLE_CONSTANTS.SLIDE_SPACING * 4,
|
||||
height: CC_SLIDESHOW_STYLE_CONSTANTS.DEFAULT_SLIDE_HEIGHT +
|
||||
CC_SLIDESHOW_STYLE_CONSTANTS.SLIDE_HEADER_HEIGHT +
|
||||
CC_SLIDESHOW_STYLE_CONSTANTS.SLIDE_SPACING * 2 +
|
||||
CC_SLIDESHOW_STYLE_CONSTANTS.SLIDE_CONTENT_PADDING * 2,
|
||||
xOffset: 0,
|
||||
yOffset: 0,
|
||||
defaultProps: {
|
||||
title: 'Slideshow',
|
||||
headerColor: CC_BASE_STYLE_CONSTANTS.COLORS.primary,
|
||||
backgroundColor: CC_BASE_STYLE_CONSTANTS.CONTENT.backgroundColor,
|
||||
isLocked: false,
|
||||
currentSlideIndex: 0,
|
||||
slidePattern: 'horizontal',
|
||||
numSlides: 3,
|
||||
}
|
||||
},
|
||||
'cc-slide': {
|
||||
width: CC_SLIDESHOW_STYLE_CONSTANTS.DEFAULT_SLIDE_WIDTH,
|
||||
height: CC_SLIDESHOW_STYLE_CONSTANTS.DEFAULT_SLIDE_HEIGHT,
|
||||
xOffset: 0,
|
||||
yOffset: 0,
|
||||
defaultProps: {
|
||||
title: 'Slide',
|
||||
headerColor: CC_BASE_STYLE_CONSTANTS.COLORS.primary,
|
||||
backgroundColor: CC_BASE_STYLE_CONSTANTS.CONTENT.backgroundColor,
|
||||
isLocked: false,
|
||||
imageData: '',
|
||||
}
|
||||
},
|
||||
'cc-youtube-embed': {
|
||||
width: 800,
|
||||
height: 450 + CC_BASE_STYLE_CONSTANTS.HEADER.height + (CC_BASE_STYLE_CONSTANTS.CONTENT.padding * 2),
|
||||
xOffset: 0,
|
||||
yOffset: 0,
|
||||
defaultProps: {
|
||||
title: 'YouTube Video',
|
||||
headerColor: CC_BASE_STYLE_CONSTANTS.COLORS.primary,
|
||||
backgroundColor: CC_BASE_STYLE_CONSTANTS.CONTENT.backgroundColor,
|
||||
isLocked: false,
|
||||
video_url: 'https://www.youtube.com/watch?v=dQw4w9WgXcQ',
|
||||
transcript: [],
|
||||
transcriptVisible: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
import { CCBaseShapeUtil } from '../CCBaseShapeUtil'
|
||||
import { ccGraphShapeProps, getDefaultCCAcademicDayNodeProps } from './cc-graph-props'
|
||||
import { CCAcademicDayNodeProps } from './cc-graph-types'
|
||||
import { NodeProperty } from './cc-graph-shared'
|
||||
import { getNodeStyles } from './cc-graph-styles'
|
||||
import { NODE_THEMES, NODE_TYPE_THEMES } from './cc-graph-styles'
|
||||
import { CCBaseShape } from '../cc-types'
|
||||
|
||||
export interface CCAcademicDayNodeShape extends CCBaseShape {
|
||||
type: 'cc-academic-day-node'
|
||||
props: CCAcademicDayNodeProps
|
||||
}
|
||||
|
||||
export class CCAcademicDayNodeShapeUtil extends CCBaseShapeUtil<CCAcademicDayNodeShape> {
|
||||
static type = 'cc-academic-day-node' as const
|
||||
static props = ccGraphShapeProps['cc-academic-day-node']
|
||||
|
||||
getDefaultProps(): CCAcademicDayNodeShape['props'] {
|
||||
const defaultProps = getDefaultCCAcademicDayNodeProps()
|
||||
const theme = NODE_THEMES[NODE_TYPE_THEMES[CCAcademicDayNodeShapeUtil.type]]
|
||||
return {
|
||||
...defaultProps,
|
||||
headerColor: theme.headerColor,
|
||||
}
|
||||
}
|
||||
|
||||
// Override to nullify the default node component
|
||||
DefaultComponent = () => null
|
||||
|
||||
renderContent = (shape: CCAcademicDayNodeShape) => {
|
||||
const styles = getNodeStyles(shape.type)
|
||||
|
||||
return (
|
||||
<div style={styles.container}>
|
||||
<NodeProperty
|
||||
label="Academic Day"
|
||||
value={shape.props.academic_day}
|
||||
labelStyle={styles.property.label}
|
||||
valueStyle={styles.property.value}
|
||||
/>
|
||||
<NodeProperty
|
||||
label="Date"
|
||||
value={shape.props.date}
|
||||
labelStyle={styles.property.label}
|
||||
valueStyle={styles.property.value}
|
||||
/>
|
||||
<NodeProperty
|
||||
label="Day of Week"
|
||||
value={shape.props.day_of_week}
|
||||
labelStyle={styles.property.label}
|
||||
valueStyle={styles.property.value}
|
||||
/>
|
||||
<NodeProperty
|
||||
label="Day Type"
|
||||
value={shape.props.day_type}
|
||||
labelStyle={styles.property.label}
|
||||
valueStyle={styles.property.value}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
import { CCBaseShapeUtil } from '../CCBaseShapeUtil'
|
||||
import { CCBaseShape } from '../cc-types'
|
||||
import { NodeProperty } from './cc-graph-shared'
|
||||
import { ccGraphShapeProps, getDefaultCCAcademicPeriodNodeProps } from './cc-graph-props'
|
||||
import { CCAcademicPeriodNodeProps } from './cc-graph-types'
|
||||
import { getNodeStyles } from './cc-graph-styles'
|
||||
import { NODE_THEMES, NODE_TYPE_THEMES } from './cc-graph-styles'
|
||||
|
||||
export interface CCAcademicPeriodNodeShape extends CCBaseShape {
|
||||
type: 'cc-academic-period-node'
|
||||
props: CCAcademicPeriodNodeProps
|
||||
}
|
||||
|
||||
export class CCAcademicPeriodNodeShapeUtil extends CCBaseShapeUtil<CCAcademicPeriodNodeShape> {
|
||||
static type = 'cc-academic-period-node' as const
|
||||
static props = ccGraphShapeProps['cc-academic-period-node']
|
||||
|
||||
getDefaultProps(): CCAcademicPeriodNodeShape['props'] {
|
||||
const defaultProps = getDefaultCCAcademicPeriodNodeProps()
|
||||
const theme = NODE_THEMES[NODE_TYPE_THEMES[CCAcademicPeriodNodeShapeUtil.type]]
|
||||
return {
|
||||
...defaultProps,
|
||||
headerColor: theme.headerColor,
|
||||
}
|
||||
}
|
||||
|
||||
// Override to nullify the default node component
|
||||
DefaultComponent = () => null
|
||||
|
||||
renderContent = (shape: CCAcademicPeriodNodeShape) => {
|
||||
const styles = getNodeStyles(shape.type)
|
||||
|
||||
return (
|
||||
<div style={styles.container}>
|
||||
<NodeProperty
|
||||
label="Name"
|
||||
value={shape.props.name}
|
||||
labelStyle={styles.property.label}
|
||||
valueStyle={styles.property.value}
|
||||
/>
|
||||
<NodeProperty
|
||||
label="Date"
|
||||
value={shape.props.date}
|
||||
labelStyle={styles.property.label}
|
||||
valueStyle={styles.property.value}
|
||||
/>
|
||||
<NodeProperty
|
||||
label="Start Time"
|
||||
value={shape.props.start_time}
|
||||
labelStyle={styles.property.label}
|
||||
valueStyle={styles.property.value}
|
||||
/>
|
||||
<NodeProperty
|
||||
label="End Time"
|
||||
value={shape.props.end_time}
|
||||
labelStyle={styles.property.label}
|
||||
valueStyle={styles.property.value}
|
||||
/>
|
||||
<NodeProperty
|
||||
label="Period Code"
|
||||
value={shape.props.period_code}
|
||||
labelStyle={styles.property.label}
|
||||
valueStyle={styles.property.value}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
import { CCBaseShapeUtil } from '../CCBaseShapeUtil'
|
||||
import { CCBaseShape } from '../cc-types'
|
||||
import { NodeProperty } from './cc-graph-shared'
|
||||
import { ccGraphShapeProps, getDefaultCCAcademicTermNodeProps } from './cc-graph-props'
|
||||
import { CCAcademicTermNodeProps } from './cc-graph-types'
|
||||
import { getNodeStyles } from './cc-graph-styles'
|
||||
import { NODE_THEMES, NODE_TYPE_THEMES } from './cc-graph-styles'
|
||||
|
||||
export interface CCAcademicTermNodeShape extends CCBaseShape {
|
||||
type: 'cc-academic-term-node'
|
||||
props: CCAcademicTermNodeProps
|
||||
}
|
||||
|
||||
export class CCAcademicTermNodeShapeUtil extends CCBaseShapeUtil<CCAcademicTermNodeShape> {
|
||||
static type = 'cc-academic-term-node' as const
|
||||
static props = ccGraphShapeProps['cc-academic-term-node']
|
||||
|
||||
getDefaultProps(): CCAcademicTermNodeShape['props'] {
|
||||
const defaultProps = getDefaultCCAcademicTermNodeProps()
|
||||
const theme = NODE_THEMES[NODE_TYPE_THEMES[CCAcademicTermNodeShapeUtil.type]]
|
||||
return {
|
||||
...defaultProps,
|
||||
headerColor: theme.headerColor,
|
||||
}
|
||||
}
|
||||
|
||||
// Override to nullify the default node component
|
||||
DefaultComponent = () => null
|
||||
|
||||
renderContent = (shape: CCAcademicTermNodeShape) => {
|
||||
const styles = getNodeStyles(shape.type)
|
||||
|
||||
return (
|
||||
<div style={styles.container}>
|
||||
<NodeProperty
|
||||
label="Term Name"
|
||||
value={shape.props.term_name}
|
||||
labelStyle={styles.property.label}
|
||||
valueStyle={styles.property.value}
|
||||
/>
|
||||
<NodeProperty
|
||||
label="Term Number"
|
||||
value={shape.props.term_number}
|
||||
labelStyle={styles.property.label}
|
||||
valueStyle={styles.property.value}
|
||||
/>
|
||||
<NodeProperty
|
||||
label="Start Date"
|
||||
value={shape.props.start_date}
|
||||
labelStyle={styles.property.label}
|
||||
valueStyle={styles.property.value}
|
||||
/>
|
||||
<NodeProperty
|
||||
label="End Date"
|
||||
value={shape.props.end_date}
|
||||
labelStyle={styles.property.label}
|
||||
valueStyle={styles.property.value}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
import { CCBaseShapeUtil } from '../CCBaseShapeUtil'
|
||||
import { CCBaseShape } from '../cc-types'
|
||||
import { NodeProperty } from './cc-graph-shared'
|
||||
import { ccGraphShapeProps, getDefaultCCAcademicWeekNodeProps } from './cc-graph-props'
|
||||
import { CCAcademicWeekNodeProps } from './cc-graph-types'
|
||||
import { getNodeStyles } from './cc-graph-styles'
|
||||
import { NODE_THEMES, NODE_TYPE_THEMES } from './cc-graph-styles'
|
||||
|
||||
export interface CCAcademicWeekNodeShape extends CCBaseShape {
|
||||
type: 'cc-academic-week-node'
|
||||
props: CCAcademicWeekNodeProps
|
||||
}
|
||||
|
||||
export class CCAcademicWeekNodeShapeUtil extends CCBaseShapeUtil<CCAcademicWeekNodeShape> {
|
||||
static type = 'cc-academic-week-node' as const
|
||||
static props = ccGraphShapeProps['cc-academic-week-node']
|
||||
|
||||
getDefaultProps(): CCAcademicWeekNodeShape['props'] {
|
||||
const defaultProps = getDefaultCCAcademicWeekNodeProps()
|
||||
const theme = NODE_THEMES[NODE_TYPE_THEMES[CCAcademicWeekNodeShapeUtil.type]]
|
||||
return {
|
||||
...defaultProps,
|
||||
headerColor: theme.headerColor,
|
||||
}
|
||||
}
|
||||
|
||||
// Override to nullify the default node component
|
||||
DefaultComponent = () => null
|
||||
|
||||
renderContent = (shape: CCAcademicWeekNodeShape) => {
|
||||
const styles = getNodeStyles(shape.type)
|
||||
|
||||
return (
|
||||
<div style={styles.container}>
|
||||
<NodeProperty
|
||||
label="Academic Week Number"
|
||||
value={shape.props.academic_week_number}
|
||||
labelStyle={styles.property.label}
|
||||
valueStyle={styles.property.value}
|
||||
/>
|
||||
<NodeProperty
|
||||
label="Start Date"
|
||||
value={shape.props.start_date}
|
||||
labelStyle={styles.property.label}
|
||||
valueStyle={styles.property.value}
|
||||
/>
|
||||
<NodeProperty
|
||||
label="Week Type"
|
||||
value={shape.props.week_type}
|
||||
labelStyle={styles.property.label}
|
||||
valueStyle={styles.property.value}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
import { CCBaseShapeUtil } from '../CCBaseShapeUtil'
|
||||
import { CCBaseShape } from '../cc-types'
|
||||
import { NodeProperty } from './cc-graph-shared'
|
||||
import { ccGraphShapeProps, getDefaultCCAcademicYearNodeProps } from './cc-graph-props'
|
||||
import { CCAcademicYearNodeProps } from './cc-graph-types'
|
||||
import { getNodeStyles } from './cc-graph-styles'
|
||||
import { NODE_THEMES, NODE_TYPE_THEMES } from './cc-graph-styles'
|
||||
|
||||
export interface CCAcademicYearNodeShape extends CCBaseShape {
|
||||
type: 'cc-academic-year-node'
|
||||
props: CCAcademicYearNodeProps
|
||||
}
|
||||
|
||||
export class CCAcademicYearNodeShapeUtil extends CCBaseShapeUtil<CCAcademicYearNodeShape> {
|
||||
static type = 'cc-academic-year-node' as const
|
||||
static props = ccGraphShapeProps['cc-academic-year-node']
|
||||
|
||||
getDefaultProps(): CCAcademicYearNodeShape['props'] {
|
||||
const defaultProps = getDefaultCCAcademicYearNodeProps()
|
||||
const theme = NODE_THEMES[NODE_TYPE_THEMES[CCAcademicYearNodeShapeUtil.type]]
|
||||
return {
|
||||
...defaultProps,
|
||||
headerColor: theme.headerColor,
|
||||
}
|
||||
}
|
||||
|
||||
// Override to nullify the default node component
|
||||
DefaultComponent = () => null
|
||||
|
||||
renderContent = (shape: CCAcademicYearNodeShape) => {
|
||||
const styles = getNodeStyles(shape.type)
|
||||
|
||||
return (
|
||||
<div style={styles.container}>
|
||||
<NodeProperty
|
||||
label="Academic Year"
|
||||
value={shape.props.year}
|
||||
labelStyle={styles.property.label}
|
||||
valueStyle={styles.property.value}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
import { CCBaseShapeUtil } from '../CCBaseShapeUtil'
|
||||
import { CCBaseShape } from '../cc-types'
|
||||
import { NodeProperty, formatDate } from './cc-graph-shared'
|
||||
import { ccGraphShapeProps, getDefaultCCCalendarDayNodeProps } from './cc-graph-props'
|
||||
import { getNodeStyles } from './cc-graph-styles'
|
||||
import { NODE_THEMES, NODE_TYPE_THEMES } from './cc-graph-styles'
|
||||
import { CCCalendarDayNodeProps } from './cc-graph-types'
|
||||
|
||||
export interface CCCalendarDayNodeShape extends CCBaseShape {
|
||||
type: 'cc-calendar-day-node'
|
||||
props: CCCalendarDayNodeProps
|
||||
}
|
||||
|
||||
export class CCCalendarDayNodeShapeUtil extends CCBaseShapeUtil<CCCalendarDayNodeShape> {
|
||||
static type = 'cc-calendar-day-node' as const
|
||||
static props = ccGraphShapeProps['cc-calendar-day-node']
|
||||
|
||||
getDefaultProps(): CCCalendarDayNodeShape['props'] {
|
||||
const defaultProps = getDefaultCCCalendarDayNodeProps()
|
||||
const theme = NODE_THEMES[NODE_TYPE_THEMES[CCCalendarDayNodeShapeUtil.type]]
|
||||
return {
|
||||
...defaultProps,
|
||||
headerColor: theme.headerColor,
|
||||
}
|
||||
}
|
||||
|
||||
// Override to nullify the default node component
|
||||
DefaultComponent = () => null
|
||||
|
||||
renderContent = (shape: CCCalendarDayNodeShape) => {
|
||||
const styles = getNodeStyles(shape.type)
|
||||
|
||||
return (
|
||||
<div style={styles.container}>
|
||||
<NodeProperty
|
||||
label="Date"
|
||||
value={formatDate(shape.props.date)}
|
||||
labelStyle={styles.property.label}
|
||||
valueStyle={styles.property.value}
|
||||
/>
|
||||
<NodeProperty
|
||||
label="Day of Week"
|
||||
value={shape.props.day_of_week}
|
||||
labelStyle={styles.property.label}
|
||||
valueStyle={styles.property.value}
|
||||
/>
|
||||
<NodeProperty
|
||||
label="ISO Day"
|
||||
value={shape.props.iso_day}
|
||||
labelStyle={styles.property.label}
|
||||
valueStyle={styles.property.value}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
import { CCBaseShapeUtil } from '../CCBaseShapeUtil'
|
||||
import { CCBaseShape } from '../cc-types'
|
||||
import { NodeProperty } from './cc-graph-shared'
|
||||
import { ccGraphShapeProps, getDefaultCCCalendarMonthNodeProps } from './cc-graph-props'
|
||||
import { CCCalendarMonthNodeProps } from './cc-graph-types'
|
||||
import { getNodeStyles } from './cc-graph-styles'
|
||||
import { NODE_THEMES, NODE_TYPE_THEMES } from './cc-graph-styles'
|
||||
|
||||
export interface CCCalendarMonthNodeShape extends CCBaseShape {
|
||||
type: 'cc-calendar-month-node'
|
||||
props: CCCalendarMonthNodeProps
|
||||
}
|
||||
|
||||
export class CCCalendarMonthNodeShapeUtil extends CCBaseShapeUtil<CCCalendarMonthNodeShape> {
|
||||
static type = 'cc-calendar-month-node' as const
|
||||
static props = ccGraphShapeProps['cc-calendar-month-node']
|
||||
|
||||
getDefaultProps(): CCCalendarMonthNodeShape['props'] {
|
||||
const defaultProps = getDefaultCCCalendarMonthNodeProps()
|
||||
const theme = NODE_THEMES[NODE_TYPE_THEMES[CCCalendarMonthNodeShapeUtil.type]]
|
||||
return {
|
||||
...defaultProps,
|
||||
headerColor: theme.headerColor,
|
||||
}
|
||||
}
|
||||
|
||||
// Override to nullify the default node component
|
||||
DefaultComponent = () => null
|
||||
|
||||
renderContent = (shape: CCCalendarMonthNodeShape) => {
|
||||
const styles = getNodeStyles(shape.type)
|
||||
|
||||
return (
|
||||
<div style={styles.container}>
|
||||
<NodeProperty
|
||||
label="Year"
|
||||
value={shape.props.year}
|
||||
labelStyle={styles.property.label}
|
||||
valueStyle={styles.property.value}
|
||||
/>
|
||||
<NodeProperty
|
||||
label="Month"
|
||||
value={shape.props.month}
|
||||
labelStyle={styles.property.label}
|
||||
valueStyle={styles.property.value}
|
||||
/>
|
||||
<NodeProperty
|
||||
label="Month Name"
|
||||
value={shape.props.month_name}
|
||||
labelStyle={styles.property.label}
|
||||
valueStyle={styles.property.value}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
import { CCBaseShapeUtil } from '../CCBaseShapeUtil'
|
||||
import { CCBaseShape } from '../cc-types'
|
||||
import { NodeProperty, formatDate } from './cc-graph-shared'
|
||||
import { ccGraphShapeProps, getDefaultCCCalendarNodeProps } from './cc-graph-props'
|
||||
import { CCCalendarNodeProps } from './cc-graph-types'
|
||||
import { getNodeStyles } from './cc-graph-styles'
|
||||
import { NODE_THEMES, NODE_TYPE_THEMES } from './cc-graph-styles'
|
||||
|
||||
export interface CCCalendarNodeShape extends CCBaseShape {
|
||||
type: 'cc-calendar-node'
|
||||
props: CCCalendarNodeProps
|
||||
}
|
||||
|
||||
export class CCCalendarNodeShapeUtil extends CCBaseShapeUtil<CCCalendarNodeShape> {
|
||||
static type = 'cc-calendar-node' as const
|
||||
static props = ccGraphShapeProps['cc-calendar-node']
|
||||
|
||||
getDefaultProps(): CCCalendarNodeShape['props'] {
|
||||
const defaultProps = getDefaultCCCalendarNodeProps()
|
||||
const theme = NODE_THEMES[NODE_TYPE_THEMES[CCCalendarNodeShapeUtil.type]]
|
||||
return {
|
||||
...defaultProps,
|
||||
headerColor: theme.headerColor,
|
||||
}
|
||||
}
|
||||
|
||||
// Override to nullify the default node component
|
||||
DefaultComponent = () => null
|
||||
|
||||
renderContent = (shape: CCCalendarNodeShape) => {
|
||||
const styles = getNodeStyles(shape.type)
|
||||
|
||||
return (
|
||||
<div style={styles.container}>
|
||||
<NodeProperty
|
||||
label="Calendar Name"
|
||||
value={shape.props.calendar_name}
|
||||
labelStyle={styles.property.label}
|
||||
valueStyle={styles.property.value}
|
||||
/>
|
||||
<NodeProperty
|
||||
label="Calendar Type"
|
||||
value={shape.props.calendar_type}
|
||||
labelStyle={styles.property.label}
|
||||
valueStyle={styles.property.value}
|
||||
/>
|
||||
<NodeProperty
|
||||
label="Start Date"
|
||||
value={formatDate(shape.props.start_date)}
|
||||
labelStyle={styles.property.label}
|
||||
valueStyle={styles.property.value}
|
||||
/>
|
||||
<NodeProperty
|
||||
label="End Date"
|
||||
value={formatDate(shape.props.end_date)}
|
||||
labelStyle={styles.property.label}
|
||||
valueStyle={styles.property.value}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
import { CCBaseShapeUtil } from '../CCBaseShapeUtil'
|
||||
import { CCBaseShape } from '../cc-types'
|
||||
import { NodeProperty } from './cc-graph-shared'
|
||||
import { ccGraphShapeProps, getDefaultCCCalendarTimeChunkNodeProps } from './cc-graph-props'
|
||||
import { getNodeStyles } from './cc-graph-styles'
|
||||
import { NODE_THEMES, NODE_TYPE_THEMES } from './cc-graph-styles'
|
||||
import { CCCalendarTimeChunkNodeProps } from './cc-graph-types'
|
||||
|
||||
export interface CCCalendarTimeChunkNodeShape extends CCBaseShape {
|
||||
type: 'cc-calendar-time-chunk-node'
|
||||
props: CCCalendarTimeChunkNodeProps
|
||||
}
|
||||
|
||||
export class CCCalendarTimeChunkNodeShapeUtil extends CCBaseShapeUtil<CCCalendarTimeChunkNodeShape> {
|
||||
static type = 'cc-calendar-time-chunk-node' as const
|
||||
static props = ccGraphShapeProps['cc-calendar-time-chunk-node']
|
||||
|
||||
getDefaultProps(): CCCalendarTimeChunkNodeShape['props'] {
|
||||
const defaultProps = getDefaultCCCalendarTimeChunkNodeProps()
|
||||
const theme = NODE_THEMES[NODE_TYPE_THEMES[CCCalendarTimeChunkNodeShapeUtil.type]]
|
||||
return {
|
||||
...defaultProps,
|
||||
headerColor: theme.headerColor,
|
||||
}
|
||||
}
|
||||
|
||||
// Override to nullify the default node component
|
||||
DefaultComponent = () => null
|
||||
|
||||
renderContent = (shape: CCCalendarTimeChunkNodeShape) => {
|
||||
const styles = getNodeStyles(shape.type)
|
||||
|
||||
return (
|
||||
<div style={styles.container}>
|
||||
<NodeProperty
|
||||
label="Start Time"
|
||||
value={shape.props.start_time}
|
||||
labelStyle={styles.property.label}
|
||||
valueStyle={styles.property.value}
|
||||
/>
|
||||
<NodeProperty
|
||||
label="End Time"
|
||||
value={shape.props.end_time}
|
||||
labelStyle={styles.property.label}
|
||||
valueStyle={styles.property.value}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
import { CCBaseShapeUtil } from '../CCBaseShapeUtil'
|
||||
import { CCBaseShape } from '../cc-types'
|
||||
import { NodeProperty, formatDate, DateValue } from './cc-graph-shared'
|
||||
import { ccGraphShapeProps, getDefaultCCCalendarWeekNodeProps } from './cc-graph-props'
|
||||
import { getNodeStyles } from './cc-graph-styles'
|
||||
import { NODE_THEMES, NODE_TYPE_THEMES } from './cc-graph-styles'
|
||||
import { CCCalendarWeekNodeProps } from './cc-graph-types'
|
||||
|
||||
export interface CCCalendarWeekNodeShape extends CCBaseShape {
|
||||
type: 'cc-calendar-week-node'
|
||||
props: CCCalendarWeekNodeProps
|
||||
}
|
||||
|
||||
export class CCCalendarWeekNodeShapeUtil extends CCBaseShapeUtil<CCCalendarWeekNodeShape> {
|
||||
static type = 'cc-calendar-week-node' as const
|
||||
static props = ccGraphShapeProps['cc-calendar-week-node']
|
||||
|
||||
getDefaultProps(): CCCalendarWeekNodeShape['props'] {
|
||||
const defaultProps = getDefaultCCCalendarWeekNodeProps()
|
||||
const theme = NODE_THEMES[NODE_TYPE_THEMES[CCCalendarWeekNodeShapeUtil.type]]
|
||||
return {
|
||||
...defaultProps,
|
||||
headerColor: theme.headerColor,
|
||||
}
|
||||
}
|
||||
|
||||
// Override to nullify the default node component
|
||||
DefaultComponent = () => null
|
||||
|
||||
renderContent = (shape: CCCalendarWeekNodeShape) => {
|
||||
const styles = getNodeStyles(shape.type)
|
||||
|
||||
return (
|
||||
<div style={styles.container}>
|
||||
<NodeProperty
|
||||
label="Start Date"
|
||||
value={formatDate(shape.props.start_date as DateValue)}
|
||||
labelStyle={styles.property.label}
|
||||
valueStyle={styles.property.value}
|
||||
/>
|
||||
<NodeProperty
|
||||
label="Week Number"
|
||||
value={shape.props.week_number}
|
||||
labelStyle={styles.property.label}
|
||||
valueStyle={styles.property.value}
|
||||
/>
|
||||
<NodeProperty
|
||||
label="ISO Week"
|
||||
value={shape.props.iso_week}
|
||||
labelStyle={styles.property.label}
|
||||
valueStyle={styles.property.value}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
import { CCBaseShapeUtil } from '../CCBaseShapeUtil'
|
||||
import { CCBaseShape } from '../cc-types'
|
||||
import { NodeProperty } from './cc-graph-shared'
|
||||
import { ccGraphShapeProps, getDefaultCCCalendarYearNodeProps } from './cc-graph-props'
|
||||
import { getNodeStyles } from './cc-graph-styles'
|
||||
import { NODE_THEMES, NODE_TYPE_THEMES } from './cc-graph-styles'
|
||||
import { CCCalendarYearNodeProps } from './cc-graph-types'
|
||||
|
||||
export interface CCCalendarYearNodeShape extends CCBaseShape {
|
||||
type: 'cc-calendar-year-node'
|
||||
props: CCCalendarYearNodeProps
|
||||
}
|
||||
|
||||
export class CCCalendarYearNodeShapeUtil extends CCBaseShapeUtil<CCCalendarYearNodeShape> {
|
||||
static type = 'cc-calendar-year-node' as const
|
||||
static props = ccGraphShapeProps['cc-calendar-year-node']
|
||||
|
||||
getDefaultProps(): CCCalendarYearNodeShape['props'] {
|
||||
const defaultProps = getDefaultCCCalendarYearNodeProps()
|
||||
const theme = NODE_THEMES[NODE_TYPE_THEMES[CCCalendarYearNodeShapeUtil.type]]
|
||||
return {
|
||||
...defaultProps,
|
||||
headerColor: theme.headerColor,
|
||||
}
|
||||
}
|
||||
|
||||
// Override to nullify the default node component
|
||||
DefaultComponent = () => null
|
||||
|
||||
renderContent = (shape: CCCalendarYearNodeShape) => {
|
||||
const styles = getNodeStyles(shape.type)
|
||||
|
||||
return (
|
||||
<div style={styles.container}>
|
||||
<NodeProperty
|
||||
label="Year"
|
||||
value={shape.props.year}
|
||||
labelStyle={styles.property.label}
|
||||
valueStyle={styles.property.value}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
import { CCBaseShapeUtil } from '../CCBaseShapeUtil'
|
||||
import { CCBaseShape } from '../cc-types'
|
||||
import { NodeProperty } from './cc-graph-shared'
|
||||
import { ccGraphShapeProps, getDefaultCCCurriculumStructureNodeProps } from './cc-graph-props'
|
||||
import { getNodeStyles } from './cc-graph-styles'
|
||||
import { NODE_THEMES, NODE_TYPE_THEMES } from './cc-graph-styles'
|
||||
import { CCCurriculumStructureNodeProps } from './cc-graph-types'
|
||||
|
||||
export interface CCCurriculumStructureNodeShape extends CCBaseShape {
|
||||
type: 'cc-curriculum-structure-node'
|
||||
props: CCCurriculumStructureNodeProps
|
||||
}
|
||||
|
||||
export class CCCurriculumStructureNodeShapeUtil extends CCBaseShapeUtil<CCCurriculumStructureNodeShape> {
|
||||
static type = 'cc-curriculum-structure-node' as const
|
||||
static props = ccGraphShapeProps['cc-curriculum-structure-node']
|
||||
|
||||
getDefaultProps(): CCCurriculumStructureNodeShape['props'] {
|
||||
const defaultProps = getDefaultCCCurriculumStructureNodeProps()
|
||||
const theme = NODE_THEMES[NODE_TYPE_THEMES[CCCurriculumStructureNodeShapeUtil.type]]
|
||||
return {
|
||||
...defaultProps,
|
||||
headerColor: theme.headerColor,
|
||||
}
|
||||
}
|
||||
|
||||
// Override to nullify the default node component
|
||||
DefaultComponent = () => null
|
||||
|
||||
renderContent = (shape: CCCurriculumStructureNodeShape) => {
|
||||
const styles = getNodeStyles(shape.type)
|
||||
|
||||
return (
|
||||
<div style={styles.container}>
|
||||
<NodeProperty
|
||||
label="Structure Type"
|
||||
value="Curriculum"
|
||||
labelStyle={styles.property.label}
|
||||
valueStyle={styles.property.value}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
import { CCBaseShapeUtil } from '../CCBaseShapeUtil'
|
||||
import { CCBaseShape } from '../cc-types'
|
||||
import { NodeProperty } from './cc-graph-shared'
|
||||
import { ccGraphShapeProps, getDefaultCCDepartmentNodeProps } from './cc-graph-props'
|
||||
import { getNodeStyles } from './cc-graph-styles'
|
||||
import { NODE_THEMES, NODE_TYPE_THEMES } from './cc-graph-styles'
|
||||
import { CCDepartmentNodeProps } from './cc-graph-types'
|
||||
|
||||
export interface CCDepartmentNodeShape extends CCBaseShape {
|
||||
type: 'cc-department-node'
|
||||
props: CCDepartmentNodeProps
|
||||
}
|
||||
|
||||
export class CCDepartmentNodeShapeUtil extends CCBaseShapeUtil<CCDepartmentNodeShape> {
|
||||
static type = 'cc-department-node' as const
|
||||
static props = ccGraphShapeProps['cc-department-node']
|
||||
|
||||
getDefaultProps(): CCDepartmentNodeShape['props'] {
|
||||
const defaultProps = getDefaultCCDepartmentNodeProps()
|
||||
const theme = NODE_THEMES[NODE_TYPE_THEMES[CCDepartmentNodeShapeUtil.type]]
|
||||
return {
|
||||
...defaultProps,
|
||||
headerColor: theme.headerColor,
|
||||
}
|
||||
}
|
||||
|
||||
// Override to nullify the default node component
|
||||
DefaultComponent = () => null
|
||||
|
||||
renderContent = (shape: CCDepartmentNodeShape) => {
|
||||
const styles = getNodeStyles(shape.type)
|
||||
|
||||
return (
|
||||
<div style={styles.container}>
|
||||
<NodeProperty
|
||||
label="Department Name"
|
||||
value={shape.props.department_name}
|
||||
labelStyle={styles.property.label}
|
||||
valueStyle={styles.property.value}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
import { CCBaseShapeUtil } from '../CCBaseShapeUtil'
|
||||
import { CCBaseShape } from '../cc-types'
|
||||
import { NodeProperty } from './cc-graph-shared'
|
||||
import { ccGraphShapeProps, getDefaultCCDepartmentStructureNodeProps } from './cc-graph-props'
|
||||
import { getNodeStyles } from './cc-graph-styles'
|
||||
import { NODE_THEMES, NODE_TYPE_THEMES } from './cc-graph-styles'
|
||||
import { CCDepartmentStructureNodeProps } from './cc-graph-types'
|
||||
|
||||
export interface CCDepartmentStructureNodeShape extends CCBaseShape {
|
||||
type: 'cc-department-structure-node'
|
||||
props: CCDepartmentStructureNodeProps
|
||||
}
|
||||
|
||||
export class CCDepartmentStructureNodeShapeUtil extends CCBaseShapeUtil<CCDepartmentStructureNodeShape> {
|
||||
static type = 'cc-department-structure-node' as const
|
||||
static props = ccGraphShapeProps['cc-department-structure-node']
|
||||
|
||||
getDefaultProps(): CCDepartmentStructureNodeShape['props'] {
|
||||
const defaultProps = getDefaultCCDepartmentStructureNodeProps()
|
||||
const theme = NODE_THEMES[NODE_TYPE_THEMES[CCDepartmentStructureNodeShapeUtil.type]]
|
||||
return {
|
||||
...defaultProps,
|
||||
headerColor: theme.headerColor,
|
||||
}
|
||||
}
|
||||
|
||||
// Override to nullify the default node component
|
||||
DefaultComponent = () => null
|
||||
|
||||
renderContent = (shape: CCDepartmentStructureNodeShape) => {
|
||||
const styles = getNodeStyles(shape.type)
|
||||
|
||||
return (
|
||||
<div style={styles.container}>
|
||||
<NodeProperty
|
||||
label="Structure Type"
|
||||
value={shape.props.department_structure_type || "Department"}
|
||||
labelStyle={styles.property.label}
|
||||
valueStyle={styles.property.value}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
import { CCBaseShapeUtil } from '../CCBaseShapeUtil'
|
||||
import { CCBaseShape } from '../cc-types'
|
||||
import { NodeProperty } from './cc-graph-shared'
|
||||
import { ccGraphShapeProps, getDefaultCCKeyStageNodeProps } from './cc-graph-props'
|
||||
import { getNodeStyles } from './cc-graph-styles'
|
||||
import { NODE_THEMES, NODE_TYPE_THEMES } from './cc-graph-styles'
|
||||
import { CCKeyStageNodeProps } from './cc-graph-types'
|
||||
|
||||
export interface CCKeyStageNodeShape extends CCBaseShape {
|
||||
type: 'cc-key-stage-node'
|
||||
props: CCKeyStageNodeProps
|
||||
}
|
||||
|
||||
export class CCKeyStageNodeShapeUtil extends CCBaseShapeUtil<CCKeyStageNodeShape> {
|
||||
static type = 'cc-key-stage-node' as const
|
||||
static props = ccGraphShapeProps['cc-key-stage-node']
|
||||
|
||||
getDefaultProps(): CCKeyStageNodeShape['props'] {
|
||||
const defaultProps = getDefaultCCKeyStageNodeProps()
|
||||
const theme = NODE_THEMES[NODE_TYPE_THEMES[CCKeyStageNodeShapeUtil.type]]
|
||||
return {
|
||||
...defaultProps,
|
||||
headerColor: theme.headerColor,
|
||||
}
|
||||
}
|
||||
|
||||
// Override to nullify the default node component
|
||||
DefaultComponent = () => null
|
||||
|
||||
renderContent = (shape: CCKeyStageNodeShape) => {
|
||||
const styles = getNodeStyles(shape.type)
|
||||
|
||||
return (
|
||||
<div style={styles.container}>
|
||||
<NodeProperty
|
||||
label="Key Stage"
|
||||
value={shape.props.key_stage}
|
||||
labelStyle={styles.property.label}
|
||||
valueStyle={styles.property.value}
|
||||
/>
|
||||
<NodeProperty
|
||||
label="Name"
|
||||
value={shape.props.key_stage_name}
|
||||
labelStyle={styles.property.label}
|
||||
valueStyle={styles.property.value}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
import { CCBaseShapeUtil } from '../CCBaseShapeUtil'
|
||||
import { CCBaseShape } from '../cc-types'
|
||||
import { NodeProperty } from './cc-graph-shared'
|
||||
import { ccGraphShapeProps, getDefaultCCKeyStageSyllabusNodeProps } from './cc-graph-props'
|
||||
import { getNodeStyles } from './cc-graph-styles'
|
||||
import { NODE_THEMES, NODE_TYPE_THEMES } from './cc-graph-styles'
|
||||
import { CCKeyStageSyllabusNodeProps } from './cc-graph-types'
|
||||
|
||||
export interface CCKeyStageSyllabusNodeShape extends CCBaseShape {
|
||||
type: 'cc-key-stage-syllabus-node'
|
||||
props: CCKeyStageSyllabusNodeProps
|
||||
}
|
||||
|
||||
export class CCKeyStageSyllabusNodeShapeUtil extends CCBaseShapeUtil<CCKeyStageSyllabusNodeShape> {
|
||||
static type = 'cc-key-stage-syllabus-node' as const
|
||||
static props = ccGraphShapeProps['cc-key-stage-syllabus-node']
|
||||
|
||||
getDefaultProps(): CCKeyStageSyllabusNodeShape['props'] {
|
||||
const defaultProps = getDefaultCCKeyStageSyllabusNodeProps()
|
||||
const theme = NODE_THEMES[NODE_TYPE_THEMES[CCKeyStageSyllabusNodeShapeUtil.type]]
|
||||
return {
|
||||
...defaultProps,
|
||||
headerColor: theme.headerColor,
|
||||
}
|
||||
}
|
||||
|
||||
// Override to nullify the default node component
|
||||
DefaultComponent = () => null
|
||||
|
||||
renderContent = (shape: CCKeyStageSyllabusNodeShape) => {
|
||||
const styles = getNodeStyles(shape.type)
|
||||
|
||||
return (
|
||||
<div style={styles.container}>
|
||||
<NodeProperty
|
||||
label="Syllabus Name"
|
||||
value={shape.props.ks_syllabus_name}
|
||||
labelStyle={styles.property.label}
|
||||
valueStyle={styles.property.value}
|
||||
/>
|
||||
<NodeProperty
|
||||
label="Key Stage"
|
||||
value={shape.props.ks_syllabus_key_stage}
|
||||
labelStyle={styles.property.label}
|
||||
valueStyle={styles.property.value}
|
||||
/>
|
||||
<NodeProperty
|
||||
label="Subject"
|
||||
value={shape.props.ks_syllabus_subject}
|
||||
labelStyle={styles.property.label}
|
||||
valueStyle={styles.property.value}
|
||||
/>
|
||||
<NodeProperty
|
||||
label="Subject Code"
|
||||
value={shape.props.ks_syllabus_subject_code}
|
||||
labelStyle={styles.property.label}
|
||||
valueStyle={styles.property.value}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
import { CCBaseShapeUtil } from '../CCBaseShapeUtil'
|
||||
import { CCBaseShape } from '../cc-types'
|
||||
import { NodeProperty } from './cc-graph-shared'
|
||||
import { ccGraphShapeProps, getDefaultCCLearningStatementNodeProps } from './cc-graph-props'
|
||||
import { getNodeStyles } from './cc-graph-styles'
|
||||
import { NODE_THEMES, NODE_TYPE_THEMES } from './cc-graph-styles'
|
||||
import { CCLearningStatementNodeProps } from './cc-graph-types'
|
||||
|
||||
export interface CCLearningStatementNodeShape extends CCBaseShape {
|
||||
type: 'cc-learning-statement-node'
|
||||
props: CCLearningStatementNodeProps
|
||||
}
|
||||
|
||||
export class CCLearningStatementNodeShapeUtil extends CCBaseShapeUtil<CCLearningStatementNodeShape> {
|
||||
static type = 'cc-learning-statement-node' as const
|
||||
static props = ccGraphShapeProps['cc-learning-statement-node']
|
||||
|
||||
getDefaultProps(): CCLearningStatementNodeShape['props'] {
|
||||
const defaultProps = getDefaultCCLearningStatementNodeProps()
|
||||
const theme = NODE_THEMES[NODE_TYPE_THEMES[CCLearningStatementNodeShapeUtil.type]]
|
||||
return {
|
||||
...defaultProps,
|
||||
headerColor: theme.headerColor,
|
||||
}
|
||||
}
|
||||
|
||||
// Override to nullify the default node component
|
||||
DefaultComponent = () => null
|
||||
|
||||
renderContent = (shape: CCLearningStatementNodeShape) => {
|
||||
const styles = getNodeStyles(shape.type)
|
||||
|
||||
return (
|
||||
<div style={styles.container}>
|
||||
<NodeProperty
|
||||
label="Learning Statement"
|
||||
value={shape.props.lesson_learning_statement}
|
||||
labelStyle={styles.property.label}
|
||||
valueStyle={styles.property.value}
|
||||
/>
|
||||
<NodeProperty
|
||||
label="Statement Type"
|
||||
value={shape.props.lesson_learning_statement_type}
|
||||
labelStyle={styles.property.label}
|
||||
valueStyle={styles.property.value}
|
||||
/>
|
||||
<NodeProperty
|
||||
label="Statement ID"
|
||||
value={shape.props.lesson_learning_statement_id}
|
||||
labelStyle={styles.property.label}
|
||||
valueStyle={styles.property.value}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
import { CCBaseShapeUtil } from '../CCBaseShapeUtil'
|
||||
import { CCBaseShape } from '../cc-types'
|
||||
import { NodeProperty } from './cc-graph-shared'
|
||||
import { ccGraphShapeProps, getDefaultCCPastoralStructureNodeProps } from './cc-graph-props'
|
||||
import { getNodeStyles } from './cc-graph-styles'
|
||||
import { NODE_THEMES, NODE_TYPE_THEMES } from './cc-graph-styles'
|
||||
import { CCPastoralStructureNodeProps } from './cc-graph-types'
|
||||
|
||||
|
||||
export interface CCPastoralStructureNodeShape extends CCBaseShape {
|
||||
type: 'cc-pastoral-structure-node'
|
||||
props: CCPastoralStructureNodeProps
|
||||
}
|
||||
|
||||
export class CCPastoralStructureNodeShapeUtil extends CCBaseShapeUtil<CCPastoralStructureNodeShape> {
|
||||
static type = 'cc-pastoral-structure-node' as const
|
||||
static props = ccGraphShapeProps['cc-pastoral-structure-node']
|
||||
|
||||
getDefaultProps(): CCPastoralStructureNodeShape['props'] {
|
||||
const defaultProps = getDefaultCCPastoralStructureNodeProps()
|
||||
const theme = NODE_THEMES[NODE_TYPE_THEMES[CCPastoralStructureNodeShapeUtil.type]]
|
||||
return {
|
||||
...defaultProps,
|
||||
headerColor: theme.headerColor,
|
||||
}
|
||||
}
|
||||
|
||||
// Override to nullify the default node component
|
||||
DefaultComponent = () => null
|
||||
|
||||
renderContent = (shape: CCPastoralStructureNodeShape) => {
|
||||
const styles = getNodeStyles(shape.type)
|
||||
|
||||
return (
|
||||
<div style={styles.container}>
|
||||
<NodeProperty
|
||||
label="Structure Type"
|
||||
value="Pastoral"
|
||||
labelStyle={styles.property.label}
|
||||
valueStyle={styles.property.value}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,140 @@
|
||||
import { CCBaseShapeUtil } from '../CCBaseShapeUtil'
|
||||
import { CCBaseShape } from '../cc-types'
|
||||
import { NodeProperty } from './cc-graph-shared'
|
||||
import { ccGraphShapeProps, getDefaultCCPlannedLessonNodeProps } from './cc-graph-props'
|
||||
import { getNodeStyles } from './cc-graph-styles'
|
||||
import { NODE_THEMES, NODE_TYPE_THEMES } from './cc-graph-styles'
|
||||
import { CCPlannedLessonNodeProps } from './cc-graph-types'
|
||||
|
||||
export interface CCPlannedLessonNodeShape extends CCBaseShape {
|
||||
type: 'cc-planned-lesson-node'
|
||||
props: CCPlannedLessonNodeProps
|
||||
}
|
||||
|
||||
export class CCPlannedLessonNodeShapeUtil extends CCBaseShapeUtil<CCPlannedLessonNodeShape> {
|
||||
static type = 'cc-planned-lesson-node' as const
|
||||
static props = ccGraphShapeProps['cc-planned-lesson-node']
|
||||
|
||||
getDefaultProps(): CCPlannedLessonNodeShape['props'] {
|
||||
const defaultProps = getDefaultCCPlannedLessonNodeProps()
|
||||
const theme = NODE_THEMES[NODE_TYPE_THEMES[CCPlannedLessonNodeShapeUtil.type]]
|
||||
return {
|
||||
...defaultProps,
|
||||
headerColor: theme.headerColor,
|
||||
}
|
||||
}
|
||||
|
||||
// Override to nullify the default node component
|
||||
DefaultComponent = () => null
|
||||
|
||||
renderContent = (shape: CCPlannedLessonNodeShape) => {
|
||||
const styles = getNodeStyles(shape.type)
|
||||
|
||||
return (
|
||||
<div style={styles.container}>
|
||||
<NodeProperty
|
||||
label="Subject Class"
|
||||
value={shape.props.subject_class}
|
||||
labelStyle={styles.property.label}
|
||||
valueStyle={styles.property.value}
|
||||
/>
|
||||
<NodeProperty
|
||||
label="Date"
|
||||
value={shape.props.date}
|
||||
labelStyle={styles.property.label}
|
||||
valueStyle={styles.property.value}
|
||||
/>
|
||||
<NodeProperty
|
||||
label="Start Time"
|
||||
value={shape.props.start_time}
|
||||
labelStyle={styles.property.label}
|
||||
valueStyle={styles.property.value}
|
||||
/>
|
||||
<NodeProperty
|
||||
label="End Time"
|
||||
value={shape.props.end_time}
|
||||
labelStyle={styles.property.label}
|
||||
valueStyle={styles.property.value}
|
||||
/>
|
||||
<NodeProperty
|
||||
label="Period Code"
|
||||
value={shape.props.period_code}
|
||||
labelStyle={styles.property.label}
|
||||
valueStyle={styles.property.value}
|
||||
/>
|
||||
<NodeProperty
|
||||
label="Year Group"
|
||||
value={shape.props.year_group}
|
||||
labelStyle={styles.property.label}
|
||||
valueStyle={styles.property.value}
|
||||
/>
|
||||
<NodeProperty
|
||||
label="Subject"
|
||||
value={shape.props.subject}
|
||||
labelStyle={styles.property.label}
|
||||
valueStyle={styles.property.value}
|
||||
/>
|
||||
<NodeProperty
|
||||
label="Teacher Code"
|
||||
value={shape.props.teacher_code}
|
||||
labelStyle={styles.property.label}
|
||||
valueStyle={styles.property.value}
|
||||
/>
|
||||
<NodeProperty
|
||||
label="Planning Status"
|
||||
value={shape.props.planning_status}
|
||||
labelStyle={styles.property.label}
|
||||
valueStyle={styles.property.value}
|
||||
/>
|
||||
<NodeProperty
|
||||
label="Topic Code"
|
||||
value={shape.props.topic_code}
|
||||
labelStyle={styles.property.label}
|
||||
valueStyle={styles.property.value}
|
||||
/>
|
||||
<NodeProperty
|
||||
label="Topic Name"
|
||||
value={shape.props.topic_name}
|
||||
labelStyle={styles.property.label}
|
||||
valueStyle={styles.property.value}
|
||||
/>
|
||||
<NodeProperty
|
||||
label="Lesson Code"
|
||||
value={shape.props.lesson_code}
|
||||
labelStyle={styles.property.label}
|
||||
valueStyle={styles.property.value}
|
||||
/>
|
||||
<NodeProperty
|
||||
label="Lesson Name"
|
||||
value={shape.props.lesson_name}
|
||||
labelStyle={styles.property.label}
|
||||
valueStyle={styles.property.value}
|
||||
/>
|
||||
<NodeProperty
|
||||
label="Learning Statement Codes"
|
||||
value={shape.props.learning_statement_codes}
|
||||
labelStyle={styles.property.label}
|
||||
valueStyle={styles.property.value}
|
||||
/>
|
||||
<NodeProperty
|
||||
label="Learning Statements"
|
||||
value={shape.props.learning_statements}
|
||||
labelStyle={styles.property.label}
|
||||
valueStyle={styles.property.value}
|
||||
/>
|
||||
<NodeProperty
|
||||
label="Learning Resource Codes"
|
||||
value={shape.props.learning_resource_codes}
|
||||
labelStyle={styles.property.label}
|
||||
valueStyle={styles.property.value}
|
||||
/>
|
||||
<NodeProperty
|
||||
label="Learning Resources"
|
||||
value={shape.props.learning_resources}
|
||||
labelStyle={styles.property.label}
|
||||
valueStyle={styles.property.value}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
import { CCBaseShapeUtil } from '../CCBaseShapeUtil'
|
||||
import { CCBaseShape } from '../cc-types'
|
||||
import { NodeProperty } from './cc-graph-shared'
|
||||
import { ccGraphShapeProps, getDefaultCCRegistrationPeriodNodeProps } from './cc-graph-props'
|
||||
import { getNodeStyles } from './cc-graph-styles'
|
||||
import { NODE_THEMES, NODE_TYPE_THEMES } from './cc-graph-styles'
|
||||
import { CCRegistrationPeriodNodeProps } from './cc-graph-types'
|
||||
|
||||
export interface CCRegistrationPeriodNodeShape extends CCBaseShape {
|
||||
type: 'cc-registration-period-node'
|
||||
props: CCRegistrationPeriodNodeProps
|
||||
}
|
||||
|
||||
export class CCRegistrationPeriodNodeShapeUtil extends CCBaseShapeUtil<CCRegistrationPeriodNodeShape> {
|
||||
static type = 'cc-registration-period-node' as const
|
||||
static props = ccGraphShapeProps['cc-registration-period-node']
|
||||
|
||||
getDefaultProps(): CCRegistrationPeriodNodeShape['props'] {
|
||||
const defaultProps = getDefaultCCRegistrationPeriodNodeProps()
|
||||
const theme = NODE_THEMES[NODE_TYPE_THEMES[CCRegistrationPeriodNodeShapeUtil.type]]
|
||||
return {
|
||||
...defaultProps,
|
||||
headerColor: theme.headerColor,
|
||||
}
|
||||
}
|
||||
|
||||
// Override to nullify the default node component
|
||||
DefaultComponent = () => null
|
||||
|
||||
renderContent = (shape: CCRegistrationPeriodNodeShape) => {
|
||||
const styles = getNodeStyles(shape.type)
|
||||
|
||||
return (
|
||||
<div style={styles.container}>
|
||||
<NodeProperty
|
||||
label="Name"
|
||||
value={shape.props.name}
|
||||
labelStyle={styles.property.label}
|
||||
valueStyle={styles.property.value}
|
||||
/>
|
||||
<NodeProperty
|
||||
label="Date"
|
||||
value={shape.props.date}
|
||||
labelStyle={styles.property.label}
|
||||
valueStyle={styles.property.value}
|
||||
/>
|
||||
<NodeProperty
|
||||
label="Start Time"
|
||||
value={shape.props.start_time}
|
||||
labelStyle={styles.property.label}
|
||||
valueStyle={styles.property.value}
|
||||
/>
|
||||
<NodeProperty
|
||||
label="End Time"
|
||||
value={shape.props.end_time}
|
||||
labelStyle={styles.property.label}
|
||||
valueStyle={styles.property.value}
|
||||
/>
|
||||
<NodeProperty
|
||||
label="Period Code"
|
||||
value={shape.props.period_code}
|
||||
labelStyle={styles.property.label}
|
||||
valueStyle={styles.property.value}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
import { CCBaseShapeUtil } from '../CCBaseShapeUtil'
|
||||
import { CCBaseShape } from '../cc-types'
|
||||
import { NodeProperty } from './cc-graph-shared'
|
||||
import { ccGraphShapeProps, getDefaultCCRoomNodeProps } from './cc-graph-props'
|
||||
import { getNodeStyles } from './cc-graph-styles'
|
||||
import { NODE_THEMES, NODE_TYPE_THEMES } from './cc-graph-styles'
|
||||
import { CCRoomNodeProps } from './cc-graph-types'
|
||||
|
||||
export interface CCRoomNodeShape extends CCBaseShape {
|
||||
type: 'cc-room-node'
|
||||
props: CCRoomNodeProps
|
||||
}
|
||||
|
||||
export class CCRoomNodeShapeUtil extends CCBaseShapeUtil<CCRoomNodeShape> {
|
||||
static type = 'cc-room-node' as const
|
||||
static props = ccGraphShapeProps['cc-room-node']
|
||||
|
||||
getDefaultProps(): CCRoomNodeShape['props'] {
|
||||
const defaultProps = getDefaultCCRoomNodeProps()
|
||||
const theme = NODE_THEMES[NODE_TYPE_THEMES[CCRoomNodeShapeUtil.type]]
|
||||
return {
|
||||
...defaultProps,
|
||||
headerColor: theme.headerColor,
|
||||
}
|
||||
}
|
||||
|
||||
// Override to nullify the default node component
|
||||
DefaultComponent = () => null
|
||||
|
||||
renderContent = (shape: CCRoomNodeShape) => {
|
||||
const styles = getNodeStyles(shape.type)
|
||||
|
||||
return (
|
||||
<div style={styles.container}>
|
||||
<NodeProperty
|
||||
label="Room Name"
|
||||
value={shape.props.room_name}
|
||||
labelStyle={styles.property.label}
|
||||
valueStyle={styles.property.value}
|
||||
/>
|
||||
<NodeProperty
|
||||
label="Room Code"
|
||||
value={shape.props.room_code}
|
||||
labelStyle={styles.property.label}
|
||||
valueStyle={styles.property.value}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
import { CCBaseShapeUtil } from '../CCBaseShapeUtil';
|
||||
import { CCBaseShape } from '../cc-types';
|
||||
import { NodeProperty } from './cc-graph-shared';
|
||||
import { ccGraphShapeProps, getDefaultCCSchoolNodeProps } from './cc-graph-props';
|
||||
import { getNodeStyles } from './cc-graph-styles';
|
||||
import { NODE_THEMES, NODE_TYPE_THEMES } from './cc-graph-styles';
|
||||
import { CCSchoolNodeProps } from './cc-graph-types';
|
||||
|
||||
export interface CCSchoolNodeShape extends CCBaseShape {
|
||||
type: 'cc-school-node';
|
||||
props: CCSchoolNodeProps;
|
||||
}
|
||||
|
||||
export class CCSchoolNodeShapeUtil extends CCBaseShapeUtil<CCSchoolNodeShape> {
|
||||
static type = 'cc-school-node' as const;
|
||||
static props = ccGraphShapeProps['cc-school-node'];
|
||||
|
||||
getDefaultProps(): CCSchoolNodeShape['props'] {
|
||||
const defaultProps = getDefaultCCSchoolNodeProps();
|
||||
const theme = NODE_THEMES[NODE_TYPE_THEMES[CCSchoolNodeShapeUtil.type]];
|
||||
return {
|
||||
...defaultProps,
|
||||
headerColor: theme.headerColor,
|
||||
};
|
||||
}
|
||||
|
||||
DefaultComponent = () => null;
|
||||
|
||||
renderContent = (shape: CCSchoolNodeShape) => {
|
||||
const styles = getNodeStyles(shape.type);
|
||||
return (
|
||||
<div style={styles.container}>
|
||||
<NodeProperty
|
||||
label="School Name"
|
||||
value={shape.props.school_name}
|
||||
labelStyle={styles.property.label}
|
||||
valueStyle={styles.property.value}
|
||||
/>
|
||||
<NodeProperty
|
||||
label="School Website"
|
||||
value={shape.props.school_website}
|
||||
labelStyle={styles.property.label}
|
||||
valueStyle={styles.property.value}
|
||||
/>
|
||||
<NodeProperty
|
||||
label="School UUID"
|
||||
value={shape.props.school_uuid}
|
||||
labelStyle={styles.property.label}
|
||||
valueStyle={styles.property.value}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
import { CCBaseShapeUtil } from '../CCBaseShapeUtil'
|
||||
import { CCBaseShape } from '../cc-types'
|
||||
import { NodeProperty } from './cc-graph-shared'
|
||||
import { ccGraphShapeProps, getDefaultCCSchoolTimetableNodeProps } from './cc-graph-props'
|
||||
import { getNodeStyles } from './cc-graph-styles'
|
||||
import { NODE_THEMES, NODE_TYPE_THEMES } from './cc-graph-styles'
|
||||
import { CCSchoolTimetableNodeProps } from './cc-graph-types'
|
||||
|
||||
export interface CCSchoolTimetableNodeShape extends CCBaseShape {
|
||||
type: 'cc-school-timetable-node'
|
||||
props: CCSchoolTimetableNodeProps
|
||||
}
|
||||
|
||||
export class CCSchoolTimetableNodeShapeUtil extends CCBaseShapeUtil<CCSchoolTimetableNodeShape> {
|
||||
static type = 'cc-school-timetable-node' as const
|
||||
static props = ccGraphShapeProps['cc-school-timetable-node']
|
||||
|
||||
getDefaultProps(): CCSchoolTimetableNodeShape['props'] {
|
||||
const defaultProps = getDefaultCCSchoolTimetableNodeProps()
|
||||
const theme = NODE_THEMES[NODE_TYPE_THEMES[CCSchoolTimetableNodeShapeUtil.type]]
|
||||
return {
|
||||
...defaultProps,
|
||||
headerColor: theme.headerColor,
|
||||
}
|
||||
}
|
||||
|
||||
// Override to nullify the default node component
|
||||
DefaultComponent = () => null
|
||||
|
||||
renderContent = (shape: CCSchoolTimetableNodeShape) => {
|
||||
const styles = getNodeStyles(shape.type)
|
||||
|
||||
return (
|
||||
<div style={styles.container}>
|
||||
<NodeProperty
|
||||
label="Start Date"
|
||||
value={shape.props.start_date}
|
||||
labelStyle={styles.property.label}
|
||||
valueStyle={styles.property.value}
|
||||
/>
|
||||
<NodeProperty
|
||||
label="End Date"
|
||||
value={shape.props.end_date}
|
||||
labelStyle={styles.property.label}
|
||||
valueStyle={styles.property.value}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
import { CCBaseShapeUtil } from '../CCBaseShapeUtil'
|
||||
import { CCBaseShape } from '../cc-types'
|
||||
import { NodeProperty } from './cc-graph-shared'
|
||||
import { ccGraphShapeProps, getDefaultCCScienceLabNodeProps } from './cc-graph-props'
|
||||
import { getNodeStyles } from './cc-graph-styles'
|
||||
import { NODE_THEMES, NODE_TYPE_THEMES } from './cc-graph-styles'
|
||||
import { CCScienceLabNodeProps } from './cc-graph-types'
|
||||
|
||||
export interface CCScienceLabNodeShape extends CCBaseShape {
|
||||
type: 'cc-science-lab-node'
|
||||
props: CCScienceLabNodeProps
|
||||
}
|
||||
|
||||
export class CCScienceLabNodeShapeUtil extends CCBaseShapeUtil<CCScienceLabNodeShape> {
|
||||
static type = 'cc-science-lab-node' as const
|
||||
static props = ccGraphShapeProps['cc-science-lab-node']
|
||||
|
||||
getDefaultProps(): CCScienceLabNodeShape['props'] {
|
||||
const defaultProps = getDefaultCCScienceLabNodeProps()
|
||||
const theme = NODE_THEMES[NODE_TYPE_THEMES[CCScienceLabNodeShapeUtil.type]]
|
||||
return {
|
||||
...defaultProps,
|
||||
headerColor: theme.headerColor,
|
||||
}
|
||||
}
|
||||
|
||||
// Override to nullify the default node component
|
||||
DefaultComponent = () => null
|
||||
|
||||
renderContent = (shape: CCScienceLabNodeShape) => {
|
||||
const styles = getNodeStyles(shape.type)
|
||||
|
||||
return (
|
||||
<div style={styles.container}>
|
||||
<NodeProperty
|
||||
label="Lab Title"
|
||||
value={shape.props.science_lab_title}
|
||||
labelStyle={styles.property.label}
|
||||
valueStyle={styles.property.value}
|
||||
/>
|
||||
<NodeProperty
|
||||
label="Summary"
|
||||
value={shape.props.science_lab_summary}
|
||||
labelStyle={styles.property.label}
|
||||
valueStyle={styles.property.value}
|
||||
/>
|
||||
<NodeProperty
|
||||
label="Requirements"
|
||||
value={shape.props.science_lab_requirements}
|
||||
labelStyle={styles.property.label}
|
||||
valueStyle={styles.property.value}
|
||||
/>
|
||||
<NodeProperty
|
||||
label="Procedure"
|
||||
value={shape.props.science_lab_procedure}
|
||||
labelStyle={styles.property.label}
|
||||
valueStyle={styles.property.value}
|
||||
/>
|
||||
<NodeProperty
|
||||
label="Safety"
|
||||
value={shape.props.science_lab_safety}
|
||||
labelStyle={styles.property.label}
|
||||
valueStyle={styles.property.value}
|
||||
/>
|
||||
<NodeProperty
|
||||
label="Web Links"
|
||||
value={shape.props.science_lab_weblinks}
|
||||
labelStyle={styles.property.label}
|
||||
valueStyle={styles.property.value}
|
||||
/>
|
||||
<NodeProperty
|
||||
label="Lab ID"
|
||||
value={shape.props.science_lab_id}
|
||||
labelStyle={styles.property.label}
|
||||
valueStyle={styles.property.value}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
import { CCBaseShapeUtil } from '../CCBaseShapeUtil';
|
||||
import { CCBaseShape } from '../cc-types';
|
||||
import { NodeProperty } from './cc-graph-shared';
|
||||
import { ccGraphShapeProps, getDefaultCCStudentNodeProps } from './cc-graph-props';
|
||||
import { getNodeStyles } from './cc-graph-styles';
|
||||
import { NODE_THEMES, NODE_TYPE_THEMES } from './cc-graph-styles';
|
||||
import { CCStudentNodeProps } from './cc-graph-types';
|
||||
|
||||
export interface CCStudentNodeShape extends CCBaseShape {
|
||||
type: 'cc-student-node';
|
||||
props: CCStudentNodeProps;
|
||||
}
|
||||
|
||||
export class CCStudentNodeShapeUtil extends CCBaseShapeUtil<CCStudentNodeShape> {
|
||||
static type = 'cc-student-node' as const;
|
||||
static props = ccGraphShapeProps['cc-student-node'];
|
||||
|
||||
getDefaultProps(): CCStudentNodeShape['props'] {
|
||||
const defaultProps = getDefaultCCStudentNodeProps();
|
||||
const theme = NODE_THEMES[NODE_TYPE_THEMES[CCStudentNodeShapeUtil.type]];
|
||||
return {
|
||||
...defaultProps,
|
||||
headerColor: theme.headerColor,
|
||||
};
|
||||
}
|
||||
|
||||
// Override to nullify the default node component
|
||||
DefaultComponent = () => null;
|
||||
|
||||
renderContent = (shape: CCStudentNodeShape) => {
|
||||
const styles = getNodeStyles(shape.type);
|
||||
|
||||
return (
|
||||
<div style={styles.container}>
|
||||
<NodeProperty
|
||||
label="Student Name"
|
||||
value={shape.props.student_name_formal}
|
||||
labelStyle={styles.property.label}
|
||||
valueStyle={styles.property.value}
|
||||
/>
|
||||
<NodeProperty
|
||||
label="Student Code"
|
||||
value={shape.props.student_code}
|
||||
labelStyle={styles.property.label}
|
||||
valueStyle={styles.property.value}
|
||||
/>
|
||||
<NodeProperty
|
||||
label="Email"
|
||||
value={shape.props.student_email}
|
||||
labelStyle={styles.property.label}
|
||||
valueStyle={styles.property.value}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
import { CCBaseShapeUtil } from '../CCBaseShapeUtil'
|
||||
import { CCBaseShape } from '../cc-types'
|
||||
import { NodeProperty } from './cc-graph-shared'
|
||||
import { ccGraphShapeProps, getDefaultCCSubjectClassNodeProps } from './cc-graph-props'
|
||||
import { getNodeStyles } from './cc-graph-styles'
|
||||
import { NODE_THEMES, NODE_TYPE_THEMES } from './cc-graph-styles'
|
||||
import { CCSubjectClassNodeProps } from './cc-graph-types'
|
||||
|
||||
export interface CCSubjectClassNodeShape extends CCBaseShape {
|
||||
type: 'cc-subject-class-node'
|
||||
props: CCSubjectClassNodeProps
|
||||
}
|
||||
|
||||
export class CCSubjectClassNodeShapeUtil extends CCBaseShapeUtil<CCSubjectClassNodeShape> {
|
||||
static type = 'cc-subject-class-node' as const
|
||||
static props = ccGraphShapeProps['cc-subject-class-node']
|
||||
|
||||
getDefaultProps(): CCSubjectClassNodeShape['props'] {
|
||||
const defaultProps = getDefaultCCSubjectClassNodeProps()
|
||||
const theme = NODE_THEMES[NODE_TYPE_THEMES[CCSubjectClassNodeShapeUtil.type]]
|
||||
return {
|
||||
...defaultProps,
|
||||
headerColor: theme.headerColor,
|
||||
}
|
||||
}
|
||||
|
||||
// Override to nullify the default node component
|
||||
DefaultComponent = () => null
|
||||
|
||||
renderContent = (shape: CCSubjectClassNodeShape) => {
|
||||
const styles = getNodeStyles(shape.type)
|
||||
|
||||
return (
|
||||
<div style={styles.container}>
|
||||
<NodeProperty
|
||||
label="Class Code"
|
||||
value={shape.props.subject_class_code}
|
||||
labelStyle={styles.property.label}
|
||||
valueStyle={styles.property.value}
|
||||
/>
|
||||
<NodeProperty
|
||||
label="Year Group"
|
||||
value={shape.props.year_group}
|
||||
labelStyle={styles.property.label}
|
||||
valueStyle={styles.property.value}
|
||||
/>
|
||||
<NodeProperty
|
||||
label="Subject"
|
||||
value={shape.props.subject}
|
||||
labelStyle={styles.property.label}
|
||||
valueStyle={styles.property.value}
|
||||
/>
|
||||
<NodeProperty
|
||||
label="Subject Code"
|
||||
value={shape.props.subject_code}
|
||||
labelStyle={styles.property.label}
|
||||
valueStyle={styles.property.value}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
import { CCBaseShapeUtil } from '../CCBaseShapeUtil'
|
||||
import { CCBaseShape } from '../cc-types'
|
||||
import { NodeProperty } from './cc-graph-shared'
|
||||
import { ccGraphShapeProps, getDefaultCCSubjectNodeProps } from './cc-graph-props'
|
||||
import { getNodeStyles } from './cc-graph-styles'
|
||||
import { NODE_THEMES, NODE_TYPE_THEMES } from './cc-graph-styles'
|
||||
import { CCSubjectNodeProps } from './cc-graph-types'
|
||||
|
||||
export interface CCSubjectNodeShape extends CCBaseShape {
|
||||
type: 'cc-subject-node'
|
||||
props: CCSubjectNodeProps
|
||||
}
|
||||
|
||||
export class CCSubjectNodeShapeUtil extends CCBaseShapeUtil<CCSubjectNodeShape> {
|
||||
static type = 'cc-subject-node' as const
|
||||
static props = ccGraphShapeProps['cc-subject-node']
|
||||
|
||||
getDefaultProps(): CCSubjectNodeShape['props'] {
|
||||
const defaultProps = getDefaultCCSubjectNodeProps()
|
||||
const theme = NODE_THEMES[NODE_TYPE_THEMES[CCSubjectNodeShapeUtil.type]]
|
||||
return {
|
||||
...defaultProps,
|
||||
headerColor: theme.headerColor,
|
||||
}
|
||||
}
|
||||
|
||||
// Override to nullify the default node component
|
||||
DefaultComponent = () => null
|
||||
|
||||
renderContent = (shape: CCSubjectNodeShape) => {
|
||||
const styles = getNodeStyles(shape.type)
|
||||
|
||||
return (
|
||||
<div style={styles.container}>
|
||||
<NodeProperty
|
||||
label="Subject Name"
|
||||
value={shape.props.subject_name}
|
||||
labelStyle={styles.property.label}
|
||||
valueStyle={styles.property.value}
|
||||
/>
|
||||
<NodeProperty
|
||||
label="Subject Code"
|
||||
value={shape.props.subject_code}
|
||||
labelStyle={styles.property.label}
|
||||
valueStyle={styles.property.value}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
import { CCBaseShapeUtil } from '../CCBaseShapeUtil'
|
||||
import { CCBaseShape } from '../cc-types'
|
||||
import {
|
||||
DefaultNodeComponent,
|
||||
NodeErrorDisplay,
|
||||
checkShapeState,
|
||||
checkDefaultComponent
|
||||
} from './cc-graph-shared'
|
||||
import { ccGraphShapeProps, getDefaultCCTeacherNodeProps } from './cc-graph-props'
|
||||
import { getNodeStyles, NODE_THEMES, NODE_TYPE_THEMES } from './cc-graph-styles'
|
||||
import { CCTeacherNodeProps } from './cc-graph-types'
|
||||
|
||||
export interface CCTeacherNodeShape extends CCBaseShape {
|
||||
type: 'cc-teacher-node'
|
||||
props: CCTeacherNodeProps
|
||||
}
|
||||
|
||||
export class CCTeacherNodeShapeUtil extends CCBaseShapeUtil<CCTeacherNodeShape> {
|
||||
static type = 'cc-teacher-node' as const
|
||||
static props = ccGraphShapeProps['cc-teacher-node']
|
||||
|
||||
getDefaultProps(): CCTeacherNodeShape['props'] {
|
||||
const defaultProps = getDefaultCCTeacherNodeProps()
|
||||
const theme = NODE_THEMES[NODE_TYPE_THEMES[CCTeacherNodeShapeUtil.type]]
|
||||
return {
|
||||
...defaultProps,
|
||||
headerColor: theme.headerColor
|
||||
}
|
||||
}
|
||||
|
||||
renderContent = (shape: CCTeacherNodeShape) => {
|
||||
const { props } = shape
|
||||
const { state, defaultComponent } = props
|
||||
const styles = getNodeStyles(shape.type)
|
||||
const isPageChild = state?.isPageChild ?? true
|
||||
|
||||
// Check state and default component
|
||||
const stateError = checkShapeState(state)
|
||||
if (stateError.hasError) {
|
||||
return <NodeErrorDisplay message={stateError.error.message} details={stateError.error.details} />
|
||||
}
|
||||
|
||||
const defaultComponentError = checkDefaultComponent(defaultComponent)
|
||||
if (defaultComponentError.hasError) {
|
||||
return <NodeErrorDisplay message={defaultComponentError.error.message} details={defaultComponentError.error.details} />
|
||||
}
|
||||
|
||||
if (isPageChild) {
|
||||
// Define properties to show for page-level view
|
||||
const properties = [
|
||||
{ label: 'Teacher Name', value: props.teacher_name_formal },
|
||||
{ label: 'Teacher Code', value: props.teacher_code },
|
||||
{ label: 'Email', value: props.teacher_email },
|
||||
{ label: 'Node Snapshot', value: props.tldraw_snapshot }
|
||||
]
|
||||
|
||||
return (
|
||||
<div style={styles.container}>
|
||||
{defaultComponent && <DefaultNodeComponent tldraw_snapshot={props.tldraw_snapshot} />}
|
||||
{properties.map((prop, index) => (
|
||||
<div key={index} style={styles.property.wrapper}>
|
||||
<span style={styles.property.label}>{prop.label}:</span>
|
||||
<span style={styles.property.value}>{prop.value}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// Simplified view when child of another shape
|
||||
return (
|
||||
<div style={styles.container}>
|
||||
<div style={styles.property.wrapper}>
|
||||
<span style={styles.property.label}>Code:</span>
|
||||
<span style={styles.property.value}>{props.teacher_code}</span>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
import { CCBaseShapeUtil } from '../CCBaseShapeUtil'
|
||||
import { CCBaseShape } from '../cc-types'
|
||||
import { NodeProperty } from './cc-graph-shared'
|
||||
import { ccGraphShapeProps, getDefaultCCTeacherTimetableNodeProps } from './cc-graph-props'
|
||||
import { getNodeStyles } from './cc-graph-styles'
|
||||
import { NODE_THEMES, NODE_TYPE_THEMES } from './cc-graph-styles'
|
||||
import { CCTeacherTimetableNodeProps } from './cc-graph-types'
|
||||
export interface CCTeacherTimetableNodeShape extends CCBaseShape {
|
||||
type: 'cc-teacher-timetable-node'
|
||||
props: CCTeacherTimetableNodeProps
|
||||
}
|
||||
|
||||
export class CCTeacherTimetableNodeShapeUtil extends CCBaseShapeUtil<CCTeacherTimetableNodeShape> {
|
||||
static type = 'cc-teacher-timetable-node' as const
|
||||
static props = ccGraphShapeProps['cc-teacher-timetable-node']
|
||||
|
||||
getDefaultProps(): CCTeacherTimetableNodeShape['props'] {
|
||||
const defaultProps = getDefaultCCTeacherTimetableNodeProps()
|
||||
const theme = NODE_THEMES[NODE_TYPE_THEMES[CCTeacherTimetableNodeShapeUtil.type]]
|
||||
return {
|
||||
...defaultProps,
|
||||
headerColor: theme.headerColor,
|
||||
}
|
||||
}
|
||||
|
||||
// Override to nullify the default node component
|
||||
DefaultComponent = () => null
|
||||
|
||||
renderContent = (shape: CCTeacherTimetableNodeShape) => {
|
||||
const styles = getNodeStyles(shape.type)
|
||||
|
||||
return (
|
||||
<div style={styles.container}>
|
||||
<NodeProperty
|
||||
label="Teacher ID"
|
||||
value={shape.props.teacher_id}
|
||||
labelStyle={styles.property.label}
|
||||
valueStyle={styles.property.value}
|
||||
/>
|
||||
<NodeProperty
|
||||
label="Start Date"
|
||||
value={shape.props.start_date}
|
||||
labelStyle={styles.property.label}
|
||||
valueStyle={styles.property.value}
|
||||
/>
|
||||
<NodeProperty
|
||||
label="End Date"
|
||||
value={shape.props.end_date}
|
||||
labelStyle={styles.property.label}
|
||||
valueStyle={styles.property.value}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
import { CCBaseShapeUtil } from '../CCBaseShapeUtil'
|
||||
import { CCBaseShape } from '../cc-types'
|
||||
import { NodeProperty } from './cc-graph-shared'
|
||||
import { ccGraphShapeProps, getDefaultCCTimetableLessonNodeProps } from './cc-graph-props'
|
||||
import { getNodeStyles } from './cc-graph-styles'
|
||||
import { NODE_THEMES, NODE_TYPE_THEMES } from './cc-graph-styles'
|
||||
import { CCTimetableLessonNodeProps } from './cc-graph-types'
|
||||
|
||||
export interface CCTimetableLessonNodeShape extends CCBaseShape {
|
||||
type: 'cc-timetable-lesson-node'
|
||||
props: CCTimetableLessonNodeProps
|
||||
}
|
||||
|
||||
export class CCTimetableLessonNodeShapeUtil extends CCBaseShapeUtil<CCTimetableLessonNodeShape> {
|
||||
static type = 'cc-timetable-lesson-node' as const
|
||||
static props = ccGraphShapeProps['cc-timetable-lesson-node']
|
||||
|
||||
getDefaultProps(): CCTimetableLessonNodeShape['props'] {
|
||||
const defaultProps = getDefaultCCTimetableLessonNodeProps()
|
||||
const theme = NODE_THEMES[NODE_TYPE_THEMES[CCTimetableLessonNodeShapeUtil.type]]
|
||||
return {
|
||||
...defaultProps,
|
||||
headerColor: theme.headerColor,
|
||||
}
|
||||
}
|
||||
|
||||
// Override to nullify the default node component
|
||||
DefaultComponent = () => null
|
||||
|
||||
renderContent = (shape: CCTimetableLessonNodeShape) => {
|
||||
const styles = getNodeStyles(shape.type)
|
||||
|
||||
return (
|
||||
<div style={styles.container}>
|
||||
<NodeProperty
|
||||
label="Subject Class"
|
||||
value={shape.props.subject_class}
|
||||
labelStyle={styles.property.label}
|
||||
valueStyle={styles.property.value}
|
||||
/>
|
||||
<NodeProperty
|
||||
label="Date"
|
||||
value={shape.props.date}
|
||||
labelStyle={styles.property.label}
|
||||
valueStyle={styles.property.value}
|
||||
/>
|
||||
<NodeProperty
|
||||
label="Start Time"
|
||||
value={shape.props.start_time}
|
||||
labelStyle={styles.property.label}
|
||||
valueStyle={styles.property.value}
|
||||
/>
|
||||
<NodeProperty
|
||||
label="End Time"
|
||||
value={shape.props.end_time}
|
||||
labelStyle={styles.property.label}
|
||||
valueStyle={styles.property.value}
|
||||
/>
|
||||
<NodeProperty
|
||||
label="Period Code"
|
||||
value={shape.props.period_code}
|
||||
labelStyle={styles.property.label}
|
||||
valueStyle={styles.property.value}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
import { CCBaseShapeUtil } from '../CCBaseShapeUtil'
|
||||
import { CCBaseShape } from '../cc-types'
|
||||
import { NodeProperty } from './cc-graph-shared'
|
||||
import { ccGraphShapeProps, getDefaultCCTopicLessonNodeProps } from './cc-graph-props'
|
||||
import { getNodeStyles } from './cc-graph-styles'
|
||||
import { NODE_THEMES, NODE_TYPE_THEMES } from './cc-graph-styles'
|
||||
import { CCTopicLessonNodeProps } from './cc-graph-types'
|
||||
|
||||
export interface CCTopicLessonNodeShape extends CCBaseShape {
|
||||
type: 'cc-topic-lesson-node'
|
||||
props: CCTopicLessonNodeProps
|
||||
}
|
||||
|
||||
export class CCTopicLessonNodeShapeUtil extends CCBaseShapeUtil<CCTopicLessonNodeShape> {
|
||||
static type = 'cc-topic-lesson-node' as const
|
||||
static props = ccGraphShapeProps['cc-topic-lesson-node']
|
||||
|
||||
getDefaultProps(): CCTopicLessonNodeShape['props'] {
|
||||
const defaultProps = getDefaultCCTopicLessonNodeProps()
|
||||
const theme = NODE_THEMES[NODE_TYPE_THEMES[CCTopicLessonNodeShapeUtil.type]]
|
||||
return {
|
||||
...defaultProps,
|
||||
headerColor: theme.headerColor,
|
||||
}
|
||||
}
|
||||
|
||||
// Override to nullify the default node component
|
||||
DefaultComponent = () => null
|
||||
|
||||
renderContent = (shape: CCTopicLessonNodeShape) => {
|
||||
const styles = getNodeStyles(shape.type)
|
||||
|
||||
return (
|
||||
<div style={styles.container}>
|
||||
<NodeProperty
|
||||
label="Lesson Title"
|
||||
value={shape.props.topic_lesson_title}
|
||||
labelStyle={styles.property.label}
|
||||
valueStyle={styles.property.value}
|
||||
/>
|
||||
<NodeProperty
|
||||
label="Lesson Type"
|
||||
value={shape.props.topic_lesson_type}
|
||||
labelStyle={styles.property.label}
|
||||
valueStyle={styles.property.value}
|
||||
/>
|
||||
<NodeProperty
|
||||
label="Length"
|
||||
value={shape.props.topic_lesson_length}
|
||||
labelStyle={styles.property.label}
|
||||
valueStyle={styles.property.value}
|
||||
/>
|
||||
<NodeProperty
|
||||
label="Skills Learned"
|
||||
value={shape.props.topic_lesson_skills_learned}
|
||||
labelStyle={styles.property.label}
|
||||
valueStyle={styles.property.value}
|
||||
/>
|
||||
<NodeProperty
|
||||
label="Suggested Activities"
|
||||
value={shape.props.topic_lesson_suggested_activities}
|
||||
labelStyle={styles.property.label}
|
||||
valueStyle={styles.property.value}
|
||||
/>
|
||||
<NodeProperty
|
||||
label="Web Links"
|
||||
value={shape.props.topic_lesson_weblinks}
|
||||
labelStyle={styles.property.label}
|
||||
valueStyle={styles.property.value}
|
||||
/>
|
||||
<NodeProperty
|
||||
label="Lesson ID"
|
||||
value={shape.props.topic_lesson_id}
|
||||
labelStyle={styles.property.label}
|
||||
valueStyle={styles.property.value}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
import { CCBaseShapeUtil } from '../CCBaseShapeUtil'
|
||||
import { CCBaseShape } from '../cc-types'
|
||||
import { NodeProperty } from './cc-graph-shared'
|
||||
import { ccGraphShapeProps, getDefaultCCTopicNodeProps } from './cc-graph-props'
|
||||
import { getNodeStyles } from './cc-graph-styles'
|
||||
import { NODE_THEMES, NODE_TYPE_THEMES } from './cc-graph-styles'
|
||||
import { CCTopicNodeProps } from './cc-graph-types'
|
||||
|
||||
export interface CCTopicNodeShape extends CCBaseShape {
|
||||
type: 'cc-topic-node'
|
||||
props: CCTopicNodeProps
|
||||
}
|
||||
|
||||
export class CCTopicNodeShapeUtil extends CCBaseShapeUtil<CCTopicNodeShape> {
|
||||
static type = 'cc-topic-node' as const
|
||||
static props = ccGraphShapeProps['cc-topic-node']
|
||||
|
||||
getDefaultProps(): CCTopicNodeShape['props'] {
|
||||
const defaultProps = getDefaultCCTopicNodeProps()
|
||||
const theme = NODE_THEMES[NODE_TYPE_THEMES[CCTopicNodeShapeUtil.type]]
|
||||
return {
|
||||
...defaultProps,
|
||||
headerColor: theme.headerColor,
|
||||
}
|
||||
}
|
||||
|
||||
// Override to nullify the default node component
|
||||
DefaultComponent = () => null
|
||||
|
||||
renderContent = (shape: CCTopicNodeShape) => {
|
||||
const styles = getNodeStyles(shape.type)
|
||||
|
||||
return (
|
||||
<div style={styles.container}>
|
||||
<NodeProperty
|
||||
label="Topic Title"
|
||||
value={shape.props.topic_title}
|
||||
labelStyle={styles.property.label}
|
||||
valueStyle={styles.property.value}
|
||||
/>
|
||||
<NodeProperty
|
||||
label="Total Lessons"
|
||||
value={shape.props.total_number_of_lessons_for_topic}
|
||||
labelStyle={styles.property.label}
|
||||
valueStyle={styles.property.value}
|
||||
/>
|
||||
<NodeProperty
|
||||
label="Topic Type"
|
||||
value={shape.props.topic_type}
|
||||
labelStyle={styles.property.label}
|
||||
valueStyle={styles.property.value}
|
||||
/>
|
||||
<NodeProperty
|
||||
label="Assessment Type"
|
||||
value={shape.props.topic_assessment_type}
|
||||
labelStyle={styles.property.label}
|
||||
valueStyle={styles.property.value}
|
||||
/>
|
||||
<NodeProperty
|
||||
label="Topic ID"
|
||||
value={shape.props.topic_id}
|
||||
labelStyle={styles.property.label}
|
||||
valueStyle={styles.property.value}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
import { CCBaseShapeUtil } from '../CCBaseShapeUtil'
|
||||
import { CCBaseShape } from '../cc-types'
|
||||
import {
|
||||
DefaultNodeComponent,
|
||||
NodeErrorDisplay,
|
||||
checkShapeState,
|
||||
checkDefaultComponent
|
||||
} from './cc-graph-shared'
|
||||
import { ccGraphShapeProps, getDefaultCCUserNodeProps } from './cc-graph-props'
|
||||
import { CCUserNodeProps } from './cc-graph-types'
|
||||
import { getNodeStyles, NODE_THEMES, NODE_TYPE_THEMES } from './cc-graph-styles'
|
||||
|
||||
export interface CCUserNodeShape extends CCBaseShape {
|
||||
type: 'cc-user-node'
|
||||
props: CCUserNodeProps
|
||||
}
|
||||
|
||||
export class CCUserNodeShapeUtil extends CCBaseShapeUtil<CCUserNodeShape> {
|
||||
static type = 'cc-user-node' as const
|
||||
static props = ccGraphShapeProps['cc-user-node']
|
||||
|
||||
getDefaultProps(): CCUserNodeProps {
|
||||
const defaultProps = getDefaultCCUserNodeProps()
|
||||
const theme = NODE_THEMES[NODE_TYPE_THEMES[CCUserNodeShapeUtil.type]]
|
||||
return {
|
||||
...defaultProps,
|
||||
headerColor: theme.headerColor,
|
||||
backgroundColor: theme.backgroundColor,
|
||||
}
|
||||
}
|
||||
|
||||
renderContent = (shape: CCUserNodeShape) => {
|
||||
const { props } = shape
|
||||
const { state, defaultComponent } = props
|
||||
const styles = getNodeStyles(shape.type)
|
||||
|
||||
// Check state and default component
|
||||
const stateError = checkShapeState(state)
|
||||
if (stateError.hasError) {
|
||||
return <NodeErrorDisplay message={stateError.error.message} details={stateError.error.details} />
|
||||
}
|
||||
|
||||
const defaultComponentError = checkDefaultComponent(defaultComponent)
|
||||
if (defaultComponentError.hasError) {
|
||||
return <NodeErrorDisplay message={defaultComponentError.error.message} details={defaultComponentError.error.details} />
|
||||
}
|
||||
|
||||
// Define properties to show based on view type
|
||||
const properties = defaultComponent ? [
|
||||
{ label: 'User Name', value: props.user_name },
|
||||
{ label: 'User Email', value: props.user_email },
|
||||
{ label: 'User Type', value: props.user_type },
|
||||
{ label: 'User ID', value: props.user_id },
|
||||
{ label: 'Node Snapshot', value: props.tldraw_snapshot },
|
||||
{ label: 'Worker Node Data', value: props.worker_node_data }
|
||||
] : [
|
||||
{ label: 'User Name', value: props.user_name },
|
||||
{ label: 'User Email', value: props.user_email }
|
||||
]
|
||||
|
||||
return (
|
||||
<div style={styles.container}>
|
||||
{defaultComponent && <DefaultNodeComponent tldraw_snapshot={props.tldraw_snapshot} />}
|
||||
{properties.map((prop, index) => (
|
||||
<div key={index} style={styles.property.wrapper}>
|
||||
<span style={styles.property.label}>{prop.label}:</span>
|
||||
<span style={styles.property.value}>{prop.value}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
import { CCBaseShapeUtil } from '../CCBaseShapeUtil'
|
||||
import { CCBaseShape } from '../cc-types'
|
||||
import { NodeProperty } from './cc-graph-shared'
|
||||
import { ccGraphShapeProps, getDefaultCCUserTeacherTimetableNodeProps } from './cc-graph-props'
|
||||
import { getNodeStyles } from './cc-graph-styles'
|
||||
import { NODE_THEMES, NODE_TYPE_THEMES } from './cc-graph-styles'
|
||||
import { CCUserTeacherTimetableNodeProps } from './cc-graph-types'
|
||||
|
||||
export interface CCUserTeacherTimetableNodeShape extends CCBaseShape {
|
||||
type: 'cc-user-teacher-timetable-node'
|
||||
props: CCUserTeacherTimetableNodeProps
|
||||
}
|
||||
|
||||
export class CCUserTeacherTimetableNodeShapeUtil extends CCBaseShapeUtil<CCUserTeacherTimetableNodeShape> {
|
||||
static type = 'cc-user-teacher-timetable-node' as const
|
||||
static props = ccGraphShapeProps['cc-user-teacher-timetable-node']
|
||||
|
||||
getDefaultProps(): CCUserTeacherTimetableNodeShape['props'] {
|
||||
const defaultProps = getDefaultCCUserTeacherTimetableNodeProps() as CCUserTeacherTimetableNodeShape['props']
|
||||
const theme = NODE_THEMES[NODE_TYPE_THEMES[CCUserTeacherTimetableNodeShapeUtil.type]]
|
||||
return {
|
||||
...defaultProps,
|
||||
headerColor: theme.headerColor,
|
||||
}
|
||||
}
|
||||
|
||||
// Override to nullify the default node component
|
||||
DefaultComponent = () => null
|
||||
|
||||
renderContent = (shape: CCUserTeacherTimetableNodeShape) => {
|
||||
const styles = getNodeStyles(shape.type)
|
||||
|
||||
return (
|
||||
<div style={styles.container}>
|
||||
<NodeProperty
|
||||
label="School DB"
|
||||
value={shape.props.school_db_name}
|
||||
labelStyle={styles.property.label}
|
||||
valueStyle={styles.property.value}
|
||||
/>
|
||||
<NodeProperty
|
||||
label="School Timetable ID"
|
||||
value={shape.props.school_timetable_id}
|
||||
labelStyle={styles.property.label}
|
||||
valueStyle={styles.property.value}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
import { CCBaseShapeUtil } from '../CCBaseShapeUtil'
|
||||
import { CCBaseShape } from '../cc-types'
|
||||
import { NodeProperty, formatDate } from './cc-graph-shared'
|
||||
import { ccGraphShapeProps, getDefaultCCUserTimetableLessonNodeProps } from './cc-graph-props'
|
||||
import { getNodeStyles } from './cc-graph-styles'
|
||||
import { NODE_THEMES, NODE_TYPE_THEMES } from './cc-graph-styles'
|
||||
import { CCUserTimetableLessonNodeProps } from './cc-graph-types'
|
||||
|
||||
export interface CCUserTimetableLessonNodeShape extends CCBaseShape {
|
||||
type: 'cc-user-timetable-lesson-node'
|
||||
props: CCUserTimetableLessonNodeProps
|
||||
}
|
||||
|
||||
export class CCUserTimetableLessonNodeShapeUtil extends CCBaseShapeUtil<CCUserTimetableLessonNodeShape> {
|
||||
static type = 'cc-user-timetable-lesson-node' as const
|
||||
static props = ccGraphShapeProps['cc-user-timetable-lesson-node']
|
||||
|
||||
getDefaultProps(): CCUserTimetableLessonNodeShape['props'] {
|
||||
const defaultProps = getDefaultCCUserTimetableLessonNodeProps() as CCUserTimetableLessonNodeShape['props']
|
||||
const theme = NODE_THEMES[NODE_TYPE_THEMES[CCUserTimetableLessonNodeShapeUtil.type]]
|
||||
return {
|
||||
...defaultProps,
|
||||
headerColor: theme.headerColor,
|
||||
}
|
||||
}
|
||||
|
||||
// Override to nullify the default node component
|
||||
DefaultComponent = () => null
|
||||
|
||||
renderContent = (shape: CCUserTimetableLessonNodeShape) => {
|
||||
const styles = getNodeStyles(shape.type)
|
||||
|
||||
return (
|
||||
<div style={styles.container}>
|
||||
<NodeProperty
|
||||
label="Subject Class"
|
||||
value={shape.props.subject_class}
|
||||
labelStyle={styles.property.label}
|
||||
valueStyle={styles.property.value}
|
||||
/>
|
||||
<NodeProperty
|
||||
label="Date"
|
||||
value={formatDate(shape.props.date)}
|
||||
labelStyle={styles.property.label}
|
||||
valueStyle={styles.property.value}
|
||||
/>
|
||||
<NodeProperty
|
||||
label="Start Time"
|
||||
value={formatDate(shape.props.start_time)}
|
||||
labelStyle={styles.property.label}
|
||||
valueStyle={styles.property.value}
|
||||
/>
|
||||
<NodeProperty
|
||||
label="End Time"
|
||||
value={formatDate(shape.props.end_time)}
|
||||
labelStyle={styles.property.label}
|
||||
valueStyle={styles.property.value}
|
||||
/>
|
||||
<NodeProperty
|
||||
label="Period Code"
|
||||
value={shape.props.period_code}
|
||||
labelStyle={styles.property.label}
|
||||
valueStyle={styles.property.value}
|
||||
/>
|
||||
<NodeProperty
|
||||
label="School DB"
|
||||
value={shape.props.school_db_name}
|
||||
labelStyle={styles.property.label}
|
||||
valueStyle={styles.property.value}
|
||||
/>
|
||||
<NodeProperty
|
||||
label="School Period ID"
|
||||
value={shape.props.school_period_id}
|
||||
labelStyle={styles.property.label}
|
||||
valueStyle={styles.property.value}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
import { CCBaseShapeUtil } from '../CCBaseShapeUtil'
|
||||
import { CCBaseShape } from '../cc-types'
|
||||
import { NodeProperty } from './cc-graph-shared'
|
||||
import { ccGraphShapeProps, getDefaultCCYearGroupNodeProps } from './cc-graph-props'
|
||||
import { getNodeStyles } from './cc-graph-styles'
|
||||
import { NODE_THEMES, NODE_TYPE_THEMES } from './cc-graph-styles'
|
||||
import { CCYearGroupNodeProps } from './cc-graph-types'
|
||||
|
||||
export interface CCYearGroupNodeShape extends CCBaseShape {
|
||||
type: 'cc-year-group-node'
|
||||
props: CCYearGroupNodeProps
|
||||
}
|
||||
|
||||
export class CCYearGroupNodeShapeUtil extends CCBaseShapeUtil<CCYearGroupNodeShape> {
|
||||
static type = 'cc-year-group-node' as const
|
||||
static props = ccGraphShapeProps['cc-year-group-node']
|
||||
|
||||
getDefaultProps(): CCYearGroupNodeShape['props'] {
|
||||
const defaultProps = getDefaultCCYearGroupNodeProps()
|
||||
const theme = NODE_THEMES[NODE_TYPE_THEMES[CCYearGroupNodeShapeUtil.type]]
|
||||
return {
|
||||
...defaultProps,
|
||||
headerColor: theme.headerColor,
|
||||
}
|
||||
}
|
||||
|
||||
// Override to nullify the default node component
|
||||
DefaultComponent = () => null
|
||||
|
||||
renderContent = (shape: CCYearGroupNodeShape) => {
|
||||
const styles = getNodeStyles(shape.type)
|
||||
|
||||
return (
|
||||
<div style={styles.container}>
|
||||
<NodeProperty
|
||||
label="Year Group"
|
||||
value={shape.props.year_group}
|
||||
labelStyle={styles.property.label}
|
||||
valueStyle={styles.property.value}
|
||||
/>
|
||||
<NodeProperty
|
||||
label="Year Group Name"
|
||||
value={shape.props.year_group_name}
|
||||
labelStyle={styles.property.label}
|
||||
valueStyle={styles.property.value}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
import { CCBaseShapeUtil } from '../CCBaseShapeUtil'
|
||||
import { CCBaseShape } from '../cc-types'
|
||||
import { NodeProperty } from './cc-graph-shared'
|
||||
import { ccGraphShapeProps, getDefaultCCYearGroupSyllabusNodeProps } from './cc-graph-props'
|
||||
import { getNodeStyles } from './cc-graph-styles'
|
||||
import { NODE_THEMES, NODE_TYPE_THEMES } from './cc-graph-styles'
|
||||
import { CCYearGroupSyllabusNodeProps } from './cc-graph-types'
|
||||
|
||||
export interface CCYearGroupSyllabusNodeShape extends CCBaseShape {
|
||||
type: 'cc-year-group-syllabus-node'
|
||||
props: CCYearGroupSyllabusNodeProps
|
||||
}
|
||||
|
||||
export class CCYearGroupSyllabusNodeShapeUtil extends CCBaseShapeUtil<CCYearGroupSyllabusNodeShape> {
|
||||
static type = 'cc-year-group-syllabus-node' as const
|
||||
static props = ccGraphShapeProps['cc-year-group-syllabus-node']
|
||||
|
||||
getDefaultProps(): CCYearGroupSyllabusNodeShape['props'] {
|
||||
const defaultProps = getDefaultCCYearGroupSyllabusNodeProps()
|
||||
const theme = NODE_THEMES[NODE_TYPE_THEMES[CCYearGroupSyllabusNodeShapeUtil.type]]
|
||||
return {
|
||||
...defaultProps,
|
||||
headerColor: theme.headerColor,
|
||||
}
|
||||
}
|
||||
|
||||
// Override to nullify the default node component
|
||||
DefaultComponent = () => null
|
||||
|
||||
renderContent = (shape: CCYearGroupSyllabusNodeShape) => {
|
||||
const styles = getNodeStyles(shape.type)
|
||||
|
||||
return (
|
||||
<div style={styles.container}>
|
||||
<NodeProperty
|
||||
label="Syllabus Name"
|
||||
value={shape.props.yr_syllabus_name}
|
||||
labelStyle={styles.property.label}
|
||||
valueStyle={styles.property.value}
|
||||
/>
|
||||
<NodeProperty
|
||||
label="Year Group"
|
||||
value={shape.props.yr_syllabus_year_group}
|
||||
labelStyle={styles.property.label}
|
||||
valueStyle={styles.property.value}
|
||||
/>
|
||||
<NodeProperty
|
||||
label="Subject"
|
||||
value={shape.props.yr_syllabus_subject}
|
||||
labelStyle={styles.property.label}
|
||||
valueStyle={styles.property.value}
|
||||
/>
|
||||
<NodeProperty
|
||||
label="Subject Code"
|
||||
value={shape.props.yr_syllabus_subject_code}
|
||||
labelStyle={styles.property.label}
|
||||
valueStyle={styles.property.value}
|
||||
/>
|
||||
<NodeProperty
|
||||
label="Syllabus ID"
|
||||
value={shape.props.yr_syllabus_id}
|
||||
labelStyle={styles.property.label}
|
||||
valueStyle={styles.property.value}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
import { ccGraphShapeProps } from './cc-graph-props'
|
||||
import { createShapePropsMigrationIds, createShapePropsMigrationSequence } from '@tldraw/tldraw'
|
||||
import { CCGraphShape, GraphShapeType } from './cc-graph-types'
|
||||
|
||||
// Helper function to create version IDs for a shape type
|
||||
const createVersions = (shapeType: GraphShapeType) => {
|
||||
return createShapePropsMigrationIds(shapeType, {
|
||||
Initial: 1 // All shapes start at version 1 as required by TLDraw
|
||||
})
|
||||
}
|
||||
|
||||
// Helper function to create a migration sequence for a shape
|
||||
const createMigrationSequence = (shapeType: GraphShapeType) => {
|
||||
const versions = createVersions(shapeType)
|
||||
return createShapePropsMigrationSequence({
|
||||
sequence: [
|
||||
{
|
||||
id: versions.Initial,
|
||||
up: (props: CCGraphShape['props']) => {
|
||||
// Initial version - no changes needed
|
||||
return props
|
||||
},
|
||||
},
|
||||
],
|
||||
})
|
||||
}
|
||||
|
||||
// Create migrations for all graph shapes
|
||||
export const ccGraphMigrations = Object.keys(ccGraphShapeProps).reduce((acc, shapeType) => ({
|
||||
...acc,
|
||||
[shapeType]: createMigrationSequence(shapeType as GraphShapeType)
|
||||
}), {} as Record<GraphShapeType, ReturnType<typeof createShapePropsMigrationSequence>>)
|
||||
@@ -0,0 +1,657 @@
|
||||
import { T, TLBinding, TLShapeId } from '@tldraw/tldraw'
|
||||
import { baseShapeProps } from '../cc-props'
|
||||
import { ShapeState } from './cc-graph-types'
|
||||
|
||||
// State props validation
|
||||
const stateProps = T.object({
|
||||
parentId: T.optional(T.string.nullable()),
|
||||
isPageChild: T.optional(T.boolean.nullable()),
|
||||
hasChildren: T.optional(T.boolean.nullable()),
|
||||
bindings: T.optional(T.arrayOf(T.object({})).nullable())
|
||||
})
|
||||
|
||||
// Base props for all nodes
|
||||
const graphBaseProps = {
|
||||
...baseShapeProps,
|
||||
__primarylabel__: T.string,
|
||||
unique_id: T.string,
|
||||
tldraw_snapshot: T.string,
|
||||
created: T.string,
|
||||
merged: T.string,
|
||||
state: T.optional(stateProps.nullable()),
|
||||
defaultComponent: T.optional(T.boolean.nullable())
|
||||
}
|
||||
|
||||
// Props for specific node types
|
||||
export const ccGraphShapeProps = {
|
||||
'cc-user-node': {
|
||||
...graphBaseProps,
|
||||
user_name: T.string,
|
||||
user_email: T.string,
|
||||
user_type: T.string,
|
||||
user_id: T.string,
|
||||
worker_node_data: T.string,
|
||||
},
|
||||
'cc-teacher-node': {
|
||||
...graphBaseProps,
|
||||
teacher_code: T.string,
|
||||
teacher_name_formal: T.string,
|
||||
teacher_email: T.string,
|
||||
user_db_name: T.string,
|
||||
worker_db_name: T.string,
|
||||
},
|
||||
'cc-student-node': {
|
||||
...graphBaseProps,
|
||||
student_code: T.string,
|
||||
student_name_formal: T.string,
|
||||
student_email: T.string,
|
||||
worker_db_name: T.string,
|
||||
},
|
||||
'cc-calendar-node': {
|
||||
...graphBaseProps,
|
||||
name: T.string,
|
||||
calendar_type: T.string,
|
||||
calendar_name: T.string,
|
||||
start_date: T.string,
|
||||
end_date: T.string,
|
||||
},
|
||||
'cc-calendar-year-node': {
|
||||
...graphBaseProps,
|
||||
year: T.string,
|
||||
},
|
||||
'cc-calendar-month-node': {
|
||||
...graphBaseProps,
|
||||
year: T.string,
|
||||
month: T.string,
|
||||
month_name: T.string,
|
||||
},
|
||||
'cc-calendar-week-node': {
|
||||
...graphBaseProps,
|
||||
start_date: T.string,
|
||||
week_number: T.string,
|
||||
iso_week: T.string,
|
||||
},
|
||||
'cc-calendar-day-node': {
|
||||
...graphBaseProps,
|
||||
date: T.string,
|
||||
day_of_week: T.string,
|
||||
iso_day: T.string,
|
||||
},
|
||||
'cc-calendar-time-chunk-node': {
|
||||
...graphBaseProps,
|
||||
start_time: T.string,
|
||||
end_time: T.string,
|
||||
},
|
||||
'cc-school-node': {
|
||||
...graphBaseProps,
|
||||
school_uuid: T.string,
|
||||
school_name: T.string,
|
||||
school_website: T.string,
|
||||
},
|
||||
'cc-department-node': {
|
||||
...graphBaseProps,
|
||||
department_name: T.string,
|
||||
},
|
||||
'cc-room-node': {
|
||||
...graphBaseProps,
|
||||
room_code: T.string,
|
||||
room_name: T.string,
|
||||
},
|
||||
'cc-subject-class-node': {
|
||||
...graphBaseProps,
|
||||
subject_class_code: T.string,
|
||||
year_group: T.string,
|
||||
subject: T.string,
|
||||
subject_code: T.string,
|
||||
},
|
||||
'cc-pastoral-structure-node': {
|
||||
...graphBaseProps,
|
||||
},
|
||||
'cc-year-group-node': {
|
||||
...graphBaseProps,
|
||||
year_group: T.string,
|
||||
year_group_name: T.string,
|
||||
},
|
||||
'cc-curriculum-structure-node': {
|
||||
...graphBaseProps,
|
||||
},
|
||||
'cc-key-stage-node': {
|
||||
...graphBaseProps,
|
||||
key_stage_name: T.string,
|
||||
key_stage: T.string,
|
||||
},
|
||||
'cc-key-stage-syllabus-node': {
|
||||
...graphBaseProps,
|
||||
ks_syllabus_id: T.string,
|
||||
ks_syllabus_name: T.string,
|
||||
ks_syllabus_key_stage: T.string,
|
||||
ks_syllabus_subject: T.string,
|
||||
ks_syllabus_subject_code: T.string,
|
||||
},
|
||||
'cc-year-group-syllabus-node': {
|
||||
...graphBaseProps,
|
||||
yr_syllabus_id: T.string,
|
||||
yr_syllabus_name: T.string,
|
||||
yr_syllabus_year_group: T.string,
|
||||
yr_syllabus_subject: T.string,
|
||||
yr_syllabus_subject_code: T.string,
|
||||
},
|
||||
'cc-subject-node': {
|
||||
...graphBaseProps,
|
||||
subject_code: T.string,
|
||||
subject_name: T.string,
|
||||
},
|
||||
'cc-topic-node': {
|
||||
...graphBaseProps,
|
||||
topic_id: T.string,
|
||||
topic_title: T.string,
|
||||
total_number_of_lessons_for_topic: T.string,
|
||||
topic_type: T.string,
|
||||
topic_assessment_type: T.string,
|
||||
},
|
||||
'cc-topic-lesson-node': {
|
||||
...graphBaseProps,
|
||||
topic_lesson_id: T.string,
|
||||
topic_lesson_title: T.string,
|
||||
topic_lesson_type: T.string,
|
||||
topic_lesson_length: T.string,
|
||||
topic_lesson_skills_learned: T.string,
|
||||
topic_lesson_suggested_activities: T.string,
|
||||
topic_lesson_weblinks: T.string,
|
||||
},
|
||||
'cc-learning-statement-node': {
|
||||
...graphBaseProps,
|
||||
lesson_learning_statement_id: T.string,
|
||||
lesson_learning_statement: T.string,
|
||||
lesson_learning_statement_type: T.string,
|
||||
},
|
||||
'cc-science-lab-node': {
|
||||
...graphBaseProps,
|
||||
science_lab_id: T.string,
|
||||
science_lab_title: T.string,
|
||||
science_lab_summary: T.string,
|
||||
science_lab_requirements: T.string,
|
||||
science_lab_procedure: T.string,
|
||||
science_lab_safety: T.string,
|
||||
science_lab_weblinks: T.string,
|
||||
},
|
||||
'cc-teacher-timetable-node': {
|
||||
...graphBaseProps,
|
||||
teacher_id: T.string,
|
||||
start_date: T.string,
|
||||
end_date: T.string,
|
||||
},
|
||||
'cc-timetable-lesson-node': {
|
||||
...graphBaseProps,
|
||||
subject_class: T.string,
|
||||
date: T.string,
|
||||
start_time: T.string,
|
||||
end_time: T.string,
|
||||
period_code: T.string,
|
||||
},
|
||||
'cc-planned-lesson-node': {
|
||||
...graphBaseProps,
|
||||
date: T.string,
|
||||
start_time: T.string,
|
||||
end_time: T.string,
|
||||
period_code: T.string,
|
||||
subject_class: T.string,
|
||||
year_group: T.string,
|
||||
subject: T.string,
|
||||
teacher_code: T.string,
|
||||
planning_status: T.string,
|
||||
topic_code: T.string,
|
||||
topic_name: T.string,
|
||||
lesson_code: T.string,
|
||||
lesson_name: T.string,
|
||||
learning_statement_codes: T.string,
|
||||
learning_statements: T.string,
|
||||
learning_resource_codes: T.string,
|
||||
learning_resources: T.string,
|
||||
},
|
||||
'cc-school-timetable-node': {
|
||||
...graphBaseProps,
|
||||
start_date: T.string,
|
||||
end_date: T.string,
|
||||
},
|
||||
'cc-academic-year-node': {
|
||||
...graphBaseProps,
|
||||
year: T.string,
|
||||
},
|
||||
'cc-academic-term-node': {
|
||||
...graphBaseProps,
|
||||
term_name: T.string,
|
||||
term_number: T.string,
|
||||
start_date: T.string,
|
||||
end_date: T.string,
|
||||
},
|
||||
'cc-academic-week-node': {
|
||||
...graphBaseProps,
|
||||
academic_week_number: T.string,
|
||||
start_date: T.string,
|
||||
week_type: T.string,
|
||||
},
|
||||
'cc-academic-day-node': {
|
||||
...graphBaseProps,
|
||||
academic_day: T.string,
|
||||
date: T.string,
|
||||
day_of_week: T.string,
|
||||
day_type: T.string,
|
||||
},
|
||||
'cc-academic-period-node': {
|
||||
...graphBaseProps,
|
||||
name: T.string,
|
||||
date: T.string,
|
||||
start_time: T.string,
|
||||
end_time: T.string,
|
||||
period_code: T.string,
|
||||
},
|
||||
'cc-registration-period-node': {
|
||||
...graphBaseProps,
|
||||
name: T.string,
|
||||
date: T.string,
|
||||
start_time: T.string,
|
||||
end_time: T.string,
|
||||
period_code: T.string,
|
||||
},
|
||||
'cc-department-structure-node': {
|
||||
...graphBaseProps,
|
||||
department_structure_type: T.string,
|
||||
},
|
||||
'cc-user-teacher-timetable-node': {
|
||||
...graphBaseProps,
|
||||
school_db_name: T.string,
|
||||
school_timetable_id: T.string,
|
||||
},
|
||||
'cc-user-timetable-lesson-node': {
|
||||
...graphBaseProps,
|
||||
subject_class: T.string,
|
||||
date: T.string,
|
||||
start_time: T.string,
|
||||
end_time: T.string,
|
||||
period_code: T.string,
|
||||
school_db_name: T.string,
|
||||
school_period_id: T.string,
|
||||
},
|
||||
} as const
|
||||
|
||||
// Default props getters
|
||||
export const getDefaultBaseProps = () => ({
|
||||
w: 200 as number,
|
||||
h: 200 as number,
|
||||
headerColor: '#3e6589' as string,
|
||||
backgroundColor: '#f0f0f0' as string,
|
||||
title: 'Untitled' as string,
|
||||
isLocked: false as boolean,
|
||||
unique_id: '' as string,
|
||||
tldraw_snapshot: '' as string,
|
||||
created: '' as string,
|
||||
merged: '' as string,
|
||||
state: {
|
||||
parentId: null as TLShapeId | null,
|
||||
isPageChild: true as boolean | null,
|
||||
hasChildren: null as boolean | null,
|
||||
bindings: null as TLBinding[] | null
|
||||
} as ShapeState | null,
|
||||
defaultComponent: true as boolean | null
|
||||
})
|
||||
|
||||
export const getDefaultCCUserNodeProps = () => ({
|
||||
...getDefaultBaseProps(),
|
||||
__primarylabel__: 'User',
|
||||
user_name: '',
|
||||
user_email: '',
|
||||
user_type: '',
|
||||
user_id: '',
|
||||
worker_node_data: ''
|
||||
})
|
||||
|
||||
export const getDefaultCCTeacherNodeProps = () => ({
|
||||
...getDefaultBaseProps(),
|
||||
__primarylabel__: 'Teacher',
|
||||
teacher_code: '',
|
||||
teacher_name_formal: '',
|
||||
teacher_email: '',
|
||||
user_db_name: '',
|
||||
school_db_name: '',
|
||||
})
|
||||
|
||||
export const getDefaultCCStudentNodeProps = () => ({
|
||||
...getDefaultBaseProps(),
|
||||
__primarylabel__: 'Student',
|
||||
student_code: '',
|
||||
student_name_formal: '',
|
||||
student_email: '',
|
||||
school_db_name: '',
|
||||
user_db_name: '',
|
||||
})
|
||||
|
||||
export const getDefaultCCCalendarNodeProps = () => ({
|
||||
...getDefaultBaseProps(),
|
||||
title: 'Calendar',
|
||||
__primarylabel__: 'Calendar',
|
||||
name: '',
|
||||
calendar_type: '',
|
||||
calendar_name: '',
|
||||
start_date: '',
|
||||
end_date: '',
|
||||
})
|
||||
|
||||
export const getDefaultCCCalendarYearNodeProps = () => ({
|
||||
...getDefaultBaseProps(),
|
||||
title: 'Calendar Year',
|
||||
__primarylabel__: 'Calendar Year',
|
||||
year: '',
|
||||
})
|
||||
|
||||
export const getDefaultCCCalendarMonthNodeProps = () => ({
|
||||
...getDefaultBaseProps(),
|
||||
title: 'Calendar Month',
|
||||
__primarylabel__: 'Calendar Month',
|
||||
year: '',
|
||||
month: '',
|
||||
month_name: '',
|
||||
})
|
||||
|
||||
export const getDefaultCCCalendarWeekNodeProps = () => ({
|
||||
...getDefaultBaseProps(),
|
||||
title: 'Calendar Week',
|
||||
__primarylabel__: 'Calendar Week',
|
||||
start_date: '',
|
||||
week_number: '',
|
||||
iso_week: '',
|
||||
})
|
||||
|
||||
export const getDefaultCCCalendarDayNodeProps = () => ({
|
||||
...getDefaultBaseProps(),
|
||||
title: 'Calendar Day',
|
||||
__primarylabel__: 'Calendar Day',
|
||||
date: '',
|
||||
day_of_week: '',
|
||||
iso_day: '',
|
||||
})
|
||||
|
||||
export const getDefaultCCCalendarTimeChunkNodeProps = () => ({
|
||||
...getDefaultBaseProps(),
|
||||
title: 'Calendar Time Chunk',
|
||||
__primarylabel__: 'Calendar Time Chunk',
|
||||
start_time: '',
|
||||
end_time: '',
|
||||
})
|
||||
|
||||
export const getDefaultCCSchoolNodeProps = () => ({
|
||||
...getDefaultBaseProps(),
|
||||
title: 'School',
|
||||
__primarylabel__: 'School',
|
||||
school_uuid: '',
|
||||
school_name: '',
|
||||
school_website: '',
|
||||
})
|
||||
|
||||
export const getDefaultCCDepartmentNodeProps = () => ({
|
||||
...getDefaultBaseProps(),
|
||||
title: 'Department',
|
||||
__primarylabel__: 'Department',
|
||||
department_name: '',
|
||||
})
|
||||
|
||||
export const getDefaultCCRoomNodeProps = () => ({
|
||||
...getDefaultBaseProps(),
|
||||
title: 'Room',
|
||||
__primarylabel__: 'Room',
|
||||
room_code: '',
|
||||
room_name: '',
|
||||
})
|
||||
|
||||
export const getDefaultCCSubjectClassNodeProps = () => ({
|
||||
...getDefaultBaseProps(),
|
||||
title: 'Subject Class',
|
||||
__primarylabel__: 'Subject Class',
|
||||
subject_class_code: '',
|
||||
year_group: '',
|
||||
subject: '',
|
||||
subject_code: '',
|
||||
})
|
||||
|
||||
export const getDefaultCCPastoralStructureNodeProps = () => ({
|
||||
...getDefaultBaseProps(),
|
||||
title: 'Pastoral Structure',
|
||||
__primarylabel__: 'Pastoral Structure',
|
||||
pastoral_structure_type: '',
|
||||
})
|
||||
|
||||
export const getDefaultCCYearGroupNodeProps = () => ({
|
||||
...getDefaultBaseProps(),
|
||||
title: 'Year Group',
|
||||
__primarylabel__: 'Year Group',
|
||||
year_group: '',
|
||||
year_group_name: '',
|
||||
})
|
||||
|
||||
export const getDefaultCCCurriculumStructureNodeProps = () => ({
|
||||
...getDefaultBaseProps(),
|
||||
title: 'Curriculum Structure',
|
||||
__primarylabel__: 'Curriculum Structure',
|
||||
curriculum_structure_type: '',
|
||||
})
|
||||
|
||||
export const getDefaultCCKeyStageNodeProps = () => ({
|
||||
...getDefaultBaseProps(),
|
||||
title: 'Key Stage',
|
||||
__primarylabel__: 'Key Stage',
|
||||
key_stage_name: '',
|
||||
key_stage: '',
|
||||
})
|
||||
|
||||
export const getDefaultCCKeyStageSyllabusNodeProps = () => ({
|
||||
...getDefaultBaseProps(),
|
||||
title: 'Key Stage Syllabus',
|
||||
__primarylabel__: 'Key Stage Syllabus',
|
||||
ks_syllabus_id: '',
|
||||
ks_syllabus_name: '',
|
||||
ks_syllabus_key_stage: '',
|
||||
ks_syllabus_subject: '',
|
||||
ks_syllabus_subject_code: '',
|
||||
})
|
||||
|
||||
export const getDefaultCCYearGroupSyllabusNodeProps = () => ({
|
||||
...getDefaultBaseProps(),
|
||||
title: 'Year Group Syllabus',
|
||||
__primarylabel__: 'Year Group Syllabus',
|
||||
yr_syllabus_id: '',
|
||||
yr_syllabus_name: '',
|
||||
yr_syllabus_year_group: '',
|
||||
yr_syllabus_subject: '',
|
||||
yr_syllabus_subject_code: '',
|
||||
})
|
||||
|
||||
export const getDefaultCCSubjectNodeProps = () => ({
|
||||
...getDefaultBaseProps(),
|
||||
title: 'Subject',
|
||||
__primarylabel__: 'Subject',
|
||||
subject_code: '',
|
||||
subject_name: '',
|
||||
})
|
||||
|
||||
export const getDefaultCCTopicNodeProps = () => ({
|
||||
...getDefaultBaseProps(),
|
||||
title: 'Topic',
|
||||
__primarylabel__: 'Topic',
|
||||
topic_id: '',
|
||||
topic_title: '',
|
||||
total_number_of_lessons_for_topic: '',
|
||||
topic_type: '',
|
||||
topic_assessment_type: '',
|
||||
})
|
||||
|
||||
export const getDefaultCCTopicLessonNodeProps = () => ({
|
||||
...getDefaultBaseProps(),
|
||||
title: 'Topic Lesson',
|
||||
__primarylabel__: 'Topic Lesson',
|
||||
topic_lesson_id: '',
|
||||
topic_lesson_title: '',
|
||||
topic_lesson_type: '',
|
||||
topic_lesson_length: '',
|
||||
topic_lesson_skills_learned: '',
|
||||
topic_lesson_suggested_activities: '',
|
||||
topic_lesson_weblinks: '',
|
||||
})
|
||||
|
||||
export const getDefaultCCLearningStatementNodeProps = () => ({
|
||||
...getDefaultBaseProps(),
|
||||
title: 'Learning Statement',
|
||||
__primarylabel__: 'Learning Statement',
|
||||
lesson_learning_statement_id: '',
|
||||
lesson_learning_statement: '',
|
||||
lesson_learning_statement_type: '',
|
||||
})
|
||||
|
||||
export const getDefaultCCScienceLabNodeProps = () => ({
|
||||
...getDefaultBaseProps(),
|
||||
title: 'Science Lab',
|
||||
__primarylabel__: 'Science Lab',
|
||||
science_lab_id: '',
|
||||
science_lab_title: '',
|
||||
science_lab_summary: '',
|
||||
science_lab_requirements: '',
|
||||
science_lab_procedure: '',
|
||||
science_lab_safety: '',
|
||||
science_lab_weblinks: '',
|
||||
})
|
||||
|
||||
export const getDefaultCCTeacherTimetableNodeProps = () => ({
|
||||
...getDefaultBaseProps(),
|
||||
title: 'Teacher Timetable',
|
||||
__primarylabel__: 'Teacher Timetable',
|
||||
teacher_id: '',
|
||||
start_date: '',
|
||||
end_date: '',
|
||||
})
|
||||
|
||||
export const getDefaultCCTimetableLessonNodeProps = () => ({
|
||||
...getDefaultBaseProps(),
|
||||
title: 'Timetable Lesson',
|
||||
__primarylabel__: 'Timetable Lesson',
|
||||
subject_class: '',
|
||||
date: '',
|
||||
start_time: '',
|
||||
end_time: '',
|
||||
period_code: '',
|
||||
})
|
||||
|
||||
export const getDefaultCCPlannedLessonNodeProps = () => ({
|
||||
...getDefaultBaseProps(),
|
||||
title: 'Planned Lesson',
|
||||
__primarylabel__: 'Planned Lesson',
|
||||
date: '',
|
||||
start_time: '',
|
||||
end_time: '',
|
||||
period_code: '',
|
||||
subject_class: '',
|
||||
year_group: '',
|
||||
subject: '',
|
||||
teacher_code: '',
|
||||
planning_status: '',
|
||||
topic_code: '',
|
||||
topic_name: '',
|
||||
lesson_code: '',
|
||||
lesson_name: '',
|
||||
learning_statement_codes: '',
|
||||
learning_statements: '',
|
||||
learning_resource_codes: '',
|
||||
learning_resources: '',
|
||||
})
|
||||
|
||||
export const getDefaultCCSchoolTimetableNodeProps = () => ({
|
||||
...getDefaultBaseProps(),
|
||||
title: 'School Timetable',
|
||||
__primarylabel__: 'School Timetable',
|
||||
start_date: '',
|
||||
end_date: '',
|
||||
})
|
||||
|
||||
export const getDefaultCCAcademicYearNodeProps = () => ({
|
||||
...getDefaultBaseProps(),
|
||||
title: 'Academic Year',
|
||||
__primarylabel__: 'Academic Year',
|
||||
year: '',
|
||||
})
|
||||
|
||||
export const getDefaultCCAcademicTermNodeProps = () => ({
|
||||
...getDefaultBaseProps(),
|
||||
title: 'Academic Term',
|
||||
__primarylabel__: 'Academic Term',
|
||||
term_name: '',
|
||||
term_number: '',
|
||||
start_date: '',
|
||||
end_date: '',
|
||||
})
|
||||
|
||||
export const getDefaultCCAcademicWeekNodeProps = () => ({
|
||||
...getDefaultBaseProps(),
|
||||
title: 'Academic Week',
|
||||
__primarylabel__: 'Academic Week',
|
||||
academic_week_number: '',
|
||||
start_date: '',
|
||||
week_type: '',
|
||||
})
|
||||
|
||||
export const getDefaultCCAcademicDayNodeProps = () => ({
|
||||
...getDefaultBaseProps(),
|
||||
title: 'Academic Day',
|
||||
__primarylabel__: 'Academic Day',
|
||||
academic_day: '',
|
||||
date: '',
|
||||
day_of_week: '',
|
||||
day_type: '',
|
||||
})
|
||||
|
||||
export const getDefaultCCAcademicPeriodNodeProps = () => ({
|
||||
...getDefaultBaseProps(),
|
||||
title: 'Academic Period',
|
||||
__primarylabel__: 'Academic Period',
|
||||
name: '',
|
||||
date: '',
|
||||
start_time: '',
|
||||
end_time: '',
|
||||
period_code: '',
|
||||
})
|
||||
|
||||
export const getDefaultCCRegistrationPeriodNodeProps = () => ({
|
||||
...getDefaultBaseProps(),
|
||||
title: 'Registration Period',
|
||||
__primarylabel__: 'Registration Period',
|
||||
name: '',
|
||||
date: '',
|
||||
start_time: '',
|
||||
end_time: '',
|
||||
period_code: '',
|
||||
})
|
||||
|
||||
export const getDefaultCCDepartmentStructureNodeProps = () => ({
|
||||
...getDefaultBaseProps(),
|
||||
title: 'Department Structure',
|
||||
__primarylabel__: 'DepartmentStructure',
|
||||
department_structure_type: '',
|
||||
})
|
||||
|
||||
export const getDefaultCCUserTeacherTimetableNodeProps = () => ({
|
||||
...getDefaultBaseProps(),
|
||||
title: 'User Teacher Timetable',
|
||||
__primarylabel__: 'UserTeacherTimetable',
|
||||
school_db_name: '',
|
||||
school_timetable_id: '',
|
||||
})
|
||||
|
||||
export const getDefaultCCUserTimetableLessonNodeProps = () => ({
|
||||
...getDefaultBaseProps(),
|
||||
title: 'User Timetable Lesson',
|
||||
__primarylabel__: 'UserTimetableLesson',
|
||||
subject_class: '',
|
||||
date: '',
|
||||
start_time: '',
|
||||
end_time: '',
|
||||
period_code: '',
|
||||
school_db_name: '',
|
||||
school_period_id: '',
|
||||
})
|
||||
@@ -0,0 +1,175 @@
|
||||
import { TLShape } from '@tldraw/tldraw'
|
||||
import { CCUserNodeShape, CCUserNodeShapeUtil } from './CCUserNodeShapeUtil'
|
||||
import { CCTeacherNodeShape, CCTeacherNodeShapeUtil } from './CCTeacherNodeShapeUtil'
|
||||
import { CCStudentNodeShape, CCStudentNodeShapeUtil } from './CCStudentNodeShapeUtil'
|
||||
import { CCCalendarNodeShape, CCCalendarNodeShapeUtil } from './CCCalendarNodeShapeUtil'
|
||||
import { CCCalendarYearNodeShape, CCCalendarYearNodeShapeUtil } from './CCCalendarYearNodeShapeUtil'
|
||||
import { CCCalendarMonthNodeShape, CCCalendarMonthNodeShapeUtil } from './CCCalendarMonthNodeShapeUtil'
|
||||
import { CCCalendarWeekNodeShape, CCCalendarWeekNodeShapeUtil } from './CCCalendarWeekNodeShapeUtil'
|
||||
import { CCCalendarDayNodeShape, CCCalendarDayNodeShapeUtil } from './CCCalendarDayNodeShapeUtil'
|
||||
import { CCCalendarTimeChunkNodeShape, CCCalendarTimeChunkNodeShapeUtil } from './CCCalendarTimeChunkNodeShapeUtil'
|
||||
import { CCSchoolNodeShape, CCSchoolNodeShapeUtil } from './CCSchoolNodeShapeUtil'
|
||||
import { CCDepartmentNodeShape, CCDepartmentNodeShapeUtil } from './CCDepartmentNodeShapeUtil'
|
||||
import { CCRoomNodeShape, CCRoomNodeShapeUtil } from './CCRoomNodeShapeUtil'
|
||||
import { CCSubjectClassNodeShape, CCSubjectClassNodeShapeUtil } from './CCSubjectClassNodeShapeUtil'
|
||||
import { CCPastoralStructureNodeShape, CCPastoralStructureNodeShapeUtil } from './CCPastoralStructureNodeShapeUtil'
|
||||
import { CCYearGroupNodeShape, CCYearGroupNodeShapeUtil } from './CCYearGroupNodeShapeUtil'
|
||||
import { CCCurriculumStructureNodeShape, CCCurriculumStructureNodeShapeUtil } from './CCCurriculumStructureNodeShapeUtil'
|
||||
import { CCKeyStageNodeShape, CCKeyStageNodeShapeUtil } from './CCKeyStageNodeShapeUtil'
|
||||
import { CCKeyStageSyllabusNodeShape, CCKeyStageSyllabusNodeShapeUtil } from './CCKeyStageSyllabusNodeShapeUtil'
|
||||
import { CCYearGroupSyllabusNodeShape, CCYearGroupSyllabusNodeShapeUtil } from './CCYearGroupSyllabusNodeShapeUtil'
|
||||
import { CCSubjectNodeShape, CCSubjectNodeShapeUtil } from './CCSubjectNodeShapeUtil'
|
||||
import { CCTopicNodeShape, CCTopicNodeShapeUtil } from './CCTopicNodeShapeUtil'
|
||||
import { CCTopicLessonNodeShape, CCTopicLessonNodeShapeUtil } from './CCTopicLessonNodeShapeUtil'
|
||||
import { CCLearningStatementNodeShape, CCLearningStatementNodeShapeUtil } from './CCLearningStatementNodeShapeUtil'
|
||||
import { CCScienceLabNodeShape, CCScienceLabNodeShapeUtil } from './CCScienceLabNodeShapeUtil'
|
||||
import { CCSchoolTimetableNodeShape, CCSchoolTimetableNodeShapeUtil } from './CCSchoolTimetableNodeShapeUtil'
|
||||
import { CCAcademicYearNodeShape, CCAcademicYearNodeShapeUtil } from './CCAcademicYearNodeShapeUtil'
|
||||
import { CCAcademicTermNodeShape, CCAcademicTermNodeShapeUtil } from './CCAcademicTermNodeShapeUtil'
|
||||
import { CCAcademicWeekNodeShape, CCAcademicWeekNodeShapeUtil } from './CCAcademicWeekNodeShapeUtil'
|
||||
import { CCAcademicDayNodeShape, CCAcademicDayNodeShapeUtil } from './CCAcademicDayNodeShapeUtil'
|
||||
import { CCAcademicPeriodNodeShape, CCAcademicPeriodNodeShapeUtil } from './CCAcademicPeriodNodeShapeUtil'
|
||||
import { CCRegistrationPeriodNodeShape, CCRegistrationPeriodNodeShapeUtil } from './CCRegistrationPeriodNodeShapeUtil'
|
||||
import { CCTeacherTimetableNodeShape, CCTeacherTimetableNodeShapeUtil } from './CCTeacherTimetableNodeShapeUtil'
|
||||
import { CCTimetableLessonNodeShape, CCTimetableLessonNodeShapeUtil } from './CCTimetableLessonNodeShapeUtil'
|
||||
import { CCPlannedLessonNodeShape, CCPlannedLessonNodeShapeUtil } from './CCPlannedLessonNodeShapeUtil'
|
||||
import { CCDepartmentStructureNodeShape, CCDepartmentStructureNodeShapeUtil } from './CCDepartmentStructureNodeShapeUtil'
|
||||
import { CCUserTeacherTimetableNodeShape, CCUserTeacherTimetableNodeShapeUtil } from './CCUserTeacherTimetableNodeShapeUtil'
|
||||
import { CCUserTimetableLessonNodeShape, CCUserTimetableLessonNodeShapeUtil } from './CCUserTimetableLessonNodeShapeUtil'
|
||||
|
||||
// Create a const object with all node types
|
||||
export const NODE_SHAPE_TYPES = {
|
||||
USER: CCUserNodeShapeUtil.type,
|
||||
TEACHER: CCTeacherNodeShapeUtil.type,
|
||||
STUDENT: CCStudentNodeShapeUtil.type,
|
||||
CALENDAR: CCCalendarNodeShapeUtil.type,
|
||||
CALENDAR_YEAR: CCCalendarYearNodeShapeUtil.type,
|
||||
CALENDAR_MONTH: CCCalendarMonthNodeShapeUtil.type,
|
||||
CALENDAR_WEEK: CCCalendarWeekNodeShapeUtil.type,
|
||||
CALENDAR_DAY: CCCalendarDayNodeShapeUtil.type,
|
||||
CALENDAR_TIME_CHUNK: CCCalendarTimeChunkNodeShapeUtil.type,
|
||||
SCHOOL: CCSchoolNodeShapeUtil.type,
|
||||
DEPARTMENT: CCDepartmentNodeShapeUtil.type,
|
||||
ROOM: CCRoomNodeShapeUtil.type,
|
||||
SUBJECT_CLASS: CCSubjectClassNodeShapeUtil.type,
|
||||
PASTORAL_STRUCTURE: CCPastoralStructureNodeShapeUtil.type,
|
||||
YEAR_GROUP: CCYearGroupNodeShapeUtil.type,
|
||||
CURRICULUM_STRUCTURE: CCCurriculumStructureNodeShapeUtil.type,
|
||||
KEY_STAGE: CCKeyStageNodeShapeUtil.type,
|
||||
KEY_STAGE_SYLLABUS: CCKeyStageSyllabusNodeShapeUtil.type,
|
||||
YEAR_GROUP_SYLLABUS: CCYearGroupSyllabusNodeShapeUtil.type,
|
||||
SUBJECT: CCSubjectNodeShapeUtil.type,
|
||||
TOPIC: CCTopicNodeShapeUtil.type,
|
||||
TOPIC_LESSON: CCTopicLessonNodeShapeUtil.type,
|
||||
LEARNING_STATEMENT: CCLearningStatementNodeShapeUtil.type,
|
||||
SCIENCE_LAB: CCScienceLabNodeShapeUtil.type,
|
||||
SCHOOL_TIMETABLE: CCSchoolTimetableNodeShapeUtil.type,
|
||||
ACADEMIC_YEAR: CCAcademicYearNodeShapeUtil.type,
|
||||
ACADEMIC_TERM: CCAcademicTermNodeShapeUtil.type,
|
||||
ACADEMIC_WEEK: CCAcademicWeekNodeShapeUtil.type,
|
||||
ACADEMIC_DAY: CCAcademicDayNodeShapeUtil.type,
|
||||
ACADEMIC_PERIOD: CCAcademicPeriodNodeShapeUtil.type,
|
||||
REGISTRATION_PERIOD: CCRegistrationPeriodNodeShapeUtil.type,
|
||||
TEACHER_TIMETABLE: CCTeacherTimetableNodeShapeUtil.type,
|
||||
TIMETABLE_LESSON: CCTimetableLessonNodeShapeUtil.type,
|
||||
PLANNED_LESSON: CCPlannedLessonNodeShapeUtil.type,
|
||||
DEPARTMENT_STRUCTURE: CCDepartmentStructureNodeShapeUtil.type,
|
||||
USER_TEACHER_TIMETABLE: CCUserTeacherTimetableNodeShapeUtil.type,
|
||||
USER_TIMETABLE_LESSON: CCUserTimetableLessonNodeShapeUtil.type,
|
||||
} as const;
|
||||
|
||||
// Create the type from the const object's values
|
||||
export type NodeShapeType = typeof NODE_SHAPE_TYPES[keyof typeof NODE_SHAPE_TYPES];
|
||||
|
||||
// Define AllNodeShapes as a union type of all shape types
|
||||
export type AllNodeShapes =
|
||||
| CCUserNodeShape
|
||||
| CCTeacherNodeShape
|
||||
| CCStudentNodeShape
|
||||
| CCCalendarNodeShape
|
||||
| CCCalendarYearNodeShape
|
||||
| CCCalendarMonthNodeShape
|
||||
| CCCalendarWeekNodeShape
|
||||
| CCCalendarDayNodeShape
|
||||
| CCCalendarTimeChunkNodeShape
|
||||
| CCSchoolNodeShape
|
||||
| CCDepartmentNodeShape
|
||||
| CCRoomNodeShape
|
||||
| CCSubjectClassNodeShape
|
||||
| CCPastoralStructureNodeShape
|
||||
| CCYearGroupNodeShape
|
||||
| CCCurriculumStructureNodeShape
|
||||
| CCKeyStageNodeShape
|
||||
| CCKeyStageSyllabusNodeShape
|
||||
| CCYearGroupSyllabusNodeShape
|
||||
| CCSubjectNodeShape
|
||||
| CCTopicNodeShape
|
||||
| CCTopicLessonNodeShape
|
||||
| CCLearningStatementNodeShape
|
||||
| CCScienceLabNodeShape
|
||||
| CCSchoolTimetableNodeShape
|
||||
| CCAcademicYearNodeShape
|
||||
| CCAcademicTermNodeShape
|
||||
| CCAcademicWeekNodeShape
|
||||
| CCAcademicDayNodeShape
|
||||
| CCAcademicPeriodNodeShape
|
||||
| CCRegistrationPeriodNodeShape
|
||||
| CCTeacherTimetableNodeShape
|
||||
| CCTimetableLessonNodeShape
|
||||
| CCPlannedLessonNodeShape
|
||||
| CCDepartmentStructureNodeShape
|
||||
| CCUserTeacherTimetableNodeShape
|
||||
| CCUserTimetableLessonNodeShape;
|
||||
|
||||
// Export all shape utils in an object for easy access
|
||||
export const ShapeUtils = {
|
||||
[CCUserNodeShapeUtil.type]: CCUserNodeShapeUtil,
|
||||
[CCTeacherNodeShapeUtil.type]: CCTeacherNodeShapeUtil,
|
||||
[CCStudentNodeShapeUtil.type]: CCStudentNodeShapeUtil,
|
||||
[CCCalendarNodeShapeUtil.type]: CCCalendarNodeShapeUtil,
|
||||
[CCCalendarYearNodeShapeUtil.type]: CCCalendarYearNodeShapeUtil,
|
||||
[CCCalendarMonthNodeShapeUtil.type]: CCCalendarMonthNodeShapeUtil,
|
||||
[CCCalendarWeekNodeShapeUtil.type]: CCCalendarWeekNodeShapeUtil,
|
||||
[CCCalendarDayNodeShapeUtil.type]: CCCalendarDayNodeShapeUtil,
|
||||
[CCCalendarTimeChunkNodeShapeUtil.type]: CCCalendarTimeChunkNodeShapeUtil,
|
||||
[CCSchoolNodeShapeUtil.type]: CCSchoolNodeShapeUtil,
|
||||
[CCDepartmentNodeShapeUtil.type]: CCDepartmentNodeShapeUtil,
|
||||
[CCRoomNodeShapeUtil.type]: CCRoomNodeShapeUtil,
|
||||
[CCSubjectClassNodeShapeUtil.type]: CCSubjectClassNodeShapeUtil,
|
||||
[CCPastoralStructureNodeShapeUtil.type]: CCPastoralStructureNodeShapeUtil,
|
||||
[CCYearGroupNodeShapeUtil.type]: CCYearGroupNodeShapeUtil,
|
||||
[CCCurriculumStructureNodeShapeUtil.type]: CCCurriculumStructureNodeShapeUtil,
|
||||
[CCKeyStageNodeShapeUtil.type]: CCKeyStageNodeShapeUtil,
|
||||
[CCKeyStageSyllabusNodeShapeUtil.type]: CCKeyStageSyllabusNodeShapeUtil,
|
||||
[CCYearGroupSyllabusNodeShapeUtil.type]: CCYearGroupSyllabusNodeShapeUtil,
|
||||
[CCSubjectNodeShapeUtil.type]: CCSubjectNodeShapeUtil,
|
||||
[CCTopicNodeShapeUtil.type]: CCTopicNodeShapeUtil,
|
||||
[CCTopicLessonNodeShapeUtil.type]: CCTopicLessonNodeShapeUtil,
|
||||
[CCLearningStatementNodeShapeUtil.type]: CCLearningStatementNodeShapeUtil,
|
||||
[CCScienceLabNodeShapeUtil.type]: CCScienceLabNodeShapeUtil,
|
||||
[CCSchoolTimetableNodeShapeUtil.type]: CCSchoolTimetableNodeShapeUtil,
|
||||
[CCAcademicYearNodeShapeUtil.type]: CCAcademicYearNodeShapeUtil,
|
||||
[CCAcademicTermNodeShapeUtil.type]: CCAcademicTermNodeShapeUtil,
|
||||
[CCAcademicWeekNodeShapeUtil.type]: CCAcademicWeekNodeShapeUtil,
|
||||
[CCAcademicDayNodeShapeUtil.type]: CCAcademicDayNodeShapeUtil,
|
||||
[CCAcademicPeriodNodeShapeUtil.type]: CCAcademicPeriodNodeShapeUtil,
|
||||
[CCRegistrationPeriodNodeShapeUtil.type]: CCRegistrationPeriodNodeShapeUtil,
|
||||
[CCTeacherTimetableNodeShapeUtil.type]: CCTeacherTimetableNodeShapeUtil,
|
||||
[CCTimetableLessonNodeShapeUtil.type]: CCTimetableLessonNodeShapeUtil,
|
||||
[CCPlannedLessonNodeShapeUtil.type]: CCPlannedLessonNodeShapeUtil,
|
||||
[CCDepartmentStructureNodeShapeUtil.type]: CCDepartmentStructureNodeShapeUtil,
|
||||
[CCUserTeacherTimetableNodeShapeUtil.type]: CCUserTeacherTimetableNodeShapeUtil,
|
||||
[CCUserTimetableLessonNodeShapeUtil.type]: CCUserTimetableLessonNodeShapeUtil,
|
||||
} as const;
|
||||
|
||||
// Add a type guard to check if a shape is a valid node shape
|
||||
export const isValidNodeShape = (shape: TLShape): shape is AllNodeShapes => {
|
||||
return shape &&
|
||||
typeof shape.type === 'string' &&
|
||||
Object.values(NODE_SHAPE_TYPES).includes(shape.type as NodeShapeType);
|
||||
};
|
||||
|
||||
// Add a type guard to check if a type string is a valid node type
|
||||
export const isValidNodeType = (type: string): type is NodeShapeType => {
|
||||
return Object.values(NODE_SHAPE_TYPES).includes(type as NodeShapeType);
|
||||
};
|
||||
@@ -0,0 +1,236 @@
|
||||
import React, { CSSProperties } from 'react';
|
||||
import { ShapeState } from './cc-graph-types'
|
||||
import { SHARED_NODE_STYLES } from './cc-graph-styles'
|
||||
import { logger } from '../../../../debugConfig'
|
||||
|
||||
interface Neo4jDate {
|
||||
_Date__year: number;
|
||||
_Date__month: number;
|
||||
_Date__day: number;
|
||||
_Date__ordinal?: number;
|
||||
}
|
||||
|
||||
interface Neo4jTime {
|
||||
_Time__hour: number;
|
||||
_Time__minute: number;
|
||||
_Time__second: number;
|
||||
_Time__nanosecond: number;
|
||||
_Time__ticks: number;
|
||||
_Time__tzinfo: null;
|
||||
}
|
||||
|
||||
interface Neo4jDateTime {
|
||||
_DateTime__date: Neo4jDate;
|
||||
_DateTime__time: Neo4jTime;
|
||||
}
|
||||
|
||||
export type DateValue = string | Date | Neo4jDate | Neo4jDateTime | null | undefined;
|
||||
|
||||
const isValidDate = (year: number | undefined, month: number | undefined, day: number | undefined): boolean => {
|
||||
if (typeof year !== 'number' || typeof month !== 'number' || typeof day !== 'number') return false;
|
||||
if (month < 1 || month > 12 || day < -31 || day > 31) return false;
|
||||
// Handle special case for end of month markers
|
||||
if (day < 0) return true;
|
||||
const date = new Date(year, month - 1, day);
|
||||
return date.getMonth() === month - 1 && date.getDate() === day;
|
||||
};
|
||||
|
||||
// Date formatting utility for Neo4j date objects and other formats
|
||||
export const formatDate = (dateValue: DateValue): string => {
|
||||
if (!dateValue) return '';
|
||||
|
||||
// Handle string dates
|
||||
if (typeof dateValue === 'string') return dateValue;
|
||||
|
||||
// Handle standard Date objects
|
||||
if (dateValue instanceof Date) {
|
||||
return dateValue.toISOString().split('T')[0];
|
||||
}
|
||||
|
||||
// Handle Neo4j DateTime objects
|
||||
if (typeof dateValue === 'object' && '_DateTime__date' in dateValue) {
|
||||
const nestedDate = dateValue._DateTime__date as Neo4jDate;
|
||||
return formatDate(nestedDate);
|
||||
}
|
||||
|
||||
// Handle Neo4j Date objects and raw date objects
|
||||
if (typeof dateValue === 'object' && '_Date__year' in dateValue) {
|
||||
const year = dateValue._Date__year;
|
||||
const month = dateValue._Date__month;
|
||||
const day = dateValue._Date__day;
|
||||
|
||||
if (isValidDate(year, month, day)) {
|
||||
try {
|
||||
// Handle negative days (end of month markers)
|
||||
if (typeof day === 'number' && day < 0 && typeof year === 'number' && typeof month === 'number') {
|
||||
const nextMonth = new Date(year, month, 1);
|
||||
nextMonth.setDate(nextMonth.getDate() - 1);
|
||||
return nextMonth.toISOString().split('T')[0];
|
||||
}
|
||||
if (typeof year === 'number' && typeof month === 'number' && typeof day === 'number') {
|
||||
const date = new Date(year, month - 1, day);
|
||||
return date.toISOString().split('T')[0];
|
||||
}
|
||||
} catch {
|
||||
logger.warn('graph-shape-shared', '⚠️ Failed to format Neo4j date', { dateValue });
|
||||
}
|
||||
}
|
||||
return 'Invalid Date';
|
||||
}
|
||||
|
||||
// For any other object, try to convert it
|
||||
try {
|
||||
const date = new Date(String(dateValue));
|
||||
if (!isNaN(date.getTime())) {
|
||||
return date.toISOString().split('T')[0];
|
||||
}
|
||||
} catch {
|
||||
logger.warn('graph-shape-shared', '⚠️ Failed to format unknown date type', { dateValue });
|
||||
}
|
||||
|
||||
return 'Invalid Date';
|
||||
}
|
||||
|
||||
// Error display component for nodes
|
||||
interface NodeErrorDisplayProps {
|
||||
message: string;
|
||||
details?: string;
|
||||
style?: CSSProperties;
|
||||
}
|
||||
|
||||
interface ErrorResult {
|
||||
hasError: true;
|
||||
error: {
|
||||
message: string;
|
||||
details: string;
|
||||
};
|
||||
}
|
||||
|
||||
interface SuccessResult {
|
||||
hasError: false;
|
||||
}
|
||||
|
||||
type ValidationResult = ErrorResult | SuccessResult;
|
||||
|
||||
export const NodeErrorDisplay: React.FC<NodeErrorDisplayProps> = ({
|
||||
message,
|
||||
details,
|
||||
style
|
||||
}) => (
|
||||
<div style={{ ...SHARED_NODE_STYLES.error.container, ...style }}>
|
||||
<div style={SHARED_NODE_STYLES.error.message}>{message}</div>
|
||||
{details && <div style={SHARED_NODE_STYLES.error.details}>{details}</div>}
|
||||
</div>
|
||||
);
|
||||
|
||||
// Error checking utilities
|
||||
export const checkShapeState = (state: ShapeState | undefined | null): ValidationResult => {
|
||||
if (!state) {
|
||||
logger.warn('graph-shape-shared', '⚠️ Missing shape state', { state })
|
||||
return {
|
||||
hasError: true,
|
||||
error: {
|
||||
message: "Invalid Shape State",
|
||||
details: "Shape state is missing or undefined"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (state.isPageChild === null) {
|
||||
logger.warn('graph-shape-shared', '⚠️ Invalid page child state', { state })
|
||||
return {
|
||||
hasError: true,
|
||||
error: {
|
||||
message: "Invalid Page State",
|
||||
details: "Unable to determine if node is a page child"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (state.isPageChild === false && state.parentId === null) {
|
||||
logger.warn('graph-shape-shared', '⚠️ Shape child with no parent', { state })
|
||||
return {
|
||||
hasError: true,
|
||||
error: {
|
||||
message: "Invalid Shape Child State",
|
||||
details: "Shape child with no parent"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return { hasError: false }
|
||||
}
|
||||
|
||||
export const checkDefaultComponent = (defaultComponent: boolean | { action: { label: string; handler: () => void } } | undefined | null): ValidationResult => {
|
||||
if (defaultComponent === null) {
|
||||
logger.warn('graph-shape-shared', '⚠️ Invalid default component', { defaultComponent })
|
||||
return {
|
||||
hasError: true,
|
||||
error: {
|
||||
message: "Invalid Default Component",
|
||||
details: "Default component is not set"
|
||||
}
|
||||
}
|
||||
}
|
||||
return { hasError: false }
|
||||
}
|
||||
|
||||
// Base component for all graph nodes
|
||||
interface DefaultNodeComponentProps {
|
||||
tldraw_snapshot: string
|
||||
onInspect?: (tldraw_snapshot: string) => void
|
||||
customAction?: {
|
||||
label: string
|
||||
handler: () => void
|
||||
}
|
||||
}
|
||||
|
||||
export const DefaultNodeComponent: React.FC<DefaultNodeComponentProps> = ({
|
||||
tldraw_snapshot,
|
||||
onInspect = () => console.log(`Inspecting node at path: ${tldraw_snapshot}`),
|
||||
customAction
|
||||
}) => {
|
||||
return (
|
||||
<div style={SHARED_NODE_STYLES.defaultComponent.container}>
|
||||
<button style={SHARED_NODE_STYLES.defaultComponent.button} onClick={() => onInspect(tldraw_snapshot)}>
|
||||
Inspect
|
||||
</button>
|
||||
{customAction && (
|
||||
<button style={SHARED_NODE_STYLES.defaultComponent.button} onClick={customAction.handler}>
|
||||
{customAction.label}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// Helper function to create a node content wrapper
|
||||
export const NodeContentWrapper = ({
|
||||
children,
|
||||
style
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
style: CSSProperties;
|
||||
}) => (
|
||||
<div style={{ ...SHARED_NODE_STYLES.container, ...style }}>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
|
||||
// Helper function to create a node property display
|
||||
export const NodeProperty = ({
|
||||
label,
|
||||
value,
|
||||
labelStyle,
|
||||
valueStyle
|
||||
}: {
|
||||
label: string;
|
||||
value: string;
|
||||
labelStyle: CSSProperties;
|
||||
valueStyle: CSSProperties;
|
||||
}) => (
|
||||
<div style={SHARED_NODE_STYLES.property.wrapper}>
|
||||
<span style={{ ...SHARED_NODE_STYLES.property.label, ...labelStyle }}>{label}:</span>
|
||||
<span style={{ ...SHARED_NODE_STYLES.property.value, ...valueStyle }}>{value}</span>
|
||||
</div>
|
||||
);
|
||||
@@ -0,0 +1,176 @@
|
||||
// Shared styles for all nodes
|
||||
export const SHARED_NODE_STYLES = {
|
||||
container: {
|
||||
display: 'flex',
|
||||
flexDirection: 'column' as const,
|
||||
padding: '8px',
|
||||
gap: '4px',
|
||||
backgroundColor: 'var(--color-muted)',
|
||||
color: 'var(--color-text)',
|
||||
borderRadius: '4px',
|
||||
minWidth: '150px',
|
||||
},
|
||||
header: {
|
||||
fontSize: '14px',
|
||||
fontWeight: 'bold' as const,
|
||||
marginBottom: '4px',
|
||||
color: 'var(--color-text)',
|
||||
},
|
||||
property: {
|
||||
label: {
|
||||
fontSize: '12px',
|
||||
color: 'var(--color-text-2)',
|
||||
marginRight: '4px',
|
||||
fontWeight: '500' as const,
|
||||
},
|
||||
value: {
|
||||
fontSize: '12px',
|
||||
color: 'var(--color-text)',
|
||||
fontWeight: '200' as const,
|
||||
},
|
||||
wrapper: {
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: '4px',
|
||||
},
|
||||
},
|
||||
error: {
|
||||
container: {
|
||||
padding: '8px',
|
||||
backgroundColor: 'var(--color-error)',
|
||||
color: 'white',
|
||||
borderRadius: '4px',
|
||||
fontSize: '12px',
|
||||
},
|
||||
message: {
|
||||
fontWeight: 'bold' as const,
|
||||
},
|
||||
details: {
|
||||
marginTop: '4px',
|
||||
opacity: 0.8,
|
||||
}
|
||||
},
|
||||
defaultComponent: {
|
||||
container: {
|
||||
display: 'flex',
|
||||
gap: '8px',
|
||||
marginBottom: '8px',
|
||||
},
|
||||
button: {
|
||||
padding: '4px 8px',
|
||||
fontSize: '12px',
|
||||
borderRadius: '4px',
|
||||
backgroundColor: 'var(--color-muted-2)',
|
||||
color: 'var(--color-text)',
|
||||
border: 'none',
|
||||
cursor: 'pointer',
|
||||
'&:hover': {
|
||||
backgroundColor: 'var(--color-muted-3)',
|
||||
}
|
||||
}
|
||||
}
|
||||
} as const
|
||||
|
||||
// Color themes for different node types
|
||||
export const NODE_THEMES = {
|
||||
calendar: {
|
||||
headerColor: '#0066cc',
|
||||
backgroundColor: '#e6f0ff',
|
||||
},
|
||||
academic: {
|
||||
headerColor: '#008000',
|
||||
backgroundColor: '#e6ffe6',
|
||||
},
|
||||
curriculum: {
|
||||
headerColor: '#ff8c00',
|
||||
backgroundColor: '#fff3e6',
|
||||
},
|
||||
pastoral: {
|
||||
headerColor: '#8a2be2',
|
||||
backgroundColor: '#f5e6ff',
|
||||
},
|
||||
people: {
|
||||
headerColor: '#cc0000',
|
||||
backgroundColor: '#ffe6e6',
|
||||
},
|
||||
resource: {
|
||||
headerColor: '#cccc00',
|
||||
backgroundColor: '#fffff0',
|
||||
},
|
||||
} as const
|
||||
|
||||
// Node type to theme mapping
|
||||
export const NODE_TYPE_THEMES: Record<string, keyof typeof NODE_THEMES> = {
|
||||
// Calendar nodes
|
||||
'cc-calendar-node': 'calendar',
|
||||
'cc-calendar-year-node': 'calendar',
|
||||
'cc-calendar-month-node': 'calendar',
|
||||
'cc-calendar-week-node': 'calendar',
|
||||
'cc-calendar-day-node': 'calendar',
|
||||
'cc-calendar-time-chunk-node': 'calendar',
|
||||
|
||||
// Academic nodes
|
||||
'cc-academic-year-node': 'academic',
|
||||
'cc-academic-term-node': 'academic',
|
||||
'cc-academic-week-node': 'academic',
|
||||
'cc-academic-day-node': 'academic',
|
||||
'cc-academic-period-node': 'academic',
|
||||
'cc-registration-period-node': 'academic',
|
||||
'cc-timetable-lesson-node': 'academic',
|
||||
'cc-planned-lesson-node': 'academic',
|
||||
'cc-school-timetable-node': 'academic',
|
||||
'cc-user-teacher-timetable-node': 'academic',
|
||||
'cc-user-timetable-lesson-node': 'academic',
|
||||
|
||||
// Curriculum nodes
|
||||
'cc-curriculum-structure-node': 'curriculum',
|
||||
'cc-key-stage-node': 'curriculum',
|
||||
'cc-key-stage-syllabus-node': 'curriculum',
|
||||
|
||||
'cc-year-group-syllabus-node': 'curriculum',
|
||||
'cc-subject-node': 'curriculum',
|
||||
'cc-topic-node': 'curriculum',
|
||||
'cc-topic-lesson-node': 'curriculum',
|
||||
'cc-learning-statement-node': 'curriculum',
|
||||
'cc-science-lab-node': 'curriculum',
|
||||
|
||||
// Pastoral nodes
|
||||
'cc-pastoral-structure-node': 'pastoral',
|
||||
'cc-year-group-node': 'pastoral',
|
||||
|
||||
// People nodes
|
||||
'cc-user-node': 'people',
|
||||
'cc-teacher-node': 'people',
|
||||
'cc-student-node': 'people',
|
||||
|
||||
// Resource nodes
|
||||
'cc-school-node': 'resource',
|
||||
'cc-department-node': 'resource',
|
||||
'cc-room-node': 'resource',
|
||||
'cc-subject-class-node': 'resource',
|
||||
} as const
|
||||
|
||||
// Helper function to get theme for a node type
|
||||
export const getNodeTheme = (nodeType: string) => {
|
||||
const themeKey = NODE_TYPE_THEMES[nodeType]
|
||||
return themeKey ? NODE_THEMES[themeKey] : NODE_THEMES.resource // Default to resource theme
|
||||
}
|
||||
|
||||
// Helper function to get theme from primary label
|
||||
export const getThemeFromLabel = (primaryLabel: string) => {
|
||||
// Convert primary label to node type format (e.g., 'User' -> 'cc-user-node')
|
||||
const nodeType = `cc-${primaryLabel.toLowerCase()}-node`;
|
||||
return getNodeTheme(nodeType);
|
||||
}
|
||||
|
||||
// Helper function to get styles for a specific node type
|
||||
export const getNodeStyles = (nodeType: string) => {
|
||||
const theme = getNodeTheme(nodeType)
|
||||
return {
|
||||
...SHARED_NODE_STYLES,
|
||||
container: {
|
||||
...SHARED_NODE_STYLES.container,
|
||||
backgroundColor: theme.backgroundColor,
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,389 @@
|
||||
import { TLBinding, TLBaseShape, TLShapeId } from '@tldraw/tldraw'
|
||||
import { CCBaseShape } from '../cc-types'
|
||||
import { CCBaseProps } from '../cc-props'
|
||||
import { ccGraphShapeProps } from './cc-graph-props'
|
||||
|
||||
// Export type for graph shape types
|
||||
export type GraphShapeType = keyof typeof ccGraphShapeProps
|
||||
|
||||
export interface ShapeState {
|
||||
parentId: TLShapeId | null
|
||||
isPageChild: boolean | null
|
||||
hasChildren: boolean | null
|
||||
bindings: TLBinding[] | null
|
||||
}
|
||||
|
||||
export type CCGraphShapeProps = CCBaseProps & {
|
||||
__primarylabel__: string
|
||||
unique_id: string
|
||||
tldraw_snapshot: string
|
||||
created: string
|
||||
merged: string
|
||||
state: ShapeState | null | undefined
|
||||
defaultComponent: boolean | null
|
||||
}
|
||||
|
||||
// Define the base shape type for graph shapes
|
||||
export type CCGraphShape = CCBaseShape & TLBaseShape<GraphShapeType, {
|
||||
__primarylabel__: CCGraphShapeProps['__primarylabel__']
|
||||
unique_id: CCGraphShapeProps['unique_id']
|
||||
tldraw_snapshot: CCGraphShapeProps['tldraw_snapshot']
|
||||
created: CCGraphShapeProps['created']
|
||||
merged: CCGraphShapeProps['merged']
|
||||
state: CCGraphShapeProps['state']
|
||||
defaultComponent: CCGraphShapeProps['defaultComponent']
|
||||
}>
|
||||
|
||||
export type CCUserNodeProps = CCGraphShapeProps & {
|
||||
user_name: string
|
||||
user_email: string
|
||||
user_type: string
|
||||
user_id: string
|
||||
worker_node_data: string
|
||||
}
|
||||
|
||||
export type CCTeacherNodeProps = CCGraphShapeProps & {
|
||||
teacher_code: string
|
||||
teacher_name_formal: string
|
||||
teacher_email: string
|
||||
user_db_name: string
|
||||
school_db_name: string
|
||||
}
|
||||
|
||||
export type CCStudentNodeProps = CCGraphShapeProps & {
|
||||
student_name_formal: string
|
||||
student_code: string
|
||||
student_email: string
|
||||
user_db_name: string
|
||||
school_db_name: string
|
||||
}
|
||||
|
||||
export type CCCalendarNodeProps = CCGraphShapeProps & {
|
||||
title: string
|
||||
name: string
|
||||
calendar_type: string
|
||||
calendar_name: string
|
||||
start_date: string
|
||||
end_date: string
|
||||
}
|
||||
|
||||
export type CCCalendarYearNodeProps = CCGraphShapeProps & {
|
||||
year: string
|
||||
}
|
||||
|
||||
export type CCCalendarMonthNodeProps = CCGraphShapeProps & {
|
||||
year: string
|
||||
month: string
|
||||
month_name: string
|
||||
}
|
||||
|
||||
export type CCCalendarWeekNodeProps = CCGraphShapeProps & {
|
||||
start_date: string
|
||||
week_number: string
|
||||
iso_week: string
|
||||
}
|
||||
|
||||
export type CCCalendarDayNodeProps = CCGraphShapeProps & {
|
||||
date: string
|
||||
day_of_week: string
|
||||
iso_day: string
|
||||
}
|
||||
|
||||
export type CCCalendarTimeChunkNodeProps = CCGraphShapeProps & {
|
||||
start_time: string
|
||||
end_time: string
|
||||
}
|
||||
|
||||
export type CCSchoolNodeProps = CCGraphShapeProps & {
|
||||
school_uuid: string
|
||||
school_name: string
|
||||
school_website: string
|
||||
}
|
||||
|
||||
export type CCDepartmentNodeProps = CCGraphShapeProps & {
|
||||
department_name: string
|
||||
}
|
||||
|
||||
export type CCRoomNodeProps = CCGraphShapeProps & {
|
||||
room_code: string
|
||||
room_name: string
|
||||
}
|
||||
|
||||
export type CCSubjectClassNodeProps = CCGraphShapeProps & {
|
||||
subject_class_code: string
|
||||
year_group: string
|
||||
subject: string
|
||||
subject_code: string
|
||||
}
|
||||
|
||||
export type CCPastoralStructureNodeProps = CCGraphShapeProps & {
|
||||
pastoral_structure_type: string
|
||||
}
|
||||
|
||||
export type CCYearGroupNodeProps = CCGraphShapeProps & {
|
||||
year_group: string
|
||||
year_group_name: string
|
||||
}
|
||||
|
||||
export type CCCurriculumStructureNodeProps = CCGraphShapeProps & {
|
||||
curriculum_structure_type: string
|
||||
}
|
||||
|
||||
export type CCKeyStageNodeProps = CCGraphShapeProps & {
|
||||
key_stage_name: string
|
||||
key_stage: string
|
||||
}
|
||||
|
||||
export type CCKeyStageSyllabusNodeProps = CCGraphShapeProps & {
|
||||
ks_syllabus_id: string
|
||||
ks_syllabus_name: string
|
||||
ks_syllabus_key_stage: string
|
||||
ks_syllabus_subject: string
|
||||
ks_syllabus_subject_code: string
|
||||
}
|
||||
|
||||
export type CCYearGroupSyllabusNodeProps = CCGraphShapeProps & {
|
||||
yr_syllabus_id: string
|
||||
yr_syllabus_name: string
|
||||
yr_syllabus_year_group: string
|
||||
yr_syllabus_subject: string
|
||||
yr_syllabus_subject_code: string
|
||||
}
|
||||
|
||||
export type CCSubjectNodeProps = CCGraphShapeProps & {
|
||||
subject_code: string
|
||||
subject_name: string
|
||||
}
|
||||
|
||||
export type CCTopicNodeProps = CCGraphShapeProps & {
|
||||
topic_id: string
|
||||
topic_title: string
|
||||
total_number_of_lessons_for_topic: string
|
||||
topic_type: string
|
||||
topic_assessment_type: string
|
||||
}
|
||||
|
||||
export type CCTopicLessonNodeProps = CCGraphShapeProps & {
|
||||
topic_lesson_id: string
|
||||
topic_lesson_title: string
|
||||
topic_lesson_type: string
|
||||
topic_lesson_length: string
|
||||
topic_lesson_skills_learned: string
|
||||
topic_lesson_suggested_activities: string
|
||||
topic_lesson_weblinks: string
|
||||
}
|
||||
|
||||
export type CCLearningStatementNodeProps = CCGraphShapeProps & {
|
||||
lesson_learning_statement_id: string
|
||||
lesson_learning_statement: string
|
||||
lesson_learning_statement_type: string
|
||||
}
|
||||
|
||||
export type CCScienceLabNodeProps = CCGraphShapeProps & {
|
||||
science_lab_id: string
|
||||
science_lab_title: string
|
||||
science_lab_summary: string
|
||||
science_lab_requirements: string
|
||||
science_lab_procedure: string
|
||||
science_lab_safety: string
|
||||
science_lab_weblinks: string
|
||||
}
|
||||
|
||||
export type CCTeacherTimetableNodeProps = CCGraphShapeProps & {
|
||||
teacher_id: string
|
||||
start_date: string
|
||||
end_date: string
|
||||
}
|
||||
|
||||
export type CCTimetableLessonNodeProps = CCGraphShapeProps & {
|
||||
subject_class: string
|
||||
date: string
|
||||
start_time: string
|
||||
end_time: string
|
||||
period_code: string
|
||||
}
|
||||
|
||||
export type CCPlannedLessonNodeProps = CCGraphShapeProps & {
|
||||
date: string
|
||||
start_time: string
|
||||
end_time: string
|
||||
period_code: string
|
||||
subject_class: string
|
||||
year_group: string
|
||||
subject: string
|
||||
teacher_code: string
|
||||
planning_status: string
|
||||
topic_code: string
|
||||
topic_name: string
|
||||
lesson_code: string
|
||||
lesson_name: string
|
||||
learning_statement_codes: string
|
||||
learning_statements: string
|
||||
learning_resource_codes: string
|
||||
learning_resources: string
|
||||
}
|
||||
|
||||
export type CCSchoolTimetableNodeProps = CCGraphShapeProps & {
|
||||
start_date: string
|
||||
end_date: string
|
||||
}
|
||||
|
||||
export type CCAcademicYearNodeProps = CCGraphShapeProps & {
|
||||
year: string
|
||||
}
|
||||
|
||||
export type CCAcademicTermNodeProps = CCGraphShapeProps & {
|
||||
term_name: string
|
||||
term_number: string
|
||||
start_date: string
|
||||
end_date: string
|
||||
}
|
||||
|
||||
export type CCAcademicWeekNodeProps = CCGraphShapeProps & {
|
||||
academic_week_number: string
|
||||
start_date: string
|
||||
week_type: string
|
||||
}
|
||||
|
||||
export type CCAcademicDayNodeProps = CCGraphShapeProps & {
|
||||
academic_day: string
|
||||
date: string
|
||||
day_of_week: string
|
||||
day_type: string
|
||||
}
|
||||
|
||||
export type CCAcademicPeriodNodeProps = CCGraphShapeProps & {
|
||||
name: string
|
||||
date: string
|
||||
start_time: string
|
||||
end_time: string
|
||||
period_code: string
|
||||
}
|
||||
|
||||
export type CCRegistrationPeriodNodeProps = CCGraphShapeProps & {
|
||||
name: string
|
||||
date: string
|
||||
start_time: string
|
||||
end_time: string
|
||||
period_code: string
|
||||
}
|
||||
|
||||
export type CCDepartmentStructureNodeProps = CCGraphShapeProps & {
|
||||
department_structure_type: string
|
||||
}
|
||||
|
||||
export type CCUserTeacherTimetableNodeProps = CCGraphShapeProps & {
|
||||
school_db_name: string
|
||||
school_timetable_id: string
|
||||
}
|
||||
|
||||
export type CCUserTimetableLessonNodeProps = CCGraphShapeProps & {
|
||||
subject_class: string
|
||||
date: string
|
||||
start_time: string
|
||||
end_time: string
|
||||
period_code: string
|
||||
school_db_name: string
|
||||
school_period_id: string
|
||||
}
|
||||
|
||||
// Define a type-safe mapping of node types to their configurations
|
||||
export type CCNodeTypes = {
|
||||
User: { props: CCUserNodeProps }
|
||||
Developer: { props: CCUserNodeProps }
|
||||
Teacher: { props: CCTeacherNodeProps }
|
||||
Student: { props: CCStudentNodeProps }
|
||||
Calendar: { props: CCCalendarNodeProps }
|
||||
TeacherTimetable: { props: CCTeacherTimetableNodeProps }
|
||||
TimetableLesson: { props: CCTimetableLessonNodeProps }
|
||||
PlannedLesson: { props: CCPlannedLessonNodeProps }
|
||||
School: { props: CCSchoolNodeProps }
|
||||
CalendarYear: { props: CCCalendarYearNodeProps }
|
||||
CalendarMonth: { props: CCCalendarMonthNodeProps }
|
||||
CalendarWeek: { props: CCCalendarWeekNodeProps }
|
||||
CalendarDay: { props: CCCalendarDayNodeProps }
|
||||
CalendarTimeChunk: { props: CCCalendarTimeChunkNodeProps }
|
||||
ScienceLab: { props: CCScienceLabNodeProps }
|
||||
KeyStageSyllabus: { props: CCKeyStageSyllabusNodeProps }
|
||||
YearGroupSyllabus: { props: CCYearGroupSyllabusNodeProps }
|
||||
CurriculumStructure: { props: CCCurriculumStructureNodeProps }
|
||||
Topic: { props: CCTopicNodeProps }
|
||||
TopicLesson: { props: CCTopicLessonNodeProps }
|
||||
LearningStatement: { props: CCLearningStatementNodeProps }
|
||||
SchoolTimetable: { props: CCSchoolTimetableNodeProps }
|
||||
AcademicYear: { props: CCAcademicYearNodeProps }
|
||||
AcademicTerm: { props: CCAcademicTermNodeProps }
|
||||
AcademicWeek: { props: CCAcademicWeekNodeProps }
|
||||
AcademicDay: { props: CCAcademicDayNodeProps }
|
||||
AcademicPeriod: { props: CCAcademicPeriodNodeProps }
|
||||
RegistrationPeriod: { props: CCRegistrationPeriodNodeProps }
|
||||
PastoralStructure: { props: CCPastoralStructureNodeProps }
|
||||
KeyStage: { props: CCKeyStageNodeProps }
|
||||
Department: { props: CCDepartmentNodeProps }
|
||||
Room: { props: CCRoomNodeProps }
|
||||
SubjectClass: { props: CCSubjectClassNodeProps }
|
||||
DepartmentStructure: { props: CCDepartmentStructureNodeProps }
|
||||
UserTeacherTimetable: { props: CCUserTeacherTimetableNodeProps }
|
||||
UserTimetableLesson: { props: CCUserTimetableLessonNodeProps }
|
||||
}
|
||||
|
||||
// Helper function to get shape type from node type
|
||||
export const getShapeType = (nodeType: keyof CCNodeTypes): string => {
|
||||
return `cc-${nodeType.replace(/([A-Z])/g, '-$1').toLowerCase().substring(1)}-node`;
|
||||
}
|
||||
|
||||
// Helper function to get allowed props from node type
|
||||
export const getAllowedProps = (): string[] => {
|
||||
return ['__primarylabel__', 'unique_id'];
|
||||
}
|
||||
|
||||
// Helper function to get node configuration
|
||||
export const getNodeConfig = <T extends keyof CCNodeTypes>(nodeType: T) => {
|
||||
const shapeType = getShapeType(nodeType);
|
||||
return {
|
||||
shapeType,
|
||||
allowedProps: getAllowedProps()
|
||||
};
|
||||
}
|
||||
|
||||
// Helper function to check if a string is a valid node type
|
||||
export const isValidNodeType = (type: string): type is keyof CCNodeTypes => {
|
||||
return type in {
|
||||
User: true,
|
||||
Developer: true,
|
||||
Teacher: true,
|
||||
Student: true,
|
||||
Calendar: true,
|
||||
TeacherTimetable: true,
|
||||
TimetableLesson: true,
|
||||
PlannedLesson: true,
|
||||
School: true,
|
||||
CalendarYear: true,
|
||||
CalendarMonth: true,
|
||||
CalendarWeek: true,
|
||||
CalendarDay: true,
|
||||
CalendarTimeChunk: true,
|
||||
ScienceLab: true,
|
||||
KeyStageSyllabus: true,
|
||||
YearGroupSyllabus: true,
|
||||
CurriculumStructure: true,
|
||||
Topic: true,
|
||||
TopicLesson: true,
|
||||
LearningStatement: true,
|
||||
SchoolTimetable: true,
|
||||
AcademicYear: true,
|
||||
AcademicTerm: true,
|
||||
AcademicWeek: true,
|
||||
AcademicDay: true,
|
||||
AcademicPeriod: true,
|
||||
RegistrationPeriod: true,
|
||||
PastoralStructure: true,
|
||||
KeyStage: true,
|
||||
Department: true,
|
||||
Room: true,
|
||||
SubjectClass: true,
|
||||
DepartmentStructure: true,
|
||||
UserTeacherTimetable: true,
|
||||
UserTimetableLesson: true,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,218 @@
|
||||
import { Editor, createShapeId } from '@tldraw/tldraw';
|
||||
import { getShapeType, isValidNodeType } from './cc-graph-types';
|
||||
import { AllNodeShapes, NodeShapeType } from './cc-graph-shapes';
|
||||
import { logger } from '../../../../debugConfig';
|
||||
|
||||
export const GRID_CELL_SIZE = 250;
|
||||
export const GRID_PADDING = 50;
|
||||
export const GRID_MAX_COLUMNS = 8;
|
||||
|
||||
export const graphState = {
|
||||
nodeData: new Map<string, AllNodeShapes>(),
|
||||
shapeIds: new Set<string>(),
|
||||
editor: null as Editor | null,
|
||||
|
||||
arrangeNodesInGrid: () => {
|
||||
if (!graphState.editor) {
|
||||
logger.error('graphStateUtil', '❌ Editor not initialized');
|
||||
return;
|
||||
}
|
||||
|
||||
const nodes = Array.from(graphState.nodeData.values());
|
||||
if (nodes.length === 0) return;
|
||||
|
||||
logger.debug('graphStateUtil', '📊 Arranging nodes in grid', {
|
||||
nodeCount: nodes.length,
|
||||
currentNodes: nodes
|
||||
});
|
||||
|
||||
// Get viewport bounds
|
||||
const viewportBounds = graphState.editor.getViewportPageBounds();
|
||||
|
||||
// Calculate available space
|
||||
const availableWidth = viewportBounds.width;
|
||||
|
||||
// Calculate number of columns based on available space
|
||||
const columnsFromSpace = Math.floor(availableWidth / (GRID_CELL_SIZE + GRID_PADDING));
|
||||
const gridColumns = Math.min(
|
||||
Math.max(1, Math.min(columnsFromSpace, GRID_MAX_COLUMNS)),
|
||||
Math.ceil(Math.sqrt(nodes.length * 2)) // Allow grid to grow with more nodes
|
||||
);
|
||||
|
||||
// Calculate total grid dimensions
|
||||
const rowCount = Math.ceil(nodes.length / gridColumns);
|
||||
const totalWidth = gridColumns * (GRID_CELL_SIZE + GRID_PADDING);
|
||||
const totalHeight = rowCount * (GRID_CELL_SIZE + GRID_PADDING);
|
||||
|
||||
// Calculate starting position to center the grid
|
||||
const startX = viewportBounds.minX + (viewportBounds.width - totalWidth) / 2;
|
||||
const startY = viewportBounds.minY + (viewportBounds.height - totalHeight) / 2;
|
||||
|
||||
// Track created/updated shapes for viewport adjustment
|
||||
const updatedShapeIds: string[] = [];
|
||||
|
||||
nodes.forEach((node, index) => {
|
||||
if (!node.props?.unique_id) return;
|
||||
|
||||
const row = Math.floor(index / gridColumns);
|
||||
const col = index % gridColumns;
|
||||
|
||||
const x = startX + (col * (GRID_CELL_SIZE + GRID_PADDING));
|
||||
const y = startY + (row * (GRID_CELL_SIZE + GRID_PADDING));
|
||||
|
||||
const shapeId = createShapeId(node.props.unique_id);
|
||||
updatedShapeIds.push(shapeId.toString());
|
||||
|
||||
// Update both our internal state and the editor
|
||||
node.x = x;
|
||||
node.y = y;
|
||||
graphState.nodeData.set(node.props.unique_id, node);
|
||||
|
||||
// Only create if the shape doesn't exist in our tracking
|
||||
if (!graphState.shapeIds.has(shapeId.toString())) {
|
||||
graphState.editor!.createShape({
|
||||
id: shapeId,
|
||||
type: node.type,
|
||||
x: x,
|
||||
y: y,
|
||||
props: node.props
|
||||
});
|
||||
graphState.shapeIds.add(shapeId.toString());
|
||||
|
||||
logger.debug('graphStateUtil', '➕ Created new shape', {
|
||||
id: shapeId.toString(),
|
||||
position: { x, y }
|
||||
});
|
||||
} else {
|
||||
graphState.editor!.updateShape({
|
||||
id: shapeId,
|
||||
type: node.type,
|
||||
x: x,
|
||||
y: y,
|
||||
});
|
||||
|
||||
logger.debug('graphStateUtil', '🔄 Updated existing shape', {
|
||||
id: shapeId.toString(),
|
||||
position: { x, y }
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Only attempt to adjust view if we have shapes
|
||||
if (updatedShapeIds.length > 0) {
|
||||
const shapeIds = updatedShapeIds.map(id => createShapeId(id));
|
||||
graphState.editor.select(...shapeIds);
|
||||
graphState.editor.zoomToSelection();
|
||||
graphState.editor.deselect();
|
||||
|
||||
logger.debug('graphStateUtil', '🔍 Adjusted viewport for shapes', {
|
||||
shapeCount: updatedShapeIds.length
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
updateShapesWithDagre: () => {
|
||||
if (!graphState.editor) {
|
||||
logger.error('graphStateUtil', '❌ Editor not initialized');
|
||||
return;
|
||||
}
|
||||
|
||||
graphState.arrangeNodesInGrid();
|
||||
},
|
||||
|
||||
addNode: (shape: AllNodeShapes) => {
|
||||
logger.debug('graphStateUtil', '🔍 Adding shape to graphState:', { shape });
|
||||
|
||||
if (!shape.props?.unique_id || !shape.type) {
|
||||
logger.error('graphStateUtil', '❌ Invalid shape data', { shape });
|
||||
return;
|
||||
}
|
||||
|
||||
const id = shape.props.unique_id;
|
||||
const shapeId = createShapeId(id).toString();
|
||||
|
||||
// Track the shape ID
|
||||
graphState.shapeIds.add(shapeId);
|
||||
|
||||
const nodeType = shape.props.__primarylabel__;
|
||||
if (!isValidNodeType(nodeType)) {
|
||||
logger.error('graphStateUtil', '❌ Unknown node type', {
|
||||
type: nodeType,
|
||||
shape
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const shapeType = getShapeType(nodeType) as NodeShapeType;
|
||||
graphState.nodeData.set(id, {
|
||||
...shape,
|
||||
type: shapeType
|
||||
} as AllNodeShapes);
|
||||
|
||||
// Only rearrange if we have an editor
|
||||
if (graphState.editor) {
|
||||
graphState.arrangeNodesInGrid();
|
||||
}
|
||||
},
|
||||
|
||||
getNode: (id: string) => {
|
||||
return graphState.nodeData.get(id);
|
||||
},
|
||||
|
||||
getAllNodes: () => {
|
||||
return Array.from(graphState.nodeData.values()).filter(item => {
|
||||
// Check if the item has a type property and it's not an edge type
|
||||
logger.debug('graphStateUtil', '🔍 Checking if item has a type property and it\'s not an edge type:', { item });
|
||||
return item.type && !item.type.includes('relationship');
|
||||
});
|
||||
},
|
||||
|
||||
setEditor: (editor: Editor) => {
|
||||
graphState.editor = editor;
|
||||
graphState.shapeIds.clear();
|
||||
},
|
||||
|
||||
updateNodePosition: (nodeId: string, newPos: { x: number, y: number }) => {
|
||||
const node = graphState.nodeData.get(nodeId);
|
||||
if (node) {
|
||||
node.x = newPos.x;
|
||||
node.y = newPos.y;
|
||||
graphState.nodeData.set(nodeId, node);
|
||||
|
||||
// If we have an editor reference, update the shape
|
||||
if (graphState.editor) {
|
||||
const shapeId = createShapeId(nodeId);
|
||||
logger.debug('graphStateUtil', '🎯 Updating shape position', {
|
||||
id: shapeId,
|
||||
position: { x: newPos.x, y: newPos.y }
|
||||
});
|
||||
graphState.editor.updateShape({
|
||||
id: shapeId,
|
||||
type: node.type,
|
||||
x: newPos.x,
|
||||
y: newPos.y,
|
||||
});
|
||||
}
|
||||
|
||||
logger.debug('graphStateUtil', '📍 Updated node position', {
|
||||
nodeId,
|
||||
newPos,
|
||||
node
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
hasNode: (uniqueId: string): boolean => {
|
||||
return graphState.nodeData.has(uniqueId);
|
||||
},
|
||||
|
||||
getShapeByUniqueId: (uniqueId: string) => {
|
||||
return Array.from(graphState.nodeData.values()).find(
|
||||
shape => shape.props?.unique_id === uniqueId
|
||||
);
|
||||
},
|
||||
|
||||
hasShape: (shapeId: string): boolean => {
|
||||
return graphState.shapeIds.has(shapeId);
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,246 @@
|
||||
import { TLRecord, TLShape } from '@tldraw/tldraw'
|
||||
import { getDefaultCCBaseProps, getDefaultCCCalendarProps, getDefaultCCLiveTranscriptionProps, getDefaultCCSettingsProps, getDefaultCCSlideProps, getDefaultCCSlideShowProps, getDefaultCCSlideLayoutBindingProps, getDefaultCCYoutubeEmbedProps, getDefaultCCSearchProps, getDefaultCCWebBrowserProps } from './cc-props'
|
||||
|
||||
// Export both shape and binding migrations
|
||||
export const ccBindingMigrations = {
|
||||
'cc-slide-layout': {
|
||||
firstVersion: 1,
|
||||
currentVersion: 1,
|
||||
migrators: {
|
||||
1: {
|
||||
up: (record: TLRecord) => {
|
||||
if (record.typeName !== 'binding') return record
|
||||
if (record.type !== 'cc-slide-layout') return record
|
||||
return {
|
||||
...record,
|
||||
props: {
|
||||
...getDefaultCCSlideLayoutBindingProps(),
|
||||
...record.props,
|
||||
},
|
||||
}
|
||||
},
|
||||
down: (record: TLRecord) => {
|
||||
return record
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
export const ccShapeMigrations = {
|
||||
base: {
|
||||
firstVersion: 1,
|
||||
currentVersion: 1,
|
||||
migrators: {
|
||||
1: {
|
||||
up: (record: TLRecord) => {
|
||||
if (record.typeName !== 'shape') return record
|
||||
const shape = record as TLShape
|
||||
if (shape.type !== 'cc-base') return record
|
||||
return {
|
||||
...shape,
|
||||
props: {
|
||||
...getDefaultCCBaseProps(),
|
||||
...shape.props,
|
||||
},
|
||||
}
|
||||
},
|
||||
down: (record: TLRecord) => {
|
||||
return record
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
calendar: {
|
||||
firstVersion: 1,
|
||||
currentVersion: 1,
|
||||
migrators: {
|
||||
1: {
|
||||
up: (record: TLRecord) => {
|
||||
if (record.typeName !== 'shape') return record
|
||||
const shape = record as TLShape
|
||||
if (shape.type !== 'cc-calendar') return record
|
||||
return {
|
||||
...shape,
|
||||
props: {
|
||||
...getDefaultCCCalendarProps(),
|
||||
...shape.props,
|
||||
},
|
||||
}
|
||||
},
|
||||
down: (record: TLRecord) => {
|
||||
return record
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
liveTranscription: {
|
||||
firstVersion: 1,
|
||||
currentVersion: 1,
|
||||
migrators: {
|
||||
1: {
|
||||
up: (record: TLRecord) => {
|
||||
if (record.typeName !== 'shape') return record
|
||||
const shape = record as TLShape
|
||||
if (shape.type !== 'cc-live-transcription') return record
|
||||
return {
|
||||
...shape,
|
||||
props: {
|
||||
...getDefaultCCLiveTranscriptionProps(),
|
||||
...shape.props,
|
||||
},
|
||||
}
|
||||
},
|
||||
down: (record: TLRecord) => {
|
||||
return record
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
settings: {
|
||||
firstVersion: 1,
|
||||
currentVersion: 1,
|
||||
migrators: {
|
||||
1: {
|
||||
up: (record: TLRecord) => {
|
||||
if (record.typeName !== 'shape') return record
|
||||
const shape = record as TLShape
|
||||
if (shape.type !== 'cc-settings') return record
|
||||
return {
|
||||
...shape,
|
||||
props: {
|
||||
...getDefaultCCSettingsProps(),
|
||||
...shape.props,
|
||||
},
|
||||
}
|
||||
},
|
||||
down: (record: TLRecord) => {
|
||||
return record
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
slideshow: {
|
||||
firstVersion: 1,
|
||||
currentVersion: 1,
|
||||
migrators: {
|
||||
1: {
|
||||
up: (record: TLRecord) => {
|
||||
if (record.typeName !== 'shape') return record
|
||||
const shape = record as TLShape
|
||||
if (shape.type !== 'cc-slideshow') return record
|
||||
return {
|
||||
...shape,
|
||||
props: {
|
||||
...getDefaultCCSlideShowProps(),
|
||||
...shape.props,
|
||||
},
|
||||
}
|
||||
},
|
||||
down: (record: TLRecord) => {
|
||||
return record
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
slide: {
|
||||
firstVersion: 1,
|
||||
currentVersion: 1,
|
||||
migrators: {
|
||||
1: {
|
||||
up: (record: TLRecord) => {
|
||||
if (record.typeName !== 'shape') return record
|
||||
const shape = record as TLShape
|
||||
if (shape.type !== 'cc-slide') return record
|
||||
return {
|
||||
...shape,
|
||||
props: {
|
||||
...getDefaultCCSlideProps(),
|
||||
...shape.props,
|
||||
},
|
||||
}
|
||||
},
|
||||
down: (record: TLRecord) => {
|
||||
return record
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
'cc-youtube-embed': {
|
||||
firstVersion: 1,
|
||||
currentVersion: 1,
|
||||
migrators: {
|
||||
1: {
|
||||
up: (record: TLRecord) => {
|
||||
if (record.typeName !== 'shape') return record
|
||||
const shape = record as TLShape
|
||||
if (shape.type !== 'cc-youtube-embed') return record
|
||||
return {
|
||||
...shape,
|
||||
props: {
|
||||
...getDefaultCCYoutubeEmbedProps(),
|
||||
...shape.props,
|
||||
},
|
||||
}
|
||||
},
|
||||
down: (record: TLRecord) => {
|
||||
return record
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
search: {
|
||||
firstVersion: 1,
|
||||
currentVersion: 1,
|
||||
migrators: {
|
||||
1: {
|
||||
up: (record: TLRecord) => {
|
||||
if (record.typeName !== 'shape') return record
|
||||
const shape = record as TLShape
|
||||
if (shape.type !== 'cc-search') return record
|
||||
return {
|
||||
...shape,
|
||||
props: {
|
||||
...getDefaultCCSearchProps(),
|
||||
...shape.props,
|
||||
},
|
||||
}
|
||||
},
|
||||
down: (record: TLRecord) => {
|
||||
return record
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
webBrowser: {
|
||||
firstVersion: 1,
|
||||
currentVersion: 1,
|
||||
migrators: {
|
||||
1: {
|
||||
up: (record: TLRecord) => {
|
||||
if (record.typeName !== 'shape') return record
|
||||
const shape = record as TLShape
|
||||
if (shape.type !== 'cc-web-browser') return record
|
||||
return {
|
||||
...shape,
|
||||
props: {
|
||||
...getDefaultCCWebBrowserProps(),
|
||||
...shape.props,
|
||||
},
|
||||
}
|
||||
},
|
||||
down: (record: TLRecord) => {
|
||||
return record
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,262 @@
|
||||
import { T } from 'tldraw'
|
||||
import { CC_BASE_STYLE_CONSTANTS, CC_SLIDESHOW_STYLE_CONSTANTS } from './cc-styles'
|
||||
|
||||
export interface CCBaseProps {
|
||||
title: string
|
||||
w: number
|
||||
h: number
|
||||
headerColor: string
|
||||
backgroundColor: string
|
||||
isLocked: boolean
|
||||
}
|
||||
|
||||
// Create a constant for the base props validation
|
||||
export const baseShapeProps = {
|
||||
title: T.string,
|
||||
w: T.number,
|
||||
h: T.number,
|
||||
headerColor: T.string,
|
||||
backgroundColor: T.string,
|
||||
isLocked: T.boolean,
|
||||
}
|
||||
|
||||
export const ccShapeProps = {
|
||||
base: baseShapeProps,
|
||||
|
||||
calendar: {
|
||||
...baseShapeProps,
|
||||
date: T.string,
|
||||
selectedDate: T.string,
|
||||
view: T.string,
|
||||
events: T.arrayOf(T.object({
|
||||
id: T.string,
|
||||
title: T.string,
|
||||
start: T.string,
|
||||
end: T.string,
|
||||
groupId: T.string.optional(),
|
||||
extendedProps: T.object({
|
||||
subjectClass: T.string,
|
||||
color: T.string,
|
||||
periodCode: T.string,
|
||||
tldraw_snapshot: T.string.optional()
|
||||
})
|
||||
})),
|
||||
},
|
||||
|
||||
liveTranscription: {
|
||||
...baseShapeProps,
|
||||
isRecording: T.boolean,
|
||||
segments: T.arrayOf(T.object({
|
||||
id: T.string,
|
||||
text: T.string,
|
||||
completed: T.boolean,
|
||||
start: T.string,
|
||||
end: T.string,
|
||||
})),
|
||||
currentSegment: T.object({
|
||||
id: T.string,
|
||||
text: T.string,
|
||||
completed: T.boolean,
|
||||
start: T.string,
|
||||
end: T.string,
|
||||
}).optional(),
|
||||
lastProcessedSegment: T.string.optional(),
|
||||
},
|
||||
|
||||
settings: {
|
||||
...baseShapeProps,
|
||||
userEmail: T.string,
|
||||
user_role: T.string,
|
||||
isTeacher: T.boolean,
|
||||
},
|
||||
|
||||
slideshow: {
|
||||
...baseShapeProps,
|
||||
currentSlideIndex: T.number,
|
||||
slidePattern: T.string,
|
||||
numSlides: T.number,
|
||||
slides: T.arrayOf(T.object({
|
||||
imageData: T.string,
|
||||
meta: T.object({
|
||||
text: T.string,
|
||||
format: T.string,
|
||||
}),
|
||||
})).optional(),
|
||||
},
|
||||
|
||||
slide: {
|
||||
...baseShapeProps,
|
||||
imageData: T.string,
|
||||
meta: T.object({
|
||||
text: T.string,
|
||||
format: T.string,
|
||||
}),
|
||||
},
|
||||
|
||||
'cc-youtube-embed': {
|
||||
...baseShapeProps,
|
||||
video_url: T.string,
|
||||
transcript: T.arrayOf(T.object({
|
||||
start: T.number,
|
||||
duration: T.number,
|
||||
text: T.string,
|
||||
})),
|
||||
transcriptVisible: T.boolean,
|
||||
},
|
||||
|
||||
search: {
|
||||
...baseShapeProps,
|
||||
query: T.string,
|
||||
results: T.arrayOf(T.object({
|
||||
title: T.string,
|
||||
url: T.string,
|
||||
content: T.string,
|
||||
})),
|
||||
isSearching: T.boolean,
|
||||
},
|
||||
|
||||
webBrowser: {
|
||||
...baseShapeProps,
|
||||
url: T.string,
|
||||
history: T.arrayOf(T.string),
|
||||
currentHistoryIndex: T.number,
|
||||
isLoading: T.boolean,
|
||||
},
|
||||
}
|
||||
|
||||
export const ccBindingProps = {
|
||||
'cc-slide-layout': {
|
||||
isMovingWithParent: T.boolean.optional(),
|
||||
placeholder: T.boolean.optional(),
|
||||
index: T.string
|
||||
},
|
||||
}
|
||||
|
||||
export const getDefaultCCBaseProps = () => ({
|
||||
title: 'Base Shape',
|
||||
w: 100,
|
||||
h: 100,
|
||||
headerColor: '#3e6589',
|
||||
backgroundColor: '#ffffff',
|
||||
isLocked: false,
|
||||
})
|
||||
|
||||
export const getDefaultCCCalendarProps = () => ({
|
||||
...getDefaultCCBaseProps(),
|
||||
date: new Date().toISOString(),
|
||||
selectedDate: new Date().toISOString(),
|
||||
view: 'timeGridWeek',
|
||||
events: [],
|
||||
})
|
||||
|
||||
export const getDefaultCCLiveTranscriptionProps = () => ({
|
||||
...getDefaultCCBaseProps(),
|
||||
isRecording: false,
|
||||
segments: [],
|
||||
currentSegment: undefined,
|
||||
lastProcessedSegment: undefined,
|
||||
})
|
||||
|
||||
export const getDefaultCCSettingsProps = () => ({
|
||||
...getDefaultCCBaseProps(),
|
||||
userEmail: '',
|
||||
user_role: '',
|
||||
isTeacher: false,
|
||||
})
|
||||
|
||||
export function getDefaultCCSlideShowProps() {
|
||||
// Base 16:9 ratio dimensions
|
||||
const baseWidth = 1280
|
||||
const baseHeight = 720
|
||||
// Add header height and spacing
|
||||
const totalHeight = baseHeight +
|
||||
CC_SLIDESHOW_STYLE_CONSTANTS.SLIDE_HEADER_HEIGHT + // Slideshow's own header
|
||||
CC_SLIDESHOW_STYLE_CONSTANTS.SLIDE_SPACING * 2 + // Top and bottom spacing
|
||||
CC_SLIDESHOW_STYLE_CONSTANTS.SLIDE_CONTENT_PADDING // Extra padding for content
|
||||
|
||||
return {
|
||||
title: 'Slideshow',
|
||||
w: baseWidth,
|
||||
h: totalHeight,
|
||||
headerColor: '#3e6589',
|
||||
backgroundColor: '#0f0f0f',
|
||||
isLocked: false,
|
||||
currentSlideIndex: 0,
|
||||
slidePattern: 'horizontal',
|
||||
numSlides: 3,
|
||||
slides: [],
|
||||
}
|
||||
}
|
||||
|
||||
export function getDefaultCCSlideProps() {
|
||||
// Base 16:9 ratio dimensions
|
||||
const baseWidth = 1280
|
||||
const baseHeight = 720
|
||||
// Add header height
|
||||
const totalHeight = baseHeight + CC_BASE_STYLE_CONSTANTS.HEADER.height
|
||||
|
||||
return {
|
||||
title: 'Slide',
|
||||
w: baseWidth,
|
||||
h: totalHeight,
|
||||
headerColor: '#3e6589',
|
||||
backgroundColor: '#0f0f0f',
|
||||
isLocked: false,
|
||||
imageData: '',
|
||||
meta: {
|
||||
text: '',
|
||||
format: 'markdown'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function getDefaultCCSlideLayoutBindingProps() {
|
||||
return {
|
||||
isMovingWithParent: false,
|
||||
placeholder: false,
|
||||
index: '0',
|
||||
}
|
||||
}
|
||||
|
||||
export function getDefaultCCYoutubeEmbedProps() {
|
||||
const videoHeight = 450
|
||||
const totalHeight = videoHeight + CC_BASE_STYLE_CONSTANTS.HEADER.height + (CC_BASE_STYLE_CONSTANTS.CONTENT.padding * 2)
|
||||
|
||||
return {
|
||||
...getDefaultCCBaseProps(),
|
||||
title: 'YouTube Video',
|
||||
w: 800,
|
||||
h: totalHeight,
|
||||
headerColor: '#ff0000',
|
||||
backgroundColor: '#0f0f0f',
|
||||
isLocked: false,
|
||||
video_url: 'https://www.youtube.com/watch?v=dQw4w9WgXcQ',
|
||||
transcript: [],
|
||||
transcriptVisible: false,
|
||||
}
|
||||
}
|
||||
|
||||
export const getDefaultCCSearchProps = () => ({
|
||||
...getDefaultCCBaseProps(),
|
||||
w: 400,
|
||||
h: 500,
|
||||
title: 'Search',
|
||||
headerColor: '#1a73e8',
|
||||
backgroundColor: '#ffffff',
|
||||
query: '',
|
||||
results: [],
|
||||
isSearching: false,
|
||||
})
|
||||
|
||||
export const getDefaultCCWebBrowserProps = () => ({
|
||||
...getDefaultCCBaseProps(),
|
||||
title: 'Web Browser',
|
||||
w: 800,
|
||||
h: 600,
|
||||
headerColor: '#1a73e8',
|
||||
backgroundColor: '#ffffff',
|
||||
url: '',
|
||||
history: [],
|
||||
currentHistoryIndex: -1,
|
||||
isLoading: false,
|
||||
})
|
||||
@@ -0,0 +1,85 @@
|
||||
import { CCBaseShapeUtil } from '../CCBaseShapeUtil'
|
||||
import { CCBaseShape } from '../cc-types'
|
||||
import { ccShapeProps, getDefaultCCSearchProps } from '../cc-props'
|
||||
import { ccShapeMigrations } from '../cc-migrations'
|
||||
import { Rectangle2d, Vec } from 'tldraw'
|
||||
import { SearchComponent } from './SearchComponent'
|
||||
import { SearchResult } from '../../../../services/tldraw/searchService'
|
||||
export interface CCSearchShape extends CCBaseShape {
|
||||
type: 'cc-search'
|
||||
props: CCBaseShape['props'] & {
|
||||
query: string
|
||||
results: SearchResult[]
|
||||
isSearching: boolean
|
||||
}
|
||||
}
|
||||
|
||||
export class CCSearchShapeUtil extends CCBaseShapeUtil<CCSearchShape> {
|
||||
static override type = 'cc-search' as const;
|
||||
static override props = ccShapeProps.search;
|
||||
static override migrations = ccShapeMigrations.search;
|
||||
|
||||
override getDefaultProps(): CCSearchShape['props'] {
|
||||
return getDefaultCCSearchProps() as CCSearchShape['props'];
|
||||
}
|
||||
|
||||
override isAspectRatioLocked = () => false
|
||||
override canResize = () => true
|
||||
override canBind = () => false
|
||||
override hideResizeHandles = () => false
|
||||
override hideRotateHandle = () => false
|
||||
override canEdit = () => false
|
||||
|
||||
override renderContent = (shape: CCSearchShape) => {
|
||||
return (
|
||||
<div style={{ width: '100%', height: '100%', pointerEvents: 'all' }}>
|
||||
<SearchComponent shape={shape} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
override getGeometry(shape: CCSearchShape) {
|
||||
return new Rectangle2d({
|
||||
width: shape.props.w,
|
||||
height: shape.props.h,
|
||||
isFilled: true,
|
||||
})
|
||||
}
|
||||
|
||||
override onResize = (
|
||||
shape: CCSearchShape,
|
||||
info: { initialShape: CCSearchShape; scaleX: number; scaleY: number }
|
||||
) => {
|
||||
const { initialShape, scaleX, scaleY } = info
|
||||
const newW = Math.max(300, Math.round(initialShape.props.w * scaleX))
|
||||
const newH = Math.max(200, Math.round(initialShape.props.h * scaleY))
|
||||
|
||||
return {
|
||||
props: {
|
||||
...shape.props,
|
||||
w: newW,
|
||||
h: newH,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
hitTestPoint = (shape: CCSearchShape, point: Vec) => {
|
||||
const geometry = this.getGeometry(shape)
|
||||
return geometry.hitTestPoint(point)
|
||||
}
|
||||
|
||||
hitTestLineSegment = (shape: CCSearchShape, start: Vec, end: Vec) => {
|
||||
const geometry = this.getGeometry(shape)
|
||||
return geometry.hitTestLineSegment(start, end)
|
||||
}
|
||||
|
||||
shouldRender = (prev: CCSearchShape, next: CCSearchShape) => {
|
||||
return (
|
||||
prev.props.w !== next.props.w ||
|
||||
prev.props.h !== next.props.h ||
|
||||
prev.props.query !== next.props.query ||
|
||||
prev.props.results !== next.props.results ||
|
||||
prev.props.isSearching !== next.props.isSearching
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,383 @@
|
||||
import React, { useState, useCallback } from 'react'
|
||||
import { useEditor } from '@tldraw/tldraw'
|
||||
import {
|
||||
TextField,
|
||||
IconButton,
|
||||
CircularProgress,
|
||||
List,
|
||||
ListItem,
|
||||
ListItemText,
|
||||
Paper,
|
||||
Button,
|
||||
ButtonGroup,
|
||||
Menu,
|
||||
MenuItem,
|
||||
Tooltip,
|
||||
IconButton as MuiIconButton
|
||||
} from '@mui/material'
|
||||
import SearchIcon from '@mui/icons-material/Search'
|
||||
import GridViewIcon from '@mui/icons-material/GridView'
|
||||
import ViewStreamIcon from '@mui/icons-material/ViewStream'
|
||||
import ViewWeekIcon from '@mui/icons-material/ViewWeek'
|
||||
import FilterNoneIcon from '@mui/icons-material/FilterNone'
|
||||
import OpenInNewIcon from '@mui/icons-material/OpenInNew'
|
||||
import { CCSearchShape } from './CCSearchShapeUtil'
|
||||
import { SearchService, SearchResult } from '../../../../services/tldraw/searchService'
|
||||
import { logger } from '../../../../debugConfig'
|
||||
import { createWebBrowserShape, createMultipleWebBrowsers } from '../shape-helpers/web-browser-helpers'
|
||||
|
||||
interface SearchComponentProps {
|
||||
shape: CCSearchShape
|
||||
}
|
||||
|
||||
type LayoutType = 'grid' | 'cascade' | 'horizontal' | 'vertical'
|
||||
|
||||
export const SearchComponent: React.FC<SearchComponentProps> = ({ shape }) => {
|
||||
const editor = useEditor()
|
||||
const [query, setQuery] = useState(shape.props.query || '')
|
||||
const [isSearching, setIsSearching] = useState(false)
|
||||
const [selectedResults, setSelectedResults] = useState<SearchResult[]>([])
|
||||
const [layoutAnchorEl, setLayoutAnchorEl] = useState<null | HTMLElement>(null)
|
||||
const [currentLayout, setCurrentLayout] = useState<LayoutType>('grid')
|
||||
|
||||
const handleSearch = useCallback(async () => {
|
||||
if (!query.trim()) return
|
||||
|
||||
logger.debug('cc-search', '🔍 Starting search', { query })
|
||||
setIsSearching(true)
|
||||
try {
|
||||
const results = await SearchService.search(query)
|
||||
logger.debug('cc-search', '✅ Search completed', {
|
||||
query,
|
||||
resultCount: results.length
|
||||
})
|
||||
|
||||
// Update the shape's properties
|
||||
editor.updateShape({
|
||||
id: shape.id,
|
||||
type: 'cc-search',
|
||||
props: {
|
||||
...shape.props,
|
||||
query,
|
||||
results,
|
||||
isSearching: false,
|
||||
},
|
||||
})
|
||||
} catch (error) {
|
||||
logger.error('cc-search', '❌ Search failed', { error })
|
||||
// Clear results on error
|
||||
editor.updateShape({
|
||||
id: shape.id,
|
||||
type: 'cc-search',
|
||||
props: {
|
||||
...shape.props,
|
||||
query,
|
||||
results: [],
|
||||
isSearching: false,
|
||||
},
|
||||
})
|
||||
} finally {
|
||||
setIsSearching(false)
|
||||
setSelectedResults([])
|
||||
}
|
||||
}, [query, editor, shape])
|
||||
|
||||
const handleKeyPress = (e: React.KeyboardEvent) => {
|
||||
if (e.key === 'Enter') {
|
||||
handleSearch()
|
||||
}
|
||||
}
|
||||
|
||||
const handleResultClick = useCallback((result: SearchResult) => {
|
||||
logger.debug('cc-search', '🖱️ Search result clicked', {
|
||||
resultTitle: result.title,
|
||||
resultUrl: result.url
|
||||
})
|
||||
|
||||
try {
|
||||
logger.debug('cc-web-browser', '🌐 Creating web browser shape', {
|
||||
url: result.url,
|
||||
position: { x: shape.x + shape.props.w + 20, y: shape.y }
|
||||
})
|
||||
|
||||
// Create a single browser shape for direct click
|
||||
createWebBrowserShape(editor, {
|
||||
url: result.url,
|
||||
title: result.title,
|
||||
x: shape.x + shape.props.w + 20,
|
||||
y: shape.y
|
||||
})
|
||||
|
||||
} catch (error) {
|
||||
logger.error('cc-web-browser', '❌ Failed to create web browser shape', { error })
|
||||
}
|
||||
}, [editor, shape])
|
||||
|
||||
const handleResultSelect = (result: SearchResult, event: React.MouseEvent) => {
|
||||
// If shift key is pressed, handle multi-select
|
||||
if (event.shiftKey) {
|
||||
setSelectedResults(prev => {
|
||||
const isSelected = prev.some(r => r.url === result.url)
|
||||
if (isSelected) {
|
||||
return prev.filter(r => r.url !== result.url)
|
||||
} else {
|
||||
return [...prev, result]
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const handleLayoutMenuOpen = (event: React.MouseEvent<HTMLElement>) => {
|
||||
setLayoutAnchorEl(event.currentTarget)
|
||||
}
|
||||
|
||||
const handleLayoutMenuClose = () => {
|
||||
setLayoutAnchorEl(null)
|
||||
}
|
||||
|
||||
const handleLayoutSelect = (layout: LayoutType) => {
|
||||
setCurrentLayout(layout)
|
||||
handleLayoutMenuClose()
|
||||
}
|
||||
|
||||
const openSelectedResults = () => {
|
||||
if (selectedResults.length === 0) return
|
||||
|
||||
logger.debug('cc-web-browser', '🌐 Creating web browser shapes', {
|
||||
count: selectedResults.length,
|
||||
layout: currentLayout
|
||||
})
|
||||
|
||||
const browsers = selectedResults.map(result => ({
|
||||
url: result.url,
|
||||
title: result.title
|
||||
}))
|
||||
|
||||
// Calculate starting position relative to the search shape
|
||||
const startX = shape.x + shape.props.w + 20
|
||||
const startY = shape.y
|
||||
|
||||
createMultipleWebBrowsers(editor, {
|
||||
browsers,
|
||||
layout: currentLayout,
|
||||
startX,
|
||||
startY,
|
||||
spacing: 30
|
||||
})
|
||||
|
||||
// Clear selection after opening
|
||||
setSelectedResults([])
|
||||
}
|
||||
|
||||
const getLayoutIcon = (layout: LayoutType) => {
|
||||
switch (layout) {
|
||||
case 'grid':
|
||||
return <GridViewIcon />
|
||||
case 'horizontal':
|
||||
return <ViewStreamIcon />
|
||||
case 'vertical':
|
||||
return <ViewWeekIcon />
|
||||
case 'cascade':
|
||||
return <FilterNoneIcon />
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
height: '100%',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
gap: '8px',
|
||||
pointerEvents: 'all', // Ensure we get pointer events
|
||||
position: 'relative', // Create a new stacking context
|
||||
zIndex: 1 // Ensure our content is above TLDraw's canvas
|
||||
}}
|
||||
onPointerDown={(e) => {
|
||||
// Prevent TLDraw from handling our pointer events
|
||||
e.stopPropagation()
|
||||
}}
|
||||
>
|
||||
<div style={{ display: 'flex', gap: '8px', alignItems: 'center' }}>
|
||||
<TextField
|
||||
fullWidth
|
||||
variant="outlined"
|
||||
size="small"
|
||||
value={query}
|
||||
onChange={(e) => setQuery(e.target.value)}
|
||||
onKeyPress={handleKeyPress}
|
||||
placeholder="Search..."
|
||||
disabled={isSearching}
|
||||
/>
|
||||
<IconButton
|
||||
onClick={handleSearch}
|
||||
disabled={isSearching || !query.trim()}
|
||||
color="primary"
|
||||
>
|
||||
{isSearching ? <CircularProgress size={24} /> : <SearchIcon />}
|
||||
</IconButton>
|
||||
</div>
|
||||
|
||||
{shape.props.results.length > 0 && (
|
||||
<div
|
||||
style={{ display: 'flex', gap: '8px', alignItems: 'center' }}
|
||||
onPointerDown={(e) => e.stopPropagation()}
|
||||
>
|
||||
<ButtonGroup variant="outlined" size="small">
|
||||
<Tooltip title="Open selected results">
|
||||
<span>
|
||||
<Button
|
||||
onClick={openSelectedResults}
|
||||
disabled={selectedResults.length === 0}
|
||||
startIcon={getLayoutIcon(currentLayout)}
|
||||
>
|
||||
Open ({selectedResults.length})
|
||||
</Button>
|
||||
</span>
|
||||
</Tooltip>
|
||||
<Tooltip title="Choose layout">
|
||||
<span>
|
||||
<Button
|
||||
size="small"
|
||||
onClick={handleLayoutMenuOpen}
|
||||
disabled={selectedResults.length === 0}
|
||||
>
|
||||
▼
|
||||
</Button>
|
||||
</span>
|
||||
</Tooltip>
|
||||
</ButtonGroup>
|
||||
<Menu
|
||||
anchorEl={layoutAnchorEl}
|
||||
open={Boolean(layoutAnchorEl)}
|
||||
onClose={handleLayoutMenuClose}
|
||||
>
|
||||
<MenuItem onClick={() => handleLayoutSelect('grid')}>
|
||||
<GridViewIcon sx={{ mr: 1 }} /> Grid
|
||||
</MenuItem>
|
||||
<MenuItem onClick={() => handleLayoutSelect('horizontal')}>
|
||||
<ViewStreamIcon sx={{ mr: 1 }} /> Horizontal
|
||||
</MenuItem>
|
||||
<MenuItem onClick={() => handleLayoutSelect('vertical')}>
|
||||
<ViewWeekIcon sx={{ mr: 1 }} /> Vertical
|
||||
</MenuItem>
|
||||
<MenuItem onClick={() => handleLayoutSelect('cascade')}>
|
||||
<FilterNoneIcon sx={{ mr: 1 }} /> Cascade
|
||||
</MenuItem>
|
||||
</Menu>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<Paper
|
||||
style={{
|
||||
flex: 1,
|
||||
overflow: 'auto',
|
||||
backgroundColor: 'rgba(255, 255, 255, 0.8)',
|
||||
position: 'relative', // Create a new stacking context
|
||||
zIndex: 1 // Ensure our content is above TLDraw's canvas
|
||||
}}
|
||||
onPointerDown={(e) => e.stopPropagation()}
|
||||
>
|
||||
<List>
|
||||
{shape.props.results.map((result, index) => (
|
||||
<ListItem
|
||||
key={index}
|
||||
onClick={(e) => handleResultSelect(result, e)}
|
||||
selected={selectedResults.some(r => r.url === result.url)}
|
||||
sx={{
|
||||
cursor: 'pointer',
|
||||
position: 'relative',
|
||||
'&:hover': {
|
||||
backgroundColor: 'rgba(25, 118, 210, 0.08)',
|
||||
},
|
||||
'&.Mui-selected': {
|
||||
backgroundColor: 'rgba(25, 118, 210, 0.12)',
|
||||
'&:hover': {
|
||||
backgroundColor: 'rgba(25, 118, 210, 0.16)',
|
||||
},
|
||||
'&::before': {
|
||||
content: '""',
|
||||
position: 'absolute',
|
||||
left: 0,
|
||||
top: 0,
|
||||
bottom: 0,
|
||||
width: '4px',
|
||||
backgroundColor: '#1a73e8',
|
||||
borderRadius: '0 2px 2px 0'
|
||||
}
|
||||
},
|
||||
transition: 'all 0.2s ease'
|
||||
}}
|
||||
onPointerDown={(e) => {
|
||||
// Prevent TLDraw from handling our pointer events
|
||||
e.stopPropagation()
|
||||
}}
|
||||
>
|
||||
<div style={{
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
flex: 1,
|
||||
gap: '4px'
|
||||
}}>
|
||||
<ListItemText
|
||||
primary={result.title}
|
||||
secondary={result.content}
|
||||
primaryTypographyProps={{
|
||||
style: {
|
||||
fontWeight: selectedResults.some(r => r.url === result.url) ? 'bold' : 'normal',
|
||||
fontSize: '0.9rem',
|
||||
color: '#1a73e8'
|
||||
}
|
||||
}}
|
||||
secondaryTypographyProps={{
|
||||
style: {
|
||||
fontSize: '0.8rem',
|
||||
display: '-webkit-box',
|
||||
WebkitLineClamp: 2,
|
||||
WebkitBoxOrient: 'vertical',
|
||||
overflow: 'hidden'
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<div style={{
|
||||
display: 'flex',
|
||||
gap: '8px',
|
||||
alignItems: 'center'
|
||||
}}>
|
||||
<span style={{
|
||||
fontSize: '0.8rem',
|
||||
color: '#006621',
|
||||
maxWidth: '200px',
|
||||
overflow: 'hidden',
|
||||
textOverflow: 'ellipsis',
|
||||
whiteSpace: 'nowrap'
|
||||
}}>
|
||||
{result.url}
|
||||
</span>
|
||||
<Tooltip title="Open in new browser">
|
||||
<MuiIconButton
|
||||
size="small"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
handleResultClick(result)
|
||||
}}
|
||||
sx={{
|
||||
marginLeft: 'auto',
|
||||
color: '#1a73e8',
|
||||
'&:hover': {
|
||||
backgroundColor: 'rgba(26, 115, 232, 0.08)'
|
||||
}
|
||||
}}
|
||||
>
|
||||
<OpenInNewIcon fontSize="small" />
|
||||
</MuiIconButton>
|
||||
</Tooltip>
|
||||
</div>
|
||||
</div>
|
||||
</ListItem>
|
||||
))}
|
||||
</List>
|
||||
</Paper>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
import { CCBaseShapeUtil } from '../CCBaseShapeUtil'
|
||||
import { CCBaseShape } from '../cc-types'
|
||||
import { ccShapeProps, getDefaultCCSettingsProps } from '../cc-props'
|
||||
import { ccShapeMigrations } from '../cc-migrations'
|
||||
import { SettingsComponent } from './SettingsComponent'
|
||||
|
||||
export interface CCSettingsShape extends CCBaseShape {
|
||||
type: 'cc-settings'
|
||||
props: {
|
||||
title: string
|
||||
w: number
|
||||
h: number
|
||||
headerColor: string
|
||||
backgroundColor: string
|
||||
isLocked: boolean
|
||||
userEmail: string
|
||||
user_role: string
|
||||
isTeacher: boolean
|
||||
}
|
||||
}
|
||||
|
||||
export class CCSettingsShapeUtil extends CCBaseShapeUtil<CCSettingsShape> {
|
||||
static override type = 'cc-settings' as const
|
||||
static override props = ccShapeProps.settings
|
||||
static override migrations = ccShapeMigrations.settings
|
||||
|
||||
override getDefaultProps(): CCSettingsShape['props'] {
|
||||
return getDefaultCCSettingsProps() as CCSettingsShape['props']
|
||||
}
|
||||
|
||||
override canResize = () => false
|
||||
override isAspectRatioLocked = () => false
|
||||
override hideResizeHandles = () => true
|
||||
override hideRotateHandle = () => false
|
||||
override canEdit = () => false
|
||||
|
||||
override renderContent = () => {
|
||||
return <SettingsComponent />
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
import React from 'react'
|
||||
import { Container, Typography, Paper, Box, Button } from '@mui/material'
|
||||
import { useAuth } from '../../../../contexts/AuthContext'
|
||||
|
||||
export const SettingsComponent: React.FC = () => {
|
||||
// Use AuthContext to show real-time user data
|
||||
const { user, user_role: currentRole } = useAuth()
|
||||
const currentEmail = user?.email || ''
|
||||
const currentUserRole = currentRole || ''
|
||||
const isCurrentTeacher = currentRole?.includes('teacher') || false
|
||||
|
||||
return (
|
||||
<Container>
|
||||
{/* User Info Section */}
|
||||
<Paper sx={{ p: 2, mb: 2 }}>
|
||||
<Typography variant="h6" gutterBottom>
|
||||
User Information
|
||||
</Typography>
|
||||
<Box sx={{ mb: 2 }}>
|
||||
<Typography variant="body1">
|
||||
Email: {currentEmail || 'Not set'}
|
||||
</Typography>
|
||||
<Typography variant="body1">
|
||||
Role: {currentUserRole || 'Not set'}
|
||||
</Typography>
|
||||
</Box>
|
||||
</Paper>
|
||||
|
||||
{/* Timetable Upload Section - Only visible for teachers */}
|
||||
{isCurrentTeacher && (
|
||||
<Paper sx={{ p: 2, mb: 2 }}>
|
||||
<Typography variant="h6" gutterBottom>
|
||||
Timetable Management
|
||||
</Typography>
|
||||
<Box sx={{ mt: 2 }}>
|
||||
<Button
|
||||
variant="contained"
|
||||
component="label"
|
||||
color="secondary"
|
||||
fullWidth
|
||||
>
|
||||
Upload Timetable
|
||||
<input
|
||||
type="file"
|
||||
hidden
|
||||
accept=".xlsx"
|
||||
/>
|
||||
</Button>
|
||||
<Typography variant="caption" color="text.secondary" sx={{ mt: 1, display: 'block' }}>
|
||||
Upload your timetable in Excel (.xlsx) format
|
||||
</Typography>
|
||||
</Box>
|
||||
</Paper>
|
||||
)}
|
||||
</Container>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,453 @@
|
||||
import { BindingUtil, TLBaseBinding, IndexKey, Vec, TLShapeId } from '@tldraw/tldraw'
|
||||
import { CCSlideShowShape } from './CCSlideShowShapeUtil'
|
||||
import { CCSlideShape } from './CCSlideShapeUtil'
|
||||
import { CC_SLIDESHOW_STYLE_CONSTANTS } from '../cc-styles'
|
||||
import { logger } from '../../../../debugConfig'
|
||||
|
||||
export interface CCSlideLayoutBinding extends TLBaseBinding<'cc-slide-layout', {
|
||||
index: IndexKey
|
||||
isMovingWithParent: boolean
|
||||
placeholder: boolean
|
||||
}> {}
|
||||
|
||||
export class CCSlideLayoutBindingUtil extends BindingUtil<CCSlideLayoutBinding> {
|
||||
static type = 'cc-slide-layout' as const
|
||||
|
||||
getDefaultProps(): CCSlideLayoutBinding['props'] {
|
||||
return {
|
||||
index: 'a1' as IndexKey,
|
||||
isMovingWithParent: false,
|
||||
placeholder: false
|
||||
}
|
||||
}
|
||||
|
||||
private updateSlidePosition(binding: CCSlideLayoutBinding) {
|
||||
const { fromId: slideshowId, toId: slideId } = binding
|
||||
const slideshow = this.editor.getShape<CCSlideShowShape>(slideshowId)
|
||||
const slide = this.editor.getShape<CCSlideShape>(slideId)
|
||||
|
||||
if (!slideshow || !slide) return
|
||||
|
||||
// Get all bindings from the slideshow, sorted by index
|
||||
const bindings = this.editor
|
||||
.getBindingsFromShape<CCSlideLayoutBinding>(slideshow, 'cc-slide-layout')
|
||||
.sort((a, b) => (a.props.index > b.props.index ? 1 : -1))
|
||||
|
||||
// Find position in sorted bindings array
|
||||
const index = bindings.findIndex(b => b.id === binding.id)
|
||||
if (index === -1) return
|
||||
|
||||
// Get all slides and their dimensions up to the current index
|
||||
const slidesBeforeCurrent = bindings
|
||||
.slice(0, index)
|
||||
.map(b => {
|
||||
const s = this.editor.getShape<CCSlideShape>(b.toId)
|
||||
return s ? { width: s.props.w, height: s.props.h } : null
|
||||
})
|
||||
.filter(s => s !== null) as { width: number, height: number }[]
|
||||
|
||||
const spacing = CC_SLIDESHOW_STYLE_CONSTANTS.SLIDE_SPACING
|
||||
const headerHeight = CC_SLIDESHOW_STYLE_CONSTANTS.SLIDE_HEADER_HEIGHT
|
||||
const contentPadding = CC_SLIDESHOW_STYLE_CONSTANTS.SLIDE_CONTENT_PADDING
|
||||
|
||||
// Calculate new position based on pattern
|
||||
let offset: Vec = new Vec(0, 0)
|
||||
if (slideshow.props.slidePattern === 'horizontal') {
|
||||
// Sum widths of all previous slides plus spacing
|
||||
const totalWidthBefore = slidesBeforeCurrent.reduce((sum, s) => sum + s.width + spacing, 0)
|
||||
offset = new Vec(
|
||||
spacing + totalWidthBefore,
|
||||
headerHeight + contentPadding + spacing
|
||||
)
|
||||
} else if (slideshow.props.slidePattern === 'vertical') {
|
||||
// Sum heights of all previous slides plus spacing
|
||||
const totalHeightBefore = slidesBeforeCurrent.reduce((sum, s) => sum + s.height + spacing, 0)
|
||||
offset = new Vec(
|
||||
spacing,
|
||||
headerHeight + contentPadding + spacing + totalHeightBefore
|
||||
)
|
||||
} else if (slideshow.props.slidePattern === 'grid') {
|
||||
const cols = Math.ceil(Math.sqrt(bindings.length))
|
||||
const row = Math.floor(index / cols)
|
||||
const col = index % cols
|
||||
|
||||
// Get maximum dimensions for each column and row up to current position
|
||||
const colWidths = new Array(cols).fill(0)
|
||||
const rowHeights = new Array(Math.ceil(bindings.length / cols)).fill(0)
|
||||
|
||||
bindings.forEach((b, i) => {
|
||||
const s = this.editor.getShape<CCSlideShape>(b.toId)
|
||||
if (!s) return
|
||||
const r = Math.floor(i / cols)
|
||||
const c = i % cols
|
||||
colWidths[c] = Math.max(colWidths[c], s.props.w)
|
||||
rowHeights[r] = Math.max(rowHeights[r], s.props.h)
|
||||
})
|
||||
|
||||
// Calculate position based on accumulated column widths and row heights
|
||||
const xPos = spacing + colWidths.slice(0, col).reduce((sum, w) => sum + w + spacing, 0)
|
||||
const yPos = headerHeight + contentPadding + spacing +
|
||||
rowHeights.slice(0, row).reduce((sum, h) => sum + h + spacing, 0)
|
||||
|
||||
offset = new Vec(xPos, yPos)
|
||||
} else if (slideshow.props.slidePattern === 'radial') {
|
||||
// For radial pattern, calculate position based on index and total slides
|
||||
const totalSlides = bindings.length
|
||||
const angle = (2 * Math.PI * index) / totalSlides
|
||||
|
||||
// Find the largest slide dimension to determine radius
|
||||
const maxDimension = Math.max(
|
||||
...bindings.map(b => {
|
||||
const s = this.editor.getShape<CCSlideShape>(b.toId)
|
||||
return s ? Math.max(s.props.w, s.props.h) : 0
|
||||
})
|
||||
)
|
||||
|
||||
const radius = maxDimension * 0.75 // Adjust radius based on largest slide
|
||||
|
||||
// Calculate position on the circle
|
||||
const x = spacing + radius + (radius * Math.cos(angle))
|
||||
const y = headerHeight + contentPadding + spacing + radius + (radius * Math.sin(angle))
|
||||
|
||||
offset = new Vec(x, y)
|
||||
}
|
||||
|
||||
const point = this.editor.getPointInParentSpace(
|
||||
slide,
|
||||
this.editor.getShapePageTransform(slideshow)!.applyToPoint(offset)
|
||||
)
|
||||
|
||||
if (slide.x !== point.x || slide.y !== point.y) {
|
||||
this.editor.updateShape<CCSlideShape>({
|
||||
id: slideId,
|
||||
type: 'cc-slide',
|
||||
x: point.x,
|
||||
y: point.y,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
private updateSlideshowSize(slideshowId: TLShapeId) {
|
||||
const slideshow = this.editor.getShape<CCSlideShowShape>(slideshowId)
|
||||
if (!slideshow) return
|
||||
|
||||
// Get all bindings, including placeholders
|
||||
const bindings = this.editor
|
||||
.getBindingsFromShape<CCSlideLayoutBinding>(slideshow, 'cc-slide-layout')
|
||||
.sort((a, b) => (a.props.index > b.props.index ? 1 : -1))
|
||||
|
||||
const spacing = CC_SLIDESHOW_STYLE_CONSTANTS.SLIDE_SPACING
|
||||
const headerHeight = CC_SLIDESHOW_STYLE_CONSTANTS.SLIDE_HEADER_HEIGHT
|
||||
const contentPadding = CC_SLIDESHOW_STYLE_CONSTANTS.SLIDE_CONTENT_PADDING
|
||||
|
||||
// Default dimensions for empty slideshow
|
||||
const defaultWidth = CC_SLIDESHOW_STYLE_CONSTANTS.DEFAULT_SLIDE_WIDTH + (spacing * 2)
|
||||
const defaultHeight = CC_SLIDESHOW_STYLE_CONSTANTS.DEFAULT_SLIDE_HEIGHT +
|
||||
headerHeight + (contentPadding * 2) + (spacing * 2)
|
||||
|
||||
// If no bindings, set to default size
|
||||
if (bindings.length === 0) {
|
||||
if (slideshow.props.w !== defaultWidth || slideshow.props.h !== defaultHeight) {
|
||||
this.editor.updateShape<CCSlideShowShape>({
|
||||
id: slideshow.id,
|
||||
type: 'cc-slideshow',
|
||||
props: {
|
||||
...slideshow.props,
|
||||
w: defaultWidth,
|
||||
h: defaultHeight
|
||||
}
|
||||
})
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// Get all slides and their dimensions
|
||||
const slides = bindings.map(binding => {
|
||||
const slide = this.editor.getShape<CCSlideShape>(binding.toId)
|
||||
if (!slide) return null
|
||||
return {
|
||||
width: slide.props.w,
|
||||
height: slide.props.h
|
||||
}
|
||||
}).filter(slide => slide !== null) as { width: number, height: number }[]
|
||||
|
||||
if (slides.length === 0) return
|
||||
|
||||
// Calculate dimensions based on pattern
|
||||
let width = defaultWidth
|
||||
let height = defaultHeight
|
||||
|
||||
if (slideshow.props.slidePattern === 'horizontal') {
|
||||
// Sum of all widths plus spacing between them
|
||||
width = Math.max(
|
||||
spacing + (slides.reduce((sum, slide) => sum + slide.width, 0) +
|
||||
((slides.length - 1) * spacing)) + spacing,
|
||||
slides[0].width + (spacing * 2) // Minimum width is first slide plus spacing
|
||||
)
|
||||
// Maximum height of slides plus header and spacing
|
||||
height = headerHeight + contentPadding * 2 + spacing * 2 +
|
||||
Math.max(...slides.map(slide => slide.height))
|
||||
} else if (slideshow.props.slidePattern === 'vertical') {
|
||||
// Maximum width of slides plus spacing
|
||||
width = Math.max(...slides.map(slide => slide.width)) + (spacing * 2)
|
||||
// Sum of all heights plus spacing between them
|
||||
height = Math.max(
|
||||
headerHeight + contentPadding * 2 + spacing +
|
||||
(slides.reduce((sum, slide) => sum + slide.height, 0) +
|
||||
((slides.length - 1) * spacing)) + spacing,
|
||||
headerHeight + contentPadding * 2 + spacing * 2 + slides[0].height
|
||||
)
|
||||
} else if (slideshow.props.slidePattern === 'grid') {
|
||||
const cols = Math.ceil(Math.sqrt(slides.length))
|
||||
const rows = Math.ceil(slides.length / cols)
|
||||
|
||||
// Find maximum width and height for grid cells
|
||||
const maxCellWidth = Math.max(...slides.map(slide => slide.width))
|
||||
const maxCellHeight = Math.max(...slides.map(slide => slide.height))
|
||||
|
||||
width = Math.max(
|
||||
spacing + (cols * maxCellWidth + (cols - 1) * spacing) + spacing,
|
||||
maxCellWidth + (spacing * 2)
|
||||
)
|
||||
height = Math.max(
|
||||
headerHeight + contentPadding * 2 + spacing +
|
||||
(rows * maxCellHeight + (rows - 1) * spacing) + spacing,
|
||||
headerHeight + contentPadding * 2 + spacing * 2 + maxCellHeight
|
||||
)
|
||||
} else if (slideshow.props.slidePattern === 'radial') {
|
||||
// For radial pattern, use the largest slide dimensions to ensure proper spacing
|
||||
const maxSlideWidth = Math.max(...slides.map(slide => slide.width))
|
||||
const maxSlideHeight = Math.max(...slides.map(slide => slide.height))
|
||||
|
||||
// Calculate dimensions to fit all slides in a circle
|
||||
const radius = Math.max(maxSlideWidth, maxSlideHeight) * 1.5 // 1.5x for spacing
|
||||
width = radius * 2 + (spacing * 2)
|
||||
height = headerHeight + contentPadding * 2 + radius * 2 + (spacing * 2)
|
||||
}
|
||||
|
||||
if (width !== slideshow.props.w || height !== slideshow.props.h) {
|
||||
this.editor.updateShape<CCSlideShowShape>({
|
||||
id: slideshow.id,
|
||||
type: 'cc-slideshow',
|
||||
props: {
|
||||
...slideshow.props,
|
||||
w: width,
|
||||
h: height
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// Update positions for all bindings
|
||||
bindings.forEach(binding => this.updateSlidePosition(binding))
|
||||
}
|
||||
|
||||
private getDistance(a: Vec, b: Vec): number {
|
||||
const dx = a.x - b.x
|
||||
const dy = a.y - b.y
|
||||
return Math.sqrt(dx * dx + dy * dy)
|
||||
}
|
||||
|
||||
private getInsertPosition(
|
||||
slideshow: CCSlideShowShape,
|
||||
draggedSlide: CCSlideShape,
|
||||
point: Vec
|
||||
): { index: number; offset: Vec } {
|
||||
const bindings = this.editor
|
||||
.getBindingsFromShape<CCSlideLayoutBinding>(slideshow, 'cc-slide-layout')
|
||||
.sort((a, b) => (a.props.index > b.props.index ? 1 : -1))
|
||||
|
||||
const spacing = CC_SLIDESHOW_STYLE_CONSTANTS.SLIDE_SPACING
|
||||
const headerHeight = CC_SLIDESHOW_STYLE_CONSTANTS.SLIDE_HEADER_HEIGHT
|
||||
const contentPadding = CC_SLIDESHOW_STYLE_CONSTANTS.SLIDE_CONTENT_PADDING
|
||||
|
||||
// Get all existing slides with their dimensions
|
||||
const existingSlides = bindings.map(b => {
|
||||
const slide = this.editor.getShape<CCSlideShape>(b.toId)
|
||||
return slide ? {
|
||||
width: slide.props.w,
|
||||
height: slide.props.h,
|
||||
binding: b
|
||||
} : null
|
||||
}).filter(s => s !== null) as { width: number; height: number; binding: CCSlideLayoutBinding }[]
|
||||
|
||||
if (slideshow.props.slidePattern === 'horizontal') {
|
||||
let currentX = spacing
|
||||
let insertIndex = 0
|
||||
|
||||
// Calculate trigger points based on slide widths and the dragged slide
|
||||
for (const slide of existingSlides) {
|
||||
// Calculate the gap between slides based on the larger width
|
||||
const gapWidth = Math.max(slide.width, draggedSlide.props.w)
|
||||
const triggerPoint = currentX + (gapWidth / 2)
|
||||
if (point.x < triggerPoint) break
|
||||
currentX += slide.width + spacing
|
||||
insertIndex++
|
||||
}
|
||||
|
||||
return {
|
||||
index: insertIndex,
|
||||
offset: new Vec(currentX, headerHeight + contentPadding + spacing)
|
||||
}
|
||||
|
||||
} else if (slideshow.props.slidePattern === 'vertical') {
|
||||
let currentY = headerHeight + contentPadding + spacing
|
||||
let insertIndex = 0
|
||||
|
||||
// Calculate trigger points based on slide heights and the dragged slide
|
||||
for (const slide of existingSlides) {
|
||||
// Calculate the gap between slides based on the larger height
|
||||
const gapHeight = Math.max(slide.height, draggedSlide.props.h)
|
||||
const triggerPoint = currentY + (gapHeight / 2)
|
||||
if (point.y < triggerPoint) break
|
||||
currentY += slide.height + spacing
|
||||
insertIndex++
|
||||
}
|
||||
|
||||
return {
|
||||
index: insertIndex,
|
||||
offset: new Vec(spacing, currentY)
|
||||
}
|
||||
|
||||
} else if (slideshow.props.slidePattern === 'grid') {
|
||||
const cols = Math.ceil(Math.sqrt(existingSlides.length + 1)) // +1 for the dragged slide
|
||||
const colWidths = new Array(cols).fill(draggedSlide.props.w) // Initialize with dragged slide width
|
||||
const rowHeights = new Array(Math.ceil((existingSlides.length + 1) / cols)).fill(draggedSlide.props.h) // Initialize with dragged slide height
|
||||
|
||||
// Calculate maximum dimensions for each column and row, including dragged slide dimensions
|
||||
existingSlides.forEach((slide, i) => {
|
||||
const row = Math.floor(i / cols)
|
||||
const col = i % cols
|
||||
colWidths[col] = Math.max(colWidths[col], slide.width)
|
||||
rowHeights[row] = Math.max(rowHeights[row], slide.height)
|
||||
})
|
||||
|
||||
// Calculate grid cell positions with dynamic cell sizes
|
||||
let bestDistance = Infinity
|
||||
let bestIndex = 0
|
||||
let bestOffset = new Vec(0, 0)
|
||||
|
||||
for (let i = 0; i <= existingSlides.length; i++) {
|
||||
const row = Math.floor(i / cols)
|
||||
const col = i % cols
|
||||
|
||||
// Calculate cell position based on accumulated widths and heights
|
||||
const cellX = spacing + colWidths.slice(0, col).reduce((sum, w) => sum + w + spacing, 0)
|
||||
const cellY = headerHeight + contentPadding + spacing +
|
||||
rowHeights.slice(0, row).reduce((sum, h) => sum + h + spacing, 0)
|
||||
|
||||
// Calculate distance to cell center
|
||||
const cellCenterX = cellX + (colWidths[col] / 2)
|
||||
const cellCenterY = cellY + (rowHeights[row] / 2)
|
||||
const dx = point.x - cellCenterX
|
||||
const dy = point.y - cellCenterY
|
||||
const distance = Math.sqrt(dx * dx + dy * dy)
|
||||
|
||||
if (distance < bestDistance) {
|
||||
bestDistance = distance
|
||||
bestIndex = i
|
||||
bestOffset = new Vec(cellX, cellY)
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
index: bestIndex,
|
||||
offset: bestOffset
|
||||
}
|
||||
|
||||
} else if (slideshow.props.slidePattern === 'radial') {
|
||||
// Find the largest slide dimension including the dragged slide
|
||||
const maxDimension = Math.max(
|
||||
draggedSlide.props.w,
|
||||
draggedSlide.props.h,
|
||||
...existingSlides.map(s => Math.max(s.width, s.height))
|
||||
)
|
||||
|
||||
const radius = maxDimension * 0.75
|
||||
const center = new Vec(
|
||||
spacing + radius,
|
||||
headerHeight + contentPadding + spacing + radius
|
||||
)
|
||||
|
||||
// Calculate angle from center to drag point using manual vector calculation
|
||||
const dx = point.x - center.x
|
||||
const dy = point.y - center.y
|
||||
const angleToPoint = Math.atan2(dy, dx)
|
||||
|
||||
// Normalize angle to 0-2π range
|
||||
const normalizedAngle = angleToPoint < 0 ? angleToPoint + 2 * Math.PI : angleToPoint
|
||||
|
||||
// Calculate insert index based on angle
|
||||
const totalPositions = existingSlides.length + 1
|
||||
const insertIndex = Math.floor((normalizedAngle * totalPositions) / (2 * Math.PI))
|
||||
|
||||
// Calculate position on circle for this index
|
||||
const angle = (2 * Math.PI * insertIndex) / totalPositions
|
||||
const x = center.x + (radius * Math.cos(angle))
|
||||
const y = center.y + (radius * Math.sin(angle))
|
||||
|
||||
return {
|
||||
index: insertIndex,
|
||||
offset: new Vec(x, y)
|
||||
}
|
||||
}
|
||||
|
||||
// Default to end of slideshow if pattern not recognized
|
||||
return {
|
||||
index: existingSlides.length,
|
||||
offset: new Vec(spacing, headerHeight + contentPadding + spacing)
|
||||
}
|
||||
}
|
||||
|
||||
onTranslateBinding(binding: CCSlideLayoutBinding, draggedShape: CCSlideShape, point: Vec): boolean {
|
||||
const slideshow = this.editor.getShape<CCSlideShowShape>(binding.fromId)
|
||||
if (!slideshow) return false
|
||||
|
||||
const { index } = this.getInsertPosition(slideshow, draggedShape, point)
|
||||
const newIndex = `a${String(index + 1).padStart(3, '0')}` as IndexKey
|
||||
|
||||
this.editor.updateBinding({
|
||||
id: binding.id,
|
||||
type: 'cc-slide-layout',
|
||||
fromId: binding.fromId,
|
||||
toId: binding.toId,
|
||||
props: { index: newIndex }
|
||||
})
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
override onAfterCreate({ binding }: { binding: CCSlideLayoutBinding }): void {
|
||||
logger.debug('binding', '✅ onAfterCreate', { binding })
|
||||
this.updateSlidePosition(binding)
|
||||
this.updateSlideshowSize(binding.fromId)
|
||||
}
|
||||
|
||||
override onAfterChange({ bindingAfter }: { bindingAfter: CCSlideLayoutBinding }): void {
|
||||
logger.debug('binding', '✅ onAfterChange', { bindingAfter })
|
||||
// Check if the slideshow still exists
|
||||
const slideshow = this.editor.getShape<CCSlideShowShape>(bindingAfter.fromId)
|
||||
if (!slideshow) return
|
||||
|
||||
this.updateSlidePosition(bindingAfter)
|
||||
this.updateSlideshowSize(bindingAfter.fromId)
|
||||
}
|
||||
|
||||
override onAfterChangeFromShape({ binding }: { binding: CCSlideLayoutBinding }): void {
|
||||
logger.debug('binding', '✅ onAfterChangeFromShape', { binding })
|
||||
// Check if the slideshow still exists
|
||||
const slideshow = this.editor.getShape<CCSlideShowShape>(binding.fromId)
|
||||
if (!slideshow) return
|
||||
|
||||
this.updateSlidePosition(binding)
|
||||
this.updateSlideshowSize(binding.fromId)
|
||||
}
|
||||
|
||||
override onAfterDelete({ binding }: { binding: CCSlideLayoutBinding }): void {
|
||||
logger.debug('binding', '✅ onAfterDelete', { binding })
|
||||
// Check if the slideshow still exists
|
||||
const slideshow = this.editor.getShape<CCSlideShowShape>(binding.fromId)
|
||||
if (!slideshow) return
|
||||
|
||||
this.updateSlideshowSize(binding.fromId)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
import { BaseBoxShapeTool, StateNode } from '@tldraw/tldraw'
|
||||
|
||||
export class CCSlideShowShapeTool extends BaseBoxShapeTool {
|
||||
static override id = 'cc-slideshow'
|
||||
static override initial = 'idle'
|
||||
override shapeType = 'cc-slideshow'
|
||||
|
||||
override onPointerDown = () => {
|
||||
return this.transition('pointing')
|
||||
}
|
||||
|
||||
override onPointerUp: StateNode['onPointerUp'] = () => {
|
||||
const shape = this.editor.getSelectedShapes()[0]
|
||||
if (shape?.type === 'cc-slideshow') {
|
||||
// Switch to select tool after creating slideshow
|
||||
this.editor.setCurrentTool('select')
|
||||
}
|
||||
return this.transition('idle')
|
||||
}
|
||||
}
|
||||
|
||||
export class CCSlideShapeTool extends BaseBoxShapeTool {
|
||||
static override id = 'cc-slide'
|
||||
static override initial = 'idle'
|
||||
override shapeType = 'cc-slide'
|
||||
|
||||
override onPointerDown = () => {
|
||||
// Check if there's a selected slideshow before allowing slide creation
|
||||
const selectedShapes = this.editor.getSelectedShapes()
|
||||
const slideshow = selectedShapes.find((s) => s.type === 'cc-slideshow')
|
||||
|
||||
if (!slideshow) {
|
||||
this.editor.setCurrentTool('select')
|
||||
return this.transition('idle')
|
||||
}
|
||||
|
||||
return this.transition('pointing')
|
||||
}
|
||||
|
||||
override onPointerUp: StateNode['onPointerUp'] = () => {
|
||||
return this.transition('idle')
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,338 @@
|
||||
import { DefaultColorStyle, DefaultDashStyle, DefaultSizeStyle, Vec, getIndexBetween, clamp } from '@tldraw/tldraw'
|
||||
import { ccShapeProps, getDefaultCCSlideProps, CCBaseProps } from '../cc-props'
|
||||
import { CCBaseShapeUtil } from '../CCBaseShapeUtil'
|
||||
import { CCSlideShowShape } from './CCSlideShowShapeUtil'
|
||||
import { CCSlideLayoutBinding } from './CCSlideLayoutBindingUtil'
|
||||
import { CC_BASE_STYLE_CONSTANTS, CC_SLIDESHOW_STYLE_CONSTANTS } from '../cc-styles'
|
||||
import { ccShapeMigrations } from '../cc-migrations'
|
||||
import { logger } from '../../../../debugConfig'
|
||||
import { CCBaseShape } from '../cc-types'
|
||||
|
||||
type CCSlideProps = CCBaseProps & {
|
||||
imageData?: string
|
||||
meta: {
|
||||
text: string
|
||||
format: string
|
||||
}
|
||||
}
|
||||
|
||||
export interface CCSlideShape extends CCBaseShape {
|
||||
type: 'cc-slide'
|
||||
props: CCSlideProps
|
||||
}
|
||||
|
||||
export class CCSlideShapeUtil extends CCBaseShapeUtil<CCSlideShape> {
|
||||
static override type = 'cc-slide' as const
|
||||
static override props = ccShapeProps.slide
|
||||
static override migrations = ccShapeMigrations.slide
|
||||
|
||||
static styles = {
|
||||
color: DefaultColorStyle,
|
||||
dash: DefaultDashStyle,
|
||||
size: DefaultSizeStyle,
|
||||
}
|
||||
|
||||
override getDefaultProps(): CCSlideShape['props'] {
|
||||
return getDefaultCCSlideProps() as CCSlideShape['props']
|
||||
}
|
||||
|
||||
override canResize = () => false
|
||||
override isAspectRatioLocked = () => true
|
||||
override hideResizeHandles = () => true
|
||||
override hideRotateHandle = () => true
|
||||
override canEdit = () => false
|
||||
|
||||
override canBind(args: { fromShapeType: string; toShapeType: string; bindingType: string }): boolean {
|
||||
return args.fromShapeType === 'cc-slideshow' && args.toShapeType === 'cc-slide' && args.bindingType === 'cc-slide-layout'
|
||||
}
|
||||
|
||||
private getTargetSlideshow(shape: CCSlideShape, pageAnchor: Vec) {
|
||||
return this.editor.getShapeAtPoint(pageAnchor, {
|
||||
hitInside: true,
|
||||
filter: (otherShape) =>
|
||||
this.editor.canBindShapes({ fromShape: otherShape, toShape: shape, binding: 'cc-slide-layout' }),
|
||||
}) as CCSlideShowShape | undefined
|
||||
}
|
||||
|
||||
getBindingIndexForPosition(shape: CCSlideShape, slideshow: CCSlideShowShape, pageAnchor: Vec) {
|
||||
// Get all non-placeholder bindings, sorted by index
|
||||
const allBindings = this.editor
|
||||
.getBindingsFromShape<CCSlideLayoutBinding>(slideshow, 'cc-slide-layout')
|
||||
.filter(b => !b.props.placeholder || b.toId === shape.id) // Include our binding if it's placeholder
|
||||
.sort((a, b) => (a.props.index > b.props.index ? 1 : -1))
|
||||
|
||||
const spacing = CC_SLIDESHOW_STYLE_CONSTANTS.SLIDE_SPACING
|
||||
const headerHeight = CC_BASE_STYLE_CONSTANTS.HEADER.height
|
||||
const contentPadding = CC_SLIDESHOW_STYLE_CONSTANTS.SLIDE_CONTENT_PADDING
|
||||
|
||||
// Calculate target order based on position
|
||||
let order: number
|
||||
if (slideshow.props.slidePattern === 'horizontal') {
|
||||
order = clamp(
|
||||
Math.round((pageAnchor.x - slideshow.x - spacing) / (shape.props.w + spacing)),
|
||||
0,
|
||||
allBindings.length
|
||||
)
|
||||
} else if (slideshow.props.slidePattern === 'vertical') {
|
||||
order = clamp(
|
||||
Math.round((pageAnchor.y - slideshow.y - headerHeight - contentPadding - spacing) / (shape.props.h + spacing)),
|
||||
0,
|
||||
allBindings.length
|
||||
)
|
||||
} else if (slideshow.props.slidePattern === 'grid') {
|
||||
const cols = Math.ceil(Math.sqrt(allBindings.length))
|
||||
const col = clamp(
|
||||
Math.round((pageAnchor.x - slideshow.x - spacing) / (shape.props.w + spacing)),
|
||||
0,
|
||||
cols
|
||||
)
|
||||
const row = clamp(
|
||||
Math.round((pageAnchor.y - slideshow.y - headerHeight - contentPadding - spacing) / (shape.props.h + spacing)),
|
||||
0,
|
||||
Math.ceil(allBindings.length / cols)
|
||||
)
|
||||
order = clamp(row * cols + col, 0, allBindings.length)
|
||||
} else {
|
||||
order = 0
|
||||
}
|
||||
|
||||
// Get the bindings before and after our target position
|
||||
const belowSib = allBindings[order - 1]
|
||||
const aboveSib = allBindings[order]
|
||||
|
||||
// If we're already at this position, keep our current index
|
||||
if (belowSib?.toId === shape.id) {
|
||||
return belowSib.props.index
|
||||
} else if (aboveSib?.toId === shape.id) {
|
||||
return aboveSib.props.index
|
||||
}
|
||||
|
||||
// Otherwise, get an index between the two siblings
|
||||
return getIndexBetween(belowSib?.props.index, aboveSib?.props.index)
|
||||
}
|
||||
|
||||
override onTranslateStart = (shape: CCSlideShape) => {
|
||||
const bindings = this.editor.getBindingsToShape<CCSlideLayoutBinding>(shape.id, 'cc-slide-layout')
|
||||
logger.debug('shape', '✅ onTranslateStart', {
|
||||
shape,
|
||||
bindings,
|
||||
hasBindings: bindings.length > 0,
|
||||
bindingTypes: bindings.map(b => ({
|
||||
id: b.id,
|
||||
fromId: b.fromId,
|
||||
placeholder: b.props.placeholder,
|
||||
isMovingWithParent: b.props.isMovingWithParent
|
||||
}))
|
||||
})
|
||||
|
||||
this.editor.updateBindings(
|
||||
bindings.map((binding) => ({
|
||||
...binding,
|
||||
props: { ...binding.props, placeholder: true },
|
||||
}))
|
||||
)
|
||||
}
|
||||
|
||||
override onTranslate = (initial: CCSlideShape, current: CCSlideShape) => {
|
||||
const pageAnchor = this.editor.getShapePageTransform(current).applyToPoint({ x: current.props.w / 2, y: current.props.h / 2 })
|
||||
const targetSlideshow = this.getTargetSlideshow(current, pageAnchor)
|
||||
|
||||
// Get current binding if any
|
||||
const currentBindings = this.editor.getBindingsToShape<CCSlideLayoutBinding>(current.id, 'cc-slide-layout')
|
||||
const currentBinding = currentBindings[0]
|
||||
const currentSlideshow = currentBinding ? this.editor.getShape<CCSlideShowShape>(currentBinding.fromId) : undefined
|
||||
|
||||
logger.debug('shape', '✅ onTranslate', {
|
||||
initial,
|
||||
current,
|
||||
hasTargetSlideshow: !!targetSlideshow,
|
||||
targetSlideshowId: targetSlideshow?.id,
|
||||
currentBindings: currentBindings.map(b => ({
|
||||
id: b.id,
|
||||
fromId: b.fromId,
|
||||
placeholder: b.props.placeholder,
|
||||
isMovingWithParent: b.props.isMovingWithParent
|
||||
})),
|
||||
isInSlideshow: targetSlideshow ? this.isSlideInSlideshow(current, targetSlideshow) : false
|
||||
})
|
||||
|
||||
// If we're moving out of a slideshow
|
||||
if (currentBinding && currentSlideshow && !this.isSlideInSlideshow(current, currentSlideshow)) {
|
||||
logger.debug('shape', '✅ onTranslate: Moving out of slideshow', {
|
||||
slideId: current.id,
|
||||
slideshowId: currentSlideshow.id
|
||||
})
|
||||
// Delete all bindings
|
||||
this.editor.deleteBindings(currentBindings)
|
||||
return current
|
||||
}
|
||||
|
||||
// If we have no target slideshow, return
|
||||
if (!targetSlideshow) {
|
||||
return current
|
||||
}
|
||||
|
||||
// Calculate new index
|
||||
const index = this.getBindingIndexForPosition(current, targetSlideshow, pageAnchor)
|
||||
|
||||
// If we have a current binding and it's for this slideshow
|
||||
if (currentBinding && currentBinding.fromId === targetSlideshow.id) {
|
||||
// Only update if index changed
|
||||
if (currentBinding.props.index !== index) {
|
||||
logger.debug('shape', '✅ onTranslate: Updating binding index', {
|
||||
slideId: current.id,
|
||||
slideshowId: targetSlideshow.id,
|
||||
oldIndex: currentBinding.props.index,
|
||||
newIndex: index
|
||||
})
|
||||
this.editor.updateBinding<CCSlideLayoutBinding>({
|
||||
id: currentBinding.id,
|
||||
type: currentBinding.type,
|
||||
fromId: currentBinding.fromId,
|
||||
toId: currentBinding.toId,
|
||||
props: {
|
||||
...currentBinding.props,
|
||||
index,
|
||||
isMovingWithParent: true,
|
||||
},
|
||||
})
|
||||
}
|
||||
} else if (this.isSlideInSlideshow(current, targetSlideshow)) {
|
||||
logger.debug('shape', '✅ onTranslate: Creating new placeholder binding', {
|
||||
slideId: current.id,
|
||||
slideshowId: targetSlideshow.id,
|
||||
index
|
||||
})
|
||||
// Create new placeholder binding if we're inside the slideshow
|
||||
this.editor.createBinding<CCSlideLayoutBinding>({
|
||||
type: 'cc-slide-layout',
|
||||
fromId: targetSlideshow.id,
|
||||
toId: current.id,
|
||||
props: {
|
||||
index,
|
||||
isMovingWithParent: true,
|
||||
placeholder: true,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
return current
|
||||
}
|
||||
|
||||
private isSlideInSlideshow(slide: CCSlideShape, slideshow: CCSlideShowShape): boolean {
|
||||
const slideCenter = this.editor.getShapePageTransform(slide).applyToPoint({
|
||||
x: slide.props.w / 2,
|
||||
y: slide.props.h / 2
|
||||
})
|
||||
const slideshowBounds = this.editor.getShapeGeometry(slideshow).bounds
|
||||
|
||||
// Use smaller padding to be more lenient
|
||||
const padding = CC_SLIDESHOW_STYLE_CONSTANTS.SLIDE_SPACING / 4
|
||||
return (
|
||||
slideCenter.x >= slideshow.x + padding &&
|
||||
slideCenter.x <= slideshow.x + slideshowBounds.width - padding &&
|
||||
slideCenter.y >= slideshow.y + padding &&
|
||||
slideCenter.y <= slideshow.y + slideshowBounds.height - padding
|
||||
)
|
||||
}
|
||||
|
||||
override onTranslateEnd = (shape: CCSlideShape) => {
|
||||
const pageAnchor = this.editor.getShapePageTransform(shape).applyToPoint({ x: shape.props.w / 2, y: shape.props.h / 2 })
|
||||
const targetSlideshow = this.getTargetSlideshow(shape, pageAnchor)
|
||||
const bindings = this.editor.getBindingsToShape<CCSlideLayoutBinding>(shape.id, 'cc-slide-layout')
|
||||
|
||||
logger.debug('shape', '✅ onTranslateEnd', {
|
||||
shape,
|
||||
hasTargetSlideshow: !!targetSlideshow,
|
||||
targetSlideshowId: targetSlideshow?.id,
|
||||
bindings: bindings.map(b => ({
|
||||
id: b.id,
|
||||
fromId: b.fromId,
|
||||
placeholder: b.props.placeholder,
|
||||
isMovingWithParent: b.props.isMovingWithParent
|
||||
})),
|
||||
isInSlideshow: targetSlideshow ? this.isSlideInSlideshow(shape, targetSlideshow) : false
|
||||
})
|
||||
|
||||
// If we have a target slideshow and the slide is inside it
|
||||
if (targetSlideshow && this.isSlideInSlideshow(shape, targetSlideshow)) {
|
||||
const index = this.getBindingIndexForPosition(shape, targetSlideshow, pageAnchor)
|
||||
|
||||
// Instead of deleting and recreating, update existing binding if it exists
|
||||
const existingBinding = bindings[0]
|
||||
if (existingBinding && existingBinding.fromId === targetSlideshow.id) {
|
||||
logger.debug('shape', '✅ onTranslateEnd: Updating existing binding', {
|
||||
slideId: shape.id,
|
||||
slideshowId: targetSlideshow.id,
|
||||
bindingId: existingBinding.id,
|
||||
index
|
||||
})
|
||||
this.editor.updateBinding<CCSlideLayoutBinding>({
|
||||
id: existingBinding.id,
|
||||
type: existingBinding.type,
|
||||
fromId: existingBinding.fromId,
|
||||
toId: existingBinding.toId,
|
||||
props: {
|
||||
index,
|
||||
isMovingWithParent: true,
|
||||
placeholder: false,
|
||||
},
|
||||
})
|
||||
} else {
|
||||
logger.debug('shape', '✅ onTranslateEnd: Creating new binding', {
|
||||
slideId: shape.id,
|
||||
slideshowId: targetSlideshow.id,
|
||||
index
|
||||
})
|
||||
// If no existing binding or from different slideshow, delete and create new
|
||||
this.editor.deleteBindings(bindings)
|
||||
this.editor.createBinding<CCSlideLayoutBinding>({
|
||||
type: 'cc-slide-layout',
|
||||
fromId: targetSlideshow.id,
|
||||
toId: shape.id,
|
||||
props: {
|
||||
index,
|
||||
isMovingWithParent: true,
|
||||
placeholder: false,
|
||||
},
|
||||
})
|
||||
}
|
||||
} else {
|
||||
logger.debug('shape', '✅ onTranslateEnd: Removing bindings', {
|
||||
slideId: shape.id,
|
||||
bindings: bindings.map(b => b.id)
|
||||
})
|
||||
// Just delete bindings if we're not in a slideshow
|
||||
this.editor.deleteBindings(bindings)
|
||||
}
|
||||
}
|
||||
|
||||
override renderContent = (shape: CCSlideShape) => {
|
||||
|
||||
return (
|
||||
<div style={{
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
position: 'relative',
|
||||
backgroundColor: 'white',
|
||||
borderRadius: '4px',
|
||||
overflow: 'hidden'
|
||||
}}>
|
||||
{shape.props.imageData && (
|
||||
<img
|
||||
src={shape.props.imageData}
|
||||
alt={shape.props.title}
|
||||
style={{
|
||||
width: '100%',
|
||||
height: `100%`,
|
||||
objectFit: 'contain',
|
||||
position: 'absolute',
|
||||
top: 0,
|
||||
left: 0,
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
import { DefaultColorStyle, DefaultDashStyle, DefaultSizeStyle } from '@tldraw/tldraw'
|
||||
import { CCBaseShapeUtil } from '../CCBaseShapeUtil'
|
||||
import { ccShapeProps, getDefaultCCSlideShowProps, CCBaseProps } from '../cc-props'
|
||||
import { ccShapeMigrations } from '../cc-migrations'
|
||||
import { CCBaseShape } from '../cc-types'
|
||||
|
||||
type CCSlideshowProps = CCBaseProps & {
|
||||
currentSlideIndex: number
|
||||
slidePattern: string
|
||||
numSlides: number
|
||||
slides: string[]
|
||||
}
|
||||
|
||||
export interface CCSlideShowShape extends CCBaseShape {
|
||||
type: 'cc-slideshow'
|
||||
props: CCSlideshowProps
|
||||
}
|
||||
|
||||
export class CCSlideShowShapeUtil extends CCBaseShapeUtil<CCSlideShowShape> {
|
||||
static override type = 'cc-slideshow' as const
|
||||
static override props = ccShapeProps.slideshow
|
||||
static override migrations = ccShapeMigrations.slideshow
|
||||
|
||||
static styles = {
|
||||
color: DefaultColorStyle,
|
||||
dash: DefaultDashStyle,
|
||||
size: DefaultSizeStyle,
|
||||
}
|
||||
|
||||
getDefaultProps(): CCSlideShowShape['props'] {
|
||||
return getDefaultCCSlideShowProps() as CCSlideShowShape['props']
|
||||
}
|
||||
|
||||
override canResize = () => false
|
||||
override isAspectRatioLocked = () => true
|
||||
override hideResizeHandles = () => false
|
||||
override hideRotateHandle = () => false
|
||||
override canEdit = () => false
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
override canBind(args: { fromShapeType: string; toShapeType: string; bindingType: string }): boolean {
|
||||
return true
|
||||
}
|
||||
|
||||
onBeforeCreate(shape: CCSlideShowShape): CCSlideShowShape {
|
||||
return shape
|
||||
}
|
||||
|
||||
override renderContent = () => {
|
||||
return <div />
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,165 @@
|
||||
import { Editor, atom, useEditor, useValue } from '@tldraw/tldraw'
|
||||
import { CCSlideShowShape } from './CCSlideShowShapeUtil'
|
||||
import { CCSlideShape } from './CCSlideShapeUtil'
|
||||
import { logger } from '../../../../debugConfig'
|
||||
import { CCSlideLayoutBinding } from './CCSlideLayoutBindingUtil'
|
||||
|
||||
// Atoms for tracking current slideshow and slide
|
||||
export const $currentSlideShow = atom<CCSlideShowShape | null>('current slideshow', null)
|
||||
export const $currentSlide = atom<CCSlideShape | null>('current slide', null)
|
||||
|
||||
// Helper functions for getting slides and slideshows
|
||||
export function getSlidesFromPage(editor: Editor) {
|
||||
return editor
|
||||
.getSortedChildIdsForParent(editor.getCurrentPageId())
|
||||
.map((id) => editor.getShape(id))
|
||||
.filter((s): s is CCSlideShape => s?.type === 'cc-slide')
|
||||
}
|
||||
|
||||
export function getSlideShowsFromPage(editor: Editor) {
|
||||
return editor
|
||||
.getSortedChildIdsForParent(editor.getCurrentPageId())
|
||||
.map((id) => editor.getShape(id))
|
||||
.filter((s): s is CCSlideShowShape => s?.type === 'cc-slideshow')
|
||||
}
|
||||
|
||||
// Hooks for accessing slides and slideshows
|
||||
export function useSlideShows() {
|
||||
const editor = useEditor()
|
||||
return useValue<CCSlideShowShape[]>('slideshow shapes', () => getSlideShowsFromPage(editor), [editor])
|
||||
}
|
||||
|
||||
export function useSlides() {
|
||||
const editor = useEditor()
|
||||
return useValue<CCSlideShape[]>('slide shapes', () => getSlidesFromPage(editor), [editor])
|
||||
}
|
||||
|
||||
export function useCurrentSlide() {
|
||||
return useValue($currentSlide)
|
||||
}
|
||||
|
||||
export function useCurrentSlideShow() {
|
||||
return useValue($currentSlideShow)
|
||||
}
|
||||
|
||||
// Navigation functions
|
||||
export function moveToSlide(editor: Editor, slide: CCSlideShape, isPresentation: boolean = false) {
|
||||
logger.info('navigation', '🎯 Moving to slide', {
|
||||
slideId: slide.id,
|
||||
currentProps: slide.props,
|
||||
isPresentation,
|
||||
timestamp: new Date().toISOString()
|
||||
})
|
||||
|
||||
// Find the parent slideshow through bindings
|
||||
const binding = editor.getBindingsToShape(slide.id, 'cc-slide-layout')[0]
|
||||
if (!binding) {
|
||||
logger.warn('navigation', '⚠️ No binding found for slide', { slideId: slide.id })
|
||||
return
|
||||
}
|
||||
|
||||
const parentSlideshow = editor.getShape(binding.fromId) as CCSlideShowShape
|
||||
if (!parentSlideshow) {
|
||||
logger.warn('navigation', '⚠️ No parent slideshow found for slide', { slideId: slide.id })
|
||||
return
|
||||
}
|
||||
|
||||
// Get all bindings for this slideshow, sorted by index
|
||||
const bindings = editor
|
||||
.getBindingsFromShape(parentSlideshow, 'cc-slide-layout')
|
||||
.filter((b): b is CCSlideLayoutBinding => b.type === 'cc-slide-layout')
|
||||
.filter(b => !b.props.placeholder)
|
||||
.sort((a, b) => (a.props.index > b.props.index ? 1 : -1))
|
||||
|
||||
// Find the index of this slide's binding
|
||||
const slideIndex = bindings.findIndex(b => b.toId === slide.id)
|
||||
logger.debug('selection', '📍 Current slide position', {
|
||||
slideId: slide.id,
|
||||
slideIndex,
|
||||
slideshowId: parentSlideshow.id,
|
||||
totalSlides: bindings.length
|
||||
})
|
||||
|
||||
editor.batch(() => {
|
||||
logger.debug('navigation', '🔄 Starting slide transition', {
|
||||
from: parentSlideshow.props.currentSlideIndex,
|
||||
to: slideIndex
|
||||
})
|
||||
|
||||
// Update the slideshow's currentSlideIndex and slides array
|
||||
editor.updateShape<CCSlideShowShape>({
|
||||
id: parentSlideshow.id,
|
||||
type: 'cc-slideshow',
|
||||
props: {
|
||||
...parentSlideshow.props,
|
||||
currentSlideIndex: slideIndex,
|
||||
slides: bindings.map(b => b.toId),
|
||||
numSlides: bindings.length
|
||||
}
|
||||
})
|
||||
|
||||
// Update UI atoms for state tracking
|
||||
$currentSlide.set(slide)
|
||||
$currentSlideShow.set(parentSlideshow)
|
||||
})
|
||||
|
||||
logger.info('navigation', '✅ Slide transition complete', {
|
||||
slideId: slide.id,
|
||||
slideIndex,
|
||||
slideshowId: parentSlideshow.id
|
||||
})
|
||||
}
|
||||
|
||||
export function moveToSlideShow(editor: Editor, slideshow: CCSlideShowShape, isPresentation: boolean = false) {
|
||||
logger.info('navigation', '🎯 Moving to slideshow', {
|
||||
slideshowId: slideshow.id,
|
||||
currentIndex: slideshow.props.currentSlideIndex,
|
||||
isPresentation,
|
||||
timestamp: new Date().toISOString()
|
||||
})
|
||||
|
||||
// Update current slideshow state
|
||||
$currentSlideShow.set(slideshow)
|
||||
|
||||
// Get all bindings for this slideshow, sorted by index
|
||||
const bindings = editor
|
||||
.getBindingsFromShape(slideshow, 'cc-slide-layout')
|
||||
.filter((b): b is CCSlideLayoutBinding => b.type === 'cc-slide-layout')
|
||||
.filter(b => !b.props.placeholder)
|
||||
.sort((a, b) => (a.props.index > b.props.index ? 1 : -1))
|
||||
|
||||
// Get the current slide based on currentSlideIndex
|
||||
const currentBinding = bindings[slideshow.props.currentSlideIndex]
|
||||
if (currentBinding) {
|
||||
const currentSlide = editor.getShape(currentBinding.toId) as CCSlideShape
|
||||
if (currentSlide) {
|
||||
moveToSlide(editor, currentSlide, isPresentation)
|
||||
} else {
|
||||
logger.warn('navigation', '⚠️ Could not find current slide in slideshow', {
|
||||
slideshowId: slideshow.id,
|
||||
currentSlideId: currentBinding.toId,
|
||||
currentIndex: slideshow.props.currentSlideIndex
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Helper functions for labels
|
||||
export function getSlideLabel(slide: CCSlideShape, index: number) {
|
||||
return `Slide ${index + 1} (${slide.id})`
|
||||
}
|
||||
|
||||
export function getSlideShowLabel(slideshow: CCSlideShowShape, index: number) {
|
||||
return `Slideshow ${index + 1} (${slideshow.id})`
|
||||
}
|
||||
|
||||
// Current ID getters
|
||||
export function getCurrentSlideId(): string | undefined {
|
||||
const currentSlide = $currentSlide.get()
|
||||
return currentSlide?.id
|
||||
}
|
||||
|
||||
export function getCurrentSlideShowId(): string | undefined {
|
||||
const currentSlideShow = $currentSlideShow.get()
|
||||
return currentSlideShow?.id
|
||||
}
|
||||
@@ -0,0 +1,118 @@
|
||||
// Style constants used by all CC shapes
|
||||
export const CC_BASE_STYLE_CONSTANTS = {
|
||||
FONT_FAMILY: 'Inter, sans-serif',
|
||||
FONT_SIZES: {
|
||||
small: 12,
|
||||
medium: 14,
|
||||
large: 16,
|
||||
},
|
||||
// Container styles
|
||||
CONTAINER: {
|
||||
borderRadius: '4px',
|
||||
borderWidth: '2px',
|
||||
borderColor: '#e2e8f0',
|
||||
boxShadow: '0 2px 4px var(--color-muted-1)',
|
||||
},
|
||||
HEADER: {
|
||||
height: 32,
|
||||
padding: 8,
|
||||
borderRadius: 4,
|
||||
},
|
||||
CONTENT: {
|
||||
padding: 8,
|
||||
borderRadius: 8,
|
||||
borderWidth: 2,
|
||||
backgroundColor: 'white',
|
||||
},
|
||||
HANDLE: {
|
||||
width: 8,
|
||||
},
|
||||
COLORS: {
|
||||
primary: '#3e6589',
|
||||
primary_dark: '#2e4a69',
|
||||
secondary: '#718096',
|
||||
secondary_dark: '#5a687a',
|
||||
background: '#ffffff',
|
||||
border: '#e2e8f0',
|
||||
text: '#1a202c',
|
||||
textLight: '#718096',
|
||||
},
|
||||
// Minimum dimensions
|
||||
MIN_DIMENSIONS: {
|
||||
width: 100,
|
||||
height: 100,
|
||||
},
|
||||
} as const
|
||||
|
||||
// Calendar specific styles
|
||||
export const CC_CALENDAR_STYLE_CONSTANTS = {
|
||||
// Common button styles
|
||||
COMMON_BUTTON: {
|
||||
border: 'none',
|
||||
borderRadius: '5px',
|
||||
padding: '0.4em 1em',
|
||||
fontSize: '0.95em',
|
||||
textTransform: 'uppercase',
|
||||
letterSpacing: '0.05em',
|
||||
cursor: 'pointer',
|
||||
transition: 'background-color 0.3s ease',
|
||||
boxShadow: '0 4px 6px rgba(0, 0, 0, 0.1)',
|
||||
},
|
||||
|
||||
// Application button styles
|
||||
APPLICATION_BUTTON: {
|
||||
backgroundColor: '#4f80ff',
|
||||
color: '#fff',
|
||||
},
|
||||
|
||||
// Option button styles
|
||||
OPTION_BUTTON: {
|
||||
backgroundColor: '#f0f4f9',
|
||||
color: '#2c3e50',
|
||||
border: '1px solid #ddd',
|
||||
},
|
||||
|
||||
// Calendar event styles
|
||||
EVENT: {
|
||||
mainFrame: {
|
||||
backgroundColor: 'transparent',
|
||||
padding: '0px',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
minHeight: '100%',
|
||||
borderRadius: '4px',
|
||||
},
|
||||
title: {
|
||||
fontSize: '1.1em',
|
||||
fontWeight: 'normal',
|
||||
textAlign: 'center',
|
||||
overflow: 'hidden',
|
||||
textOverflow: 'ellipsis',
|
||||
whiteSpace: 'nowrap',
|
||||
opacity: 1,
|
||||
padding: '0px 0px',
|
||||
width: '100%',
|
||||
letterSpacing: '0.02em',
|
||||
margin: '0px 0px',
|
||||
}
|
||||
}
|
||||
} as const
|
||||
|
||||
// Slideshow specific styles
|
||||
export const CC_SLIDESHOW_STYLE_CONSTANTS = {
|
||||
DEFAULT_SLIDE_WIDTH: 1280,
|
||||
DEFAULT_SLIDE_HEIGHT: 720,
|
||||
SLIDE_HEADER_HEIGHT: 32,
|
||||
SLIDE_HEADER_PADDING: 8,
|
||||
SLIDE_CONTENT_PADDING: 0,
|
||||
SLIDE_BORDER_RADIUS: 4,
|
||||
SLIDE_BORDER_WIDTH: 1,
|
||||
SLIDE_SPACING: 16,
|
||||
SLIDE_COLORS: {
|
||||
background: '#ffffff',
|
||||
border: '#e2e8f0',
|
||||
text: '#ffffff',
|
||||
secondary: '#718096',
|
||||
},
|
||||
} as const
|
||||
@@ -0,0 +1,342 @@
|
||||
import React from 'react';
|
||||
import { CCBaseShapeUtil } from '../CCBaseShapeUtil';
|
||||
import { TLShapeId } from '@tldraw/tldraw';
|
||||
import { CCBaseShape } from '../cc-types';
|
||||
import { TranscriptionManager } from '../cc-transcription/TranscriptionManager';
|
||||
import { ccShapeProps, getDefaultCCLiveTranscriptionProps } from '../cc-props';
|
||||
import { ccShapeMigrations } from '../cc-migrations';
|
||||
import { CC_BASE_STYLE_CONSTANTS } from '../cc-styles';
|
||||
|
||||
export interface TranscriptionSegment {
|
||||
id: string
|
||||
text: string
|
||||
completed: boolean
|
||||
start: string
|
||||
end: string
|
||||
}
|
||||
|
||||
export interface CCLiveTranscriptionShape extends CCBaseShape {
|
||||
type: 'cc-live-transcription'
|
||||
props: {
|
||||
title: string
|
||||
w: number
|
||||
h: number
|
||||
headerColor: string
|
||||
backgroundColor: string
|
||||
isLocked: boolean
|
||||
isRecording: boolean
|
||||
segments: TranscriptionSegment[]
|
||||
currentSegment?: TranscriptionSegment
|
||||
lastProcessedSegment?: string // Track last processed segment to avoid duplicates
|
||||
}
|
||||
}
|
||||
|
||||
export class CCLiveTranscriptionShapeUtil extends CCBaseShapeUtil<CCLiveTranscriptionShape> {
|
||||
static override type = 'cc-live-transcription' as const;
|
||||
static override props = ccShapeProps.liveTranscription;
|
||||
static override migrations = ccShapeMigrations.liveTranscription;
|
||||
|
||||
override getDefaultProps(): CCLiveTranscriptionShape['props'] {
|
||||
return getDefaultCCLiveTranscriptionProps() as unknown as CCLiveTranscriptionShape['props'];
|
||||
}
|
||||
|
||||
override renderContent = (shape: CCLiveTranscriptionShape) => {
|
||||
return this.renderShapeContent(shape)
|
||||
}
|
||||
|
||||
renderShapeContent = (shape: CCLiveTranscriptionShape) => {
|
||||
const { isRecording, segments, currentSegment } = shape.props;
|
||||
const contentHeight = shape.props.h - CC_BASE_STYLE_CONSTANTS.HEADER.height - 2 * CC_BASE_STYLE_CONSTANTS.CONTENT.padding;
|
||||
const controlsHeight = 80;
|
||||
const transcriptHeight = contentHeight - controlsHeight;
|
||||
|
||||
return (
|
||||
<div style={{
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
pointerEvents: 'all'
|
||||
}}>
|
||||
{/* Microphone Controls */}
|
||||
<div style={{
|
||||
height: controlsHeight,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
gap: '16px',
|
||||
borderBottom: '1px solid #e0e0e0',
|
||||
padding: '8px'
|
||||
}}>
|
||||
<div
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
onPointerDown={(e) => {
|
||||
e.stopPropagation();
|
||||
}}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
this.toggleRecording(shape);
|
||||
}}
|
||||
style={{
|
||||
width: '48px',
|
||||
height: '48px',
|
||||
borderRadius: '50%',
|
||||
border: 'none',
|
||||
backgroundColor: isRecording ? '#f44336' : '#4CAF50',
|
||||
color: 'white',
|
||||
fontSize: '24px',
|
||||
cursor: 'pointer',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
boxShadow: '0 2px 4px rgba(0,0,0,0.2)',
|
||||
transition: 'all 0.3s ease',
|
||||
userSelect: 'none',
|
||||
}}
|
||||
>
|
||||
{isRecording ? '⏹' : '🎤'}
|
||||
</div>
|
||||
<div style={{
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
alignItems: 'flex-start'
|
||||
}}>
|
||||
<div style={{
|
||||
fontSize: '16px',
|
||||
fontWeight: 'bold',
|
||||
color: isRecording ? '#f44336' : '#4CAF50'
|
||||
}}>
|
||||
{isRecording ? 'Recording...' : 'Ready'}
|
||||
</div>
|
||||
<div style={{ fontSize: '12px', color: '#666' }}>
|
||||
{isRecording ? 'Click to stop' : 'Click to start recording'}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Transcription Content */}
|
||||
<AutoScrollContainer height={transcriptHeight}>
|
||||
{/* Completed Segments */}
|
||||
{segments.map((segment) => (
|
||||
<div
|
||||
key={segment.id}
|
||||
style={{
|
||||
padding: '8px',
|
||||
backgroundColor: '#FFF',
|
||||
borderRadius: '8px',
|
||||
fontSize: '14px',
|
||||
lineHeight: '1.4',
|
||||
color: '#000000',
|
||||
whiteSpace: 'pre-wrap',
|
||||
wordBreak: 'break-word',
|
||||
width: '100%',
|
||||
transition: 'color 0.3s ease',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
gap: '2px'
|
||||
}}
|
||||
>
|
||||
<div style={{
|
||||
fontSize: '11px',
|
||||
color: '#888',
|
||||
fontFamily: 'monospace'
|
||||
}}>
|
||||
{segment.start}s - {segment.end}s
|
||||
</div>
|
||||
<div>
|
||||
{segment.text}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
|
||||
{/* Current Segment */}
|
||||
{currentSegment && (
|
||||
<div
|
||||
style={{
|
||||
padding: '8px',
|
||||
backgroundColor: '#f5f5f5',
|
||||
borderRadius: '8px',
|
||||
fontSize: '14px',
|
||||
lineHeight: '1.4',
|
||||
color: '#666666',
|
||||
whiteSpace: 'pre-wrap',
|
||||
wordBreak: 'break-word',
|
||||
width: '100%',
|
||||
transition: 'color 0.3s ease',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
gap: '2px'
|
||||
}}
|
||||
>
|
||||
<div style={{
|
||||
fontSize: '11px',
|
||||
color: '#888',
|
||||
fontFamily: 'monospace'
|
||||
}}>
|
||||
{currentSegment.start}s - {currentSegment.end}s
|
||||
</div>
|
||||
<div>
|
||||
{currentSegment.text}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Initial State */}
|
||||
{!isRecording && segments.length === 0 && !currentSegment && (
|
||||
<div
|
||||
style={{
|
||||
padding: '8px',
|
||||
backgroundColor: '#FFF',
|
||||
borderRadius: '8px',
|
||||
fontSize: '18px',
|
||||
lineHeight: '1.5',
|
||||
color: '#666666',
|
||||
whiteSpace: 'pre-wrap',
|
||||
wordBreak: 'break-word',
|
||||
width: '100%',
|
||||
textAlign: 'center'
|
||||
}}
|
||||
>
|
||||
Click the microphone to start
|
||||
</div>
|
||||
)}
|
||||
</AutoScrollContainer>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
private toggleRecording(shape: CCLiveTranscriptionShape) {
|
||||
console.log('🎤 Toggle recording clicked');
|
||||
const { id } = shape;
|
||||
const { isRecording } = shape.props;
|
||||
|
||||
console.log('Current state:', { id, isRecording });
|
||||
|
||||
// When starting new recording, preserve existing props but reset segments
|
||||
const newProps = !isRecording ? {
|
||||
...shape.props,
|
||||
isRecording: true,
|
||||
segments: [],
|
||||
currentSegment: undefined,
|
||||
lastProcessedSegment: undefined,
|
||||
} : {
|
||||
...shape.props,
|
||||
isRecording: false,
|
||||
};
|
||||
|
||||
this.editor.updateShape<CCLiveTranscriptionShape>({
|
||||
id,
|
||||
type: 'cc-live-transcription',
|
||||
props: newProps,
|
||||
});
|
||||
|
||||
const manager = TranscriptionManager.getManager(this.editor);
|
||||
console.log('Got transcription manager');
|
||||
|
||||
if (!isRecording) {
|
||||
console.log('Starting transcription...');
|
||||
manager.startTranscription(id);
|
||||
} else {
|
||||
console.log('Stopping transcription...');
|
||||
manager.stopTranscription();
|
||||
}
|
||||
}
|
||||
|
||||
updateText(
|
||||
id: TLShapeId,
|
||||
text: string,
|
||||
isConfirmed: boolean,
|
||||
metadata?: { start: string | number, end: string | number }
|
||||
) {
|
||||
console.log('📝 Updating text:', { id, text, isConfirmed, metadata });
|
||||
const shape = this.editor.getShape<CCLiveTranscriptionShape>(id);
|
||||
if (!shape) {
|
||||
console.warn('❌ Shape not found for updating text:', id);
|
||||
return;
|
||||
}
|
||||
|
||||
// Format timestamps consistently
|
||||
const start = typeof metadata?.start === 'number' ? metadata.start.toFixed(3) : metadata?.start;
|
||||
const end = typeof metadata?.end === 'number' ? metadata.end.toFixed(3) : metadata?.end;
|
||||
|
||||
const segmentId = isConfirmed ? crypto.randomUUID() : 'current';
|
||||
const newSegment: TranscriptionSegment = {
|
||||
id: segmentId,
|
||||
text,
|
||||
completed: isConfirmed,
|
||||
start: start || '0.000',
|
||||
end: end || '0.000'
|
||||
};
|
||||
|
||||
// Handle current (incomplete) segment
|
||||
if (!isConfirmed) {
|
||||
// Only update if text has changed
|
||||
if (shape.props.currentSegment?.text !== text) {
|
||||
this.editor.updateShape<CCLiveTranscriptionShape>({
|
||||
id,
|
||||
type: 'cc-live-transcription',
|
||||
props: {
|
||||
...shape.props,
|
||||
currentSegment: newSegment
|
||||
},
|
||||
});
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Handle completed segment
|
||||
let segments = [...shape.props.segments];
|
||||
|
||||
// Check if this segment is different from the last processed one
|
||||
// and not already in our segments list
|
||||
const isDuplicate = segments.some(s => s.text === text);
|
||||
if (shape.props.lastProcessedSegment !== text && !isDuplicate) {
|
||||
// Add new completed segment
|
||||
segments.push(newSegment);
|
||||
|
||||
this.editor.updateShape<CCLiveTranscriptionShape>({
|
||||
id,
|
||||
type: 'cc-live-transcription',
|
||||
props: {
|
||||
...shape.props,
|
||||
segments,
|
||||
lastProcessedSegment: text,
|
||||
// Clear current segment if it matches the completed one
|
||||
currentSegment: shape.props.currentSegment?.text === text ? undefined : shape.props.currentSegment
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
console.log('✅ Text updated');
|
||||
}
|
||||
}
|
||||
|
||||
// Auto-scrolling container component
|
||||
function AutoScrollContainer({ children, height }: { children: React.ReactNode, height: number }) {
|
||||
const containerRef = React.useRef<HTMLDivElement>(null);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (containerRef.current) {
|
||||
containerRef.current.scrollTop = containerRef.current.scrollHeight;
|
||||
}
|
||||
}, [children]);
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={containerRef}
|
||||
style={{
|
||||
height,
|
||||
padding: '16px',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
justifyContent: 'flex-start',
|
||||
gap: '8px',
|
||||
overflow: 'auto',
|
||||
scrollBehavior: 'smooth'
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
import type { Meta, StoryObj } from '@storybook/react';
|
||||
import { CCLiveTranscriptionShapeUtil } from './CCLiveTranscriptionShapeUtil';
|
||||
import { TranscriptionService } from './transcriptionService';
|
||||
import { getDefaultCCLiveTranscriptionProps } from '../cc-props';
|
||||
import { CCLiveTranscriptionShape } from './CCLiveTranscriptionShapeUtil';
|
||||
import { TLShapeId, IndexKey, TLParentId } from '@tldraw/tldraw';
|
||||
import { Editor } from '@tldraw/editor';
|
||||
|
||||
const meta: Meta = {
|
||||
title: 'Components/Transcription',
|
||||
parameters: {
|
||||
layout: 'centered',
|
||||
},
|
||||
tags: ['autodocs'],
|
||||
};
|
||||
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof meta>;
|
||||
|
||||
// Mock editor
|
||||
const mockEditor = {
|
||||
updateShape: () => {},
|
||||
} as unknown as Editor;
|
||||
|
||||
// Mock shape data
|
||||
const mockShape: CCLiveTranscriptionShape = {
|
||||
id: 'shape:1' as TLShapeId,
|
||||
type: 'cc-live-transcription',
|
||||
x: 0,
|
||||
y: 0,
|
||||
rotation: 0,
|
||||
index: 'a1' as IndexKey,
|
||||
parentId: 'page:page' as TLParentId,
|
||||
isLocked: false,
|
||||
opacity: 1,
|
||||
meta: {},
|
||||
typeName: 'shape',
|
||||
props: getDefaultCCLiveTranscriptionProps(),
|
||||
};
|
||||
|
||||
// Basic story showing the transcription component
|
||||
export const Default: Story = {
|
||||
args: {},
|
||||
render: () => {
|
||||
const shapeUtil = new CCLiveTranscriptionShapeUtil(mockEditor);
|
||||
return (
|
||||
<div style={{ width: '800px', height: '600px', border: '1px solid #ccc' }}>
|
||||
{shapeUtil.renderShapeContent(mockShape)}
|
||||
</div>
|
||||
);
|
||||
},
|
||||
};
|
||||
|
||||
// Story showing the transcription service in action
|
||||
export const WithService: Story = {
|
||||
args: {},
|
||||
render: () => {
|
||||
const service = new TranscriptionService('default');
|
||||
const shapeUtil = new CCLiveTranscriptionShapeUtil(mockEditor);
|
||||
|
||||
return (
|
||||
<div style={{ width: '800px', height: '600px', border: '1px solid #ccc' }}>
|
||||
<div style={{ padding: '20px' }}>
|
||||
<h3>Transcription Service Demo</h3>
|
||||
<button
|
||||
onClick={() => service.startTranscription()}
|
||||
style={{ marginRight: '10px', padding: '10px' }}
|
||||
>
|
||||
Start Transcription
|
||||
</button>
|
||||
<button
|
||||
onClick={() => service.stopTranscription()}
|
||||
style={{ padding: '10px' }}
|
||||
>
|
||||
Stop Transcription
|
||||
</button>
|
||||
</div>
|
||||
<div style={{ marginTop: '20px' }}>
|
||||
{shapeUtil.renderShapeContent(mockShape)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,73 @@
|
||||
import { Editor, TLShapeId } from '@tldraw/tldraw';
|
||||
import { TranscriptionService } from './transcriptionService';
|
||||
import { CCLiveTranscriptionShapeUtil } from './CCLiveTranscriptionShapeUtil';
|
||||
|
||||
export class TranscriptionManager {
|
||||
private static instances = new WeakMap<Editor, TranscriptionManager>();
|
||||
private transcriptionService?: TranscriptionService;
|
||||
private currentShapeId?: TLShapeId;
|
||||
private sameOutputCount = 0;
|
||||
private lastText = '';
|
||||
private readonly SAME_OUTPUT_THRESHOLD = 10;
|
||||
|
||||
constructor(private editor: Editor) {}
|
||||
|
||||
static getManager(editor: Editor): TranscriptionManager {
|
||||
let manager = TranscriptionManager.instances.get(editor);
|
||||
if (!manager) {
|
||||
manager = new TranscriptionManager(editor);
|
||||
TranscriptionManager.instances.set(editor, manager);
|
||||
}
|
||||
return manager;
|
||||
}
|
||||
|
||||
startTranscription(shapeId: TLShapeId) {
|
||||
console.log('Starting transcription...');
|
||||
this.currentShapeId = shapeId;
|
||||
this.transcriptionService = new TranscriptionService();
|
||||
|
||||
// Set up callback for transcription updates
|
||||
this.transcriptionService.setTranscriptionCallback((text: string, isFinal: boolean, metadata: { start: number, end: number }) => {
|
||||
console.log('📝 Transcription update received:', { text, metadata });
|
||||
const util = this.editor.getShapeUtil<CCLiveTranscriptionShapeUtil>('cc-live-transcription');
|
||||
if (!util) {
|
||||
console.warn('❌ Shape util not found');
|
||||
return;
|
||||
}
|
||||
console.log('Found transcription util:', !!util);
|
||||
|
||||
// Check if text is stable (same output multiple times)
|
||||
const isStable = text === this.lastText;
|
||||
if (isStable) {
|
||||
this.sameOutputCount++;
|
||||
} else {
|
||||
this.sameOutputCount = 0;
|
||||
this.lastText = text;
|
||||
}
|
||||
|
||||
// Mark as completed if we've seen the same output multiple times or if marked as final
|
||||
const isCompleted = isFinal || this.sameOutputCount >= this.SAME_OUTPUT_THRESHOLD;
|
||||
|
||||
util.updateText(
|
||||
this.currentShapeId!,
|
||||
text,
|
||||
isCompleted,
|
||||
metadata
|
||||
);
|
||||
});
|
||||
|
||||
// Start the transcription service
|
||||
this.transcriptionService.startTranscription();
|
||||
}
|
||||
|
||||
stopTranscription() {
|
||||
console.log('Stopping transcription...');
|
||||
if (this.transcriptionService) {
|
||||
this.transcriptionService.stopTranscription();
|
||||
this.transcriptionService = undefined;
|
||||
}
|
||||
this.currentShapeId = undefined;
|
||||
this.sameOutputCount = 0;
|
||||
this.lastText = '';
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,227 @@
|
||||
import logger from '../../../../debugConfig';
|
||||
|
||||
export interface TranscriptionConfig {
|
||||
language?: string;
|
||||
task?: string;
|
||||
modelSize?: string;
|
||||
useVad?: boolean;
|
||||
}
|
||||
|
||||
export class TranscriptionService {
|
||||
private socket: WebSocket | null = null;
|
||||
private stream: MediaStream | null = null;
|
||||
private audioContext: AudioContext | null = null;
|
||||
private mediaStreamSource: MediaStreamAudioSourceNode | null = null;
|
||||
private workletNode: AudioWorkletNode | null = null;
|
||||
private selectedDeviceId: string = '';
|
||||
private onTranscriptionUpdate: ((text: string, isFinal: boolean, metadata: { start: number, end: number }) => void) | null = null;
|
||||
|
||||
constructor(deviceId: string = '') {
|
||||
this.selectedDeviceId = deviceId;
|
||||
}
|
||||
|
||||
setTranscriptionCallback(callback: (text: string, isFinal: boolean, metadata: { start: number, end: number }) => void) {
|
||||
this.onTranscriptionUpdate = callback;
|
||||
}
|
||||
|
||||
async startTranscription(config: TranscriptionConfig = {}) {
|
||||
console.log('🎙️ Starting transcription service...');
|
||||
|
||||
try {
|
||||
// Get default audio device if none selected
|
||||
if (!this.selectedDeviceId) {
|
||||
console.log('No device selected, getting default device...');
|
||||
const devices = await navigator.mediaDevices.enumerateDevices();
|
||||
const audioDevice = devices.find(device => device.kind === 'audioinput');
|
||||
if (audioDevice) {
|
||||
this.selectedDeviceId = audioDevice.deviceId;
|
||||
console.log('Found default audio device:', audioDevice.label);
|
||||
}
|
||||
}
|
||||
|
||||
if (!this.selectedDeviceId) {
|
||||
logger.error('transcription-service', '⚠️ No audio device available');
|
||||
return;
|
||||
}
|
||||
|
||||
logger.info('transcription-service', '🔊 Accessing user media...');
|
||||
this.stream = await navigator.mediaDevices.getUserMedia({
|
||||
audio: { deviceId: this.selectedDeviceId },
|
||||
});
|
||||
console.log('Got audio stream');
|
||||
|
||||
const uuid = crypto.randomUUID();
|
||||
const wsUrl = import.meta.env.VITE_WHISPERLIVE_URL;
|
||||
console.log('Connecting to WebSocket:', wsUrl);
|
||||
|
||||
const ws = new WebSocket(wsUrl);
|
||||
this.socket = ws;
|
||||
|
||||
const connectionTimeout = setTimeout(() => {
|
||||
if (ws.readyState !== WebSocket.OPEN) {
|
||||
logger.error('transcription-service', '⏰ WebSocket connection timed out');
|
||||
ws.close();
|
||||
}
|
||||
}, 20000);
|
||||
|
||||
ws.onopen = () => {
|
||||
clearTimeout(connectionTimeout);
|
||||
logger.info('transcription-service', '✅ WebSocket connected');
|
||||
|
||||
// Send initial configuration message
|
||||
const message = JSON.stringify({
|
||||
uid: uuid,
|
||||
language: config.language || 'en',
|
||||
task: config.task || 'transcribe',
|
||||
model: config.modelSize || 'small',
|
||||
use_vad: config.useVad ?? true,
|
||||
});
|
||||
|
||||
ws.send(message);
|
||||
this.setupAudioProcessing();
|
||||
};
|
||||
|
||||
ws.onerror = (error) => {
|
||||
logger.error('transcription-service', '❌ WebSocket error:', error);
|
||||
};
|
||||
|
||||
ws.onclose = () => {
|
||||
logger.info('transcription-service', '🔌 WebSocket closed');
|
||||
this.cleanup();
|
||||
};
|
||||
|
||||
ws.onmessage = (event) => {
|
||||
const data = JSON.parse(event.data);
|
||||
if (data.uid !== uuid) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (data.status === 'WAIT') {
|
||||
logger.info('transcription-service', `⏳ Wait time: ${Math.round(data.message)} minutes`);
|
||||
this.cleanup();
|
||||
return;
|
||||
}
|
||||
|
||||
if (data.message === 'DISCONNECT') {
|
||||
logger.info('transcription-service', '🔕 Server requested disconnection');
|
||||
this.cleanup();
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.onTranscriptionUpdate && data.segments) {
|
||||
// Get the last segment which is the current one being updated
|
||||
const lastSegment = data.segments[data.segments.length - 1];
|
||||
|
||||
// Process completed segments
|
||||
let lastCompletedText = '';
|
||||
for (let i = 0; i < data.segments.length - 1; i++) {
|
||||
const segment = data.segments[i];
|
||||
// Only send update if this segment is different from the last one
|
||||
if (segment.text.trim() !== lastCompletedText.trim()) {
|
||||
this.onTranscriptionUpdate(
|
||||
segment.text,
|
||||
segment.completed ?? true, // Server marks completed segments
|
||||
{
|
||||
start: parseFloat(segment.start),
|
||||
end: parseFloat(segment.end)
|
||||
}
|
||||
);
|
||||
lastCompletedText = segment.text;
|
||||
}
|
||||
}
|
||||
|
||||
// Update the current (incomplete) segment only if it's different from the last completed one
|
||||
if (lastSegment && lastSegment.text.trim() !== lastCompletedText.trim()) {
|
||||
this.onTranscriptionUpdate(
|
||||
lastSegment.text,
|
||||
lastSegment.completed ?? false, // Last segment is typically incomplete unless marked otherwise
|
||||
{
|
||||
start: parseFloat(lastSegment.start),
|
||||
end: parseFloat(lastSegment.end)
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
};
|
||||
} catch (error) {
|
||||
logger.error('transcription-service', '❌ Error starting transcription:', error);
|
||||
this.cleanup();
|
||||
}
|
||||
}
|
||||
|
||||
private async setupAudioProcessing() {
|
||||
if (!this.stream || !this.socket) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
this.audioContext = new AudioContext();
|
||||
|
||||
// Load and register the audio worklet
|
||||
await this.audioContext.audioWorklet.addModule('/audioWorklet.js');
|
||||
|
||||
this.mediaStreamSource = this.audioContext.createMediaStreamSource(this.stream);
|
||||
this.workletNode = new AudioWorkletNode(this.audioContext, 'audio-processor');
|
||||
|
||||
// Handle audio data from the worklet
|
||||
this.workletNode.port.onmessage = (event) => {
|
||||
if (this.socket?.readyState === WebSocket.OPEN) {
|
||||
const resampledData = this.resampleTo16kHZ(event.data, this.audioContext!.sampleRate);
|
||||
this.socket.send(resampledData);
|
||||
}
|
||||
};
|
||||
|
||||
this.mediaStreamSource.connect(this.workletNode);
|
||||
this.workletNode.connect(this.audioContext.destination);
|
||||
} catch (error) {
|
||||
console.error('Error setting up audio processing:', error);
|
||||
}
|
||||
}
|
||||
|
||||
private resampleTo16kHZ(audioData: Float32Array, origSampleRate: number): Float32Array {
|
||||
const ratio = origSampleRate / 16000;
|
||||
const newLength = Math.round(audioData.length / ratio);
|
||||
const result = new Float32Array(newLength);
|
||||
|
||||
for (let i = 0; i < newLength; i++) {
|
||||
const pos = i * ratio;
|
||||
const leftPos = Math.floor(pos);
|
||||
const rightPos = Math.ceil(pos);
|
||||
const weight = pos - leftPos;
|
||||
result[i] = audioData[leftPos] * (1 - weight) + (audioData[rightPos] || 0) * weight;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
stopTranscription() {
|
||||
this.cleanup();
|
||||
}
|
||||
|
||||
private cleanup() {
|
||||
if (this.workletNode) {
|
||||
this.workletNode.disconnect();
|
||||
this.workletNode = null;
|
||||
}
|
||||
|
||||
if (this.mediaStreamSource) {
|
||||
this.mediaStreamSource.disconnect();
|
||||
this.mediaStreamSource = null;
|
||||
}
|
||||
|
||||
if (this.audioContext) {
|
||||
this.audioContext.close();
|
||||
this.audioContext = null;
|
||||
}
|
||||
|
||||
if (this.stream) {
|
||||
this.stream.getTracks().forEach(track => track.stop());
|
||||
this.stream = null;
|
||||
}
|
||||
|
||||
if (this.socket) {
|
||||
this.socket.close();
|
||||
this.socket = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
import { TLBaseShape } from '@tldraw/tldraw'
|
||||
import { CCBaseProps } from './cc-props'
|
||||
|
||||
export interface CCBaseShape extends TLBaseShape<string, CCBaseProps> {}
|
||||
@@ -0,0 +1,98 @@
|
||||
import { CCBaseShapeUtil } from '../CCBaseShapeUtil'
|
||||
import { CCBaseShape } from '../cc-types'
|
||||
import { ccShapeProps, getDefaultCCWebBrowserProps } from '../cc-props'
|
||||
import { ccShapeMigrations } from '../cc-migrations'
|
||||
import { Rectangle2d } from 'tldraw'
|
||||
import { WebBrowserComponent } from './WebBrowserComponent'
|
||||
import { customEmbeds } from '../../embeds'
|
||||
|
||||
export interface CCWebBrowserShape extends CCBaseShape {
|
||||
type: 'cc-web-browser'
|
||||
props: CCBaseShape['props'] & {
|
||||
url: string
|
||||
history: string[]
|
||||
currentHistoryIndex: number
|
||||
isLoading: boolean
|
||||
}
|
||||
}
|
||||
|
||||
export class CCWebBrowserShapeUtil extends CCBaseShapeUtil<CCWebBrowserShape> {
|
||||
static override type = 'cc-web-browser' as const;
|
||||
static override props = ccShapeProps.webBrowser;
|
||||
static override migrations = ccShapeMigrations.webBrowser;
|
||||
|
||||
override getDefaultProps(): CCWebBrowserShape['props'] {
|
||||
return getDefaultCCWebBrowserProps() as CCWebBrowserShape['props'];
|
||||
}
|
||||
|
||||
override isAspectRatioLocked = () => false
|
||||
override canResize = () => true
|
||||
override canBind = () => false
|
||||
override hideResizeHandles = () => false
|
||||
override hideRotateHandle = () => false
|
||||
override canEdit = () => false
|
||||
|
||||
override renderContent = (shape: CCWebBrowserShape) => {
|
||||
return (
|
||||
<div style={{ width: '100%', height: '100%', pointerEvents: 'all' }}>
|
||||
<WebBrowserComponent shape={shape} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
override getGeometry(shape: CCWebBrowserShape) {
|
||||
return new Rectangle2d({
|
||||
width: shape.props.w,
|
||||
height: shape.props.h,
|
||||
isFilled: true,
|
||||
})
|
||||
}
|
||||
|
||||
override onResize = (
|
||||
shape: CCWebBrowserShape,
|
||||
info: { initialShape: CCWebBrowserShape; scaleX: number; scaleY: number }
|
||||
) => {
|
||||
const { initialShape, scaleX, scaleY } = info
|
||||
const newW = Math.max(400, Math.round(initialShape.props.w * scaleX))
|
||||
const newH = Math.max(300, Math.round(initialShape.props.h * scaleY))
|
||||
|
||||
return {
|
||||
props: {
|
||||
...shape.props,
|
||||
w: newW,
|
||||
h: newH,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
shouldRender = (prev: CCWebBrowserShape, next: CCWebBrowserShape) => {
|
||||
return (
|
||||
prev.props.w !== next.props.w ||
|
||||
prev.props.h !== next.props.h ||
|
||||
prev.props.url !== next.props.url ||
|
||||
prev.props.isLoading !== next.props.isLoading ||
|
||||
prev.props.currentHistoryIndex !== next.props.currentHistoryIndex ||
|
||||
prev.props.history.length !== next.props.history.length
|
||||
)
|
||||
}
|
||||
|
||||
static isEmbeddableUrl(url: string): { isEmbeddable: boolean; embedType?: string } {
|
||||
try {
|
||||
const urlObj = new URL(url);
|
||||
|
||||
// Check against our custom embeds
|
||||
for (const embed of customEmbeds) {
|
||||
if (embed.hostnames.some(hostname =>
|
||||
urlObj.hostname === hostname ||
|
||||
urlObj.hostname.endsWith(`.${hostname}`)
|
||||
)) {
|
||||
return { isEmbeddable: true, embedType: embed.type };
|
||||
}
|
||||
}
|
||||
|
||||
return { isEmbeddable: false };
|
||||
} catch (e) {
|
||||
return { isEmbeddable: false };
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,200 @@
|
||||
import React, { useState } from 'react'
|
||||
import { useEditor } from '@tldraw/tldraw'
|
||||
import { IconButton, TextField, CircularProgress } from '@mui/material'
|
||||
import ArrowBackIcon from '@mui/icons-material/ArrowBack'
|
||||
import ArrowForwardIcon from '@mui/icons-material/ArrowForward'
|
||||
import RefreshIcon from '@mui/icons-material/Refresh'
|
||||
import { CCWebBrowserShape } from './CCWebBrowserUtil'
|
||||
|
||||
interface WebBrowserComponentProps {
|
||||
shape: CCWebBrowserShape
|
||||
}
|
||||
|
||||
export const WebBrowserComponent: React.FC<WebBrowserComponentProps> = ({ shape }) => {
|
||||
const editor = useEditor()
|
||||
const [urlInput, setUrlInput] = useState(shape.props.url)
|
||||
const [error, setError] = useState<string | null>(null)
|
||||
|
||||
const handleUrlSubmit = (e: React.FormEvent) => {
|
||||
e.preventDefault()
|
||||
setError(null)
|
||||
const newUrl = urlInput.startsWith('http') ? urlInput : `https://${urlInput}`
|
||||
|
||||
editor.updateShape({
|
||||
id: shape.id,
|
||||
type: 'cc-web-browser',
|
||||
props: {
|
||||
...shape.props,
|
||||
url: newUrl,
|
||||
history: [...shape.props.history.slice(0, shape.props.currentHistoryIndex + 1), newUrl],
|
||||
currentHistoryIndex: shape.props.currentHistoryIndex + 1,
|
||||
isLoading: true,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
const handleBack = () => {
|
||||
if (shape.props.currentHistoryIndex > 0) {
|
||||
setError(null)
|
||||
const newIndex = shape.props.currentHistoryIndex - 1
|
||||
editor.updateShape({
|
||||
id: shape.id,
|
||||
type: 'cc-web-browser',
|
||||
props: {
|
||||
...shape.props,
|
||||
url: shape.props.history[newIndex],
|
||||
currentHistoryIndex: newIndex,
|
||||
isLoading: true,
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const handleForward = () => {
|
||||
if (shape.props.currentHistoryIndex < shape.props.history.length - 1) {
|
||||
setError(null)
|
||||
const newIndex = shape.props.currentHistoryIndex + 1
|
||||
editor.updateShape({
|
||||
id: shape.id,
|
||||
type: 'cc-web-browser',
|
||||
props: {
|
||||
...shape.props,
|
||||
url: shape.props.history[newIndex],
|
||||
currentHistoryIndex: newIndex,
|
||||
isLoading: true,
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const handleRefresh = () => {
|
||||
setError(null)
|
||||
editor.updateShape({
|
||||
id: shape.id,
|
||||
type: 'cc-web-browser',
|
||||
props: {
|
||||
...shape.props,
|
||||
isLoading: true,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
const handleIframeLoad = () => {
|
||||
editor.updateShape({
|
||||
id: shape.id,
|
||||
type: 'cc-web-browser',
|
||||
props: {
|
||||
...shape.props,
|
||||
isLoading: false,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
const handleIframeError = () => {
|
||||
setError('Failed to load the webpage. This might be due to security restrictions or the website not allowing embedding.')
|
||||
editor.updateShape({
|
||||
id: shape.id,
|
||||
type: 'cc-web-browser',
|
||||
props: {
|
||||
...shape.props,
|
||||
isLoading: false,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
return (
|
||||
<div style={{ display: 'flex', flexDirection: 'column', height: '100%' }}>
|
||||
<div style={{
|
||||
display: 'flex',
|
||||
gap: '8px',
|
||||
padding: '8px',
|
||||
alignItems: 'center',
|
||||
backgroundColor: '#f5f5f5',
|
||||
borderBottom: '1px solid #ddd'
|
||||
}}>
|
||||
<IconButton
|
||||
size="small"
|
||||
onClick={handleBack}
|
||||
disabled={shape.props.currentHistoryIndex <= 0}
|
||||
>
|
||||
<ArrowBackIcon />
|
||||
</IconButton>
|
||||
<IconButton
|
||||
size="small"
|
||||
onClick={handleForward}
|
||||
disabled={shape.props.currentHistoryIndex >= shape.props.history.length - 1}
|
||||
>
|
||||
<ArrowForwardIcon />
|
||||
</IconButton>
|
||||
<IconButton
|
||||
size="small"
|
||||
onClick={handleRefresh}
|
||||
>
|
||||
<RefreshIcon />
|
||||
</IconButton>
|
||||
<form onSubmit={handleUrlSubmit} style={{ flex: 1 }}>
|
||||
<TextField
|
||||
fullWidth
|
||||
size="small"
|
||||
value={urlInput}
|
||||
onChange={(e) => setUrlInput(e.target.value)}
|
||||
placeholder="Enter URL or search..."
|
||||
error={!!error}
|
||||
helperText={error}
|
||||
/>
|
||||
</form>
|
||||
</div>
|
||||
<div style={{
|
||||
position: 'relative',
|
||||
flex: 1,
|
||||
backgroundColor: '#fff'
|
||||
}}>
|
||||
{shape.props.isLoading && (
|
||||
<div style={{
|
||||
position: 'absolute',
|
||||
top: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
backgroundColor: 'rgba(255, 255, 255, 0.8)',
|
||||
zIndex: 1
|
||||
}}>
|
||||
<CircularProgress />
|
||||
</div>
|
||||
)}
|
||||
{shape.props.url && (
|
||||
<iframe
|
||||
src={shape.props.url}
|
||||
style={{
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
border: 'none'
|
||||
}}
|
||||
sandbox="allow-same-origin allow-scripts allow-popups allow-forms"
|
||||
referrerPolicy="no-referrer"
|
||||
loading="lazy"
|
||||
onLoad={handleIframeLoad}
|
||||
onError={handleIframeError}
|
||||
title="Web Browser"
|
||||
/>
|
||||
)}
|
||||
{error && !shape.props.isLoading && (
|
||||
<div style={{
|
||||
position: 'absolute',
|
||||
top: '50%',
|
||||
left: '50%',
|
||||
transform: 'translate(-50%, -50%)',
|
||||
textAlign: 'center',
|
||||
color: '#d32f2f',
|
||||
padding: '16px'
|
||||
}}>
|
||||
{error}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,247 @@
|
||||
import React, { useEffect, useRef, useState, memo } from 'react'
|
||||
import { CCBaseShapeUtil } from '../CCBaseShapeUtil'
|
||||
import { CCBaseShape } from '../cc-types'
|
||||
import { CC_YOUTUBE_EMBED_STYLE_CONSTANTS } from './cc-youtube-embed-styles'
|
||||
import { getYoutubeTranscript, extractVideoId } from './youtubeService'
|
||||
import { formatTime, Player, OnStateChangeEvent, PlayerState } from './cc-youtube-embed-helpers'
|
||||
import { ccShapeProps } from '../cc-props'
|
||||
import { getDefaultCCYoutubeEmbedProps } from '../cc-props'
|
||||
|
||||
// Add YouTube types to the global Window interface
|
||||
declare global {
|
||||
interface Window {
|
||||
YT: {
|
||||
Player: new (
|
||||
elementId: string,
|
||||
config: {
|
||||
events?: {
|
||||
onReady?: () => void
|
||||
onStateChange?: (event: OnStateChangeEvent) => void
|
||||
}
|
||||
}
|
||||
) => Player
|
||||
PlayerState: PlayerState
|
||||
}
|
||||
onYouTubeIframeAPIReady: () => void
|
||||
}
|
||||
}
|
||||
|
||||
interface TranscriptLine {
|
||||
start: number
|
||||
duration: number
|
||||
text: string
|
||||
}
|
||||
|
||||
export interface CCYoutubeEmbedShape extends CCBaseShape {
|
||||
type: 'cc-youtube-embed'
|
||||
props: {
|
||||
title: string
|
||||
w: number
|
||||
h: number
|
||||
headerColor: string
|
||||
backgroundColor: string
|
||||
isLocked: boolean
|
||||
video_url: string
|
||||
transcript: TranscriptLine[]
|
||||
transcriptVisible: boolean
|
||||
}
|
||||
}
|
||||
|
||||
const YoutubeEmbed = memo(({ shape }: { shape: CCYoutubeEmbedShape }) => {
|
||||
const [transcript, setTranscript] = useState<TranscriptLine[]>(shape.props.transcript)
|
||||
const [currentTime, setCurrentTime] = useState<number>(0)
|
||||
const [currentLineIndex, setCurrentLineIndex] = useState<number | null>(null)
|
||||
const transcriptRef = useRef<HTMLDivElement | null>(null)
|
||||
const hasFetchedTranscript = useRef<boolean>(false)
|
||||
const playerRef = useRef<Player | null>(null)
|
||||
const timeUpdateIntervalRef = useRef<ReturnType<typeof setTimeout> | null>(null)
|
||||
|
||||
useEffect(() => {
|
||||
const fetchTranscript = async () => {
|
||||
if (hasFetchedTranscript.current) return
|
||||
|
||||
try {
|
||||
const transcriptData = await getYoutubeTranscript(shape.props.video_url)
|
||||
hasFetchedTranscript.current = true
|
||||
setTranscript(transcriptData)
|
||||
} catch (error) {
|
||||
console.error('Error fetching transcript:', error)
|
||||
}
|
||||
}
|
||||
|
||||
fetchTranscript()
|
||||
|
||||
return () => {
|
||||
hasFetchedTranscript.current = false
|
||||
}
|
||||
}, [shape.props.video_url])
|
||||
|
||||
useEffect(() => {
|
||||
const onYouTubeIframeAPIReady = () => {
|
||||
if (playerRef.current) return
|
||||
|
||||
playerRef.current = new window.YT.Player('youtube-player', {
|
||||
events: {
|
||||
'onReady': () => console.log('YouTube player is ready'),
|
||||
'onStateChange': (event: OnStateChangeEvent) => {
|
||||
if (event.data === window.YT.PlayerState.PLAYING) {
|
||||
startTimeUpdates()
|
||||
} else {
|
||||
stopTimeUpdates()
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const startTimeUpdates = () => {
|
||||
timeUpdateIntervalRef.current = setInterval(() => {
|
||||
if (playerRef.current?.getCurrentTime) {
|
||||
const time = playerRef.current.getCurrentTime()
|
||||
setCurrentTime(time)
|
||||
}
|
||||
}, 100)
|
||||
}
|
||||
|
||||
const stopTimeUpdates = () => {
|
||||
if (timeUpdateIntervalRef.current) {
|
||||
clearInterval(timeUpdateIntervalRef.current)
|
||||
timeUpdateIntervalRef.current = null
|
||||
}
|
||||
}
|
||||
|
||||
// Load YouTube iframe API
|
||||
if (!document.querySelector('script[src="https://www.youtube.com/iframe_api"]')) {
|
||||
const tag = document.createElement('script')
|
||||
tag.src = "https://www.youtube.com/iframe_api"
|
||||
const firstScriptTag = document.getElementsByTagName('script')[0]
|
||||
firstScriptTag.parentNode?.insertBefore(tag, firstScriptTag)
|
||||
}
|
||||
|
||||
window.onYouTubeIframeAPIReady = onYouTubeIframeAPIReady
|
||||
|
||||
return () => {
|
||||
stopTimeUpdates()
|
||||
if (playerRef.current?.destroy) {
|
||||
playerRef.current.destroy()
|
||||
}
|
||||
playerRef.current = null
|
||||
}
|
||||
}, [shape.props.video_url])
|
||||
|
||||
useEffect(() => {
|
||||
const newCurrentLineIndex = transcript.findIndex(
|
||||
(line) => {
|
||||
const lineEndTime = line.start + line.duration;
|
||||
return currentTime >= line.start && currentTime <= lineEndTime;
|
||||
}
|
||||
)
|
||||
|
||||
if (newCurrentLineIndex !== -1 && newCurrentLineIndex !== currentLineIndex) {
|
||||
setCurrentLineIndex(newCurrentLineIndex)
|
||||
|
||||
// Scroll the active line into view
|
||||
if (transcriptRef.current) {
|
||||
const transcriptContainer = transcriptRef.current
|
||||
const lineElement = transcriptContainer.children[newCurrentLineIndex] as HTMLElement
|
||||
if (lineElement) {
|
||||
lineElement.scrollIntoView({ behavior: 'smooth', block: 'center' })
|
||||
}
|
||||
}
|
||||
}
|
||||
}, [currentTime, transcript, currentLineIndex])
|
||||
|
||||
return (
|
||||
<div style={{ display: 'flex', width: '100%', height: '100%' }}>
|
||||
<div style={CC_YOUTUBE_EMBED_STYLE_CONSTANTS.VIDEO.container}>
|
||||
<iframe
|
||||
id="youtube-player"
|
||||
style={{
|
||||
...CC_YOUTUBE_EMBED_STYLE_CONSTANTS.VIDEO.iframe,
|
||||
pointerEvents: 'all'
|
||||
}}
|
||||
src={`https://www.youtube.com/embed/${extractVideoId(shape.props.video_url)}?enablejsapi=1`}
|
||||
allowFullScreen
|
||||
/>
|
||||
</div>
|
||||
{shape.props.transcriptVisible && (
|
||||
<div
|
||||
ref={transcriptRef}
|
||||
style={{
|
||||
...CC_YOUTUBE_EMBED_STYLE_CONSTANTS.TRANSCRIPT.container,
|
||||
pointerEvents: 'all'
|
||||
}}
|
||||
>
|
||||
<h3 style={CC_YOUTUBE_EMBED_STYLE_CONSTANTS.TRANSCRIPT.title}>Transcript</h3>
|
||||
{transcript.length > 0 ? (
|
||||
transcript.map((line, index) => (
|
||||
<div
|
||||
key={index}
|
||||
style={{
|
||||
...CC_YOUTUBE_EMBED_STYLE_CONSTANTS.TRANSCRIPT.line,
|
||||
...(index === currentLineIndex ? CC_YOUTUBE_EMBED_STYLE_CONSTANTS.TRANSCRIPT.activeLine : {})
|
||||
}}
|
||||
>
|
||||
<span style={CC_YOUTUBE_EMBED_STYLE_CONSTANTS.TRANSCRIPT.timestamp}>
|
||||
{formatTime(line.start)}:
|
||||
</span>
|
||||
{line.text}
|
||||
</div>
|
||||
))
|
||||
) : (
|
||||
<p style={CC_YOUTUBE_EMBED_STYLE_CONSTANTS.TRANSCRIPT.loading}>Loading transcript...</p>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
})
|
||||
|
||||
export class CCYoutubeEmbedShapeUtil extends CCBaseShapeUtil<CCYoutubeEmbedShape> {
|
||||
static override type = 'cc-youtube-embed'
|
||||
static override props = ccShapeProps['cc-youtube-embed']
|
||||
|
||||
override getDefaultProps(): CCYoutubeEmbedShape['props'] {
|
||||
return getDefaultCCYoutubeEmbedProps()
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
override isAspectRatioLocked(shape: CCYoutubeEmbedShape) {
|
||||
return true
|
||||
}
|
||||
|
||||
override getToolbarItems(shape: CCYoutubeEmbedShape) {
|
||||
return [
|
||||
{
|
||||
id: 'toggle-transcript',
|
||||
icon: shape.props.transcriptVisible ? '📖' : '📕',
|
||||
label: shape.props.transcriptVisible ? 'Hide Transcript' : 'Show Transcript',
|
||||
onClick: (e: React.MouseEvent, baseShape: CCBaseShape) => {
|
||||
console.log('Toggle transcript clicked')
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
|
||||
const youtubeShape = baseShape as CCYoutubeEmbedShape
|
||||
console.log('Current visibility:', youtubeShape.props.transcriptVisible)
|
||||
|
||||
const newProps = {
|
||||
...youtubeShape.props,
|
||||
transcriptVisible: !youtubeShape.props.transcriptVisible
|
||||
}
|
||||
|
||||
console.log('New visibility:', newProps.transcriptVisible)
|
||||
this.editor.updateShape({
|
||||
id: youtubeShape.id,
|
||||
type: 'cc-youtube-embed',
|
||||
props: newProps
|
||||
})
|
||||
},
|
||||
isActive: shape.props.transcriptVisible,
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
override renderContent = (shape: CCYoutubeEmbedShape) => {
|
||||
return <YoutubeEmbed shape={shape} />
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
export interface Player {
|
||||
getCurrentTime(): number;
|
||||
destroy(): void;
|
||||
}
|
||||
|
||||
export interface OnStateChangeEvent {
|
||||
data: number;
|
||||
}
|
||||
|
||||
export interface PlayerState {
|
||||
PLAYING: number;
|
||||
PAUSED: number;
|
||||
ENDED: number;
|
||||
}
|
||||
|
||||
export function formatTime(seconds: number): string {
|
||||
const minutes = Math.floor(seconds / 60)
|
||||
const remainingSeconds = Math.floor(seconds % 60)
|
||||
return `${minutes}:${remainingSeconds.toString().padStart(2, '0')}`
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
export const CC_YOUTUBE_EMBED_STYLE_CONSTANTS = {
|
||||
VIDEO: {
|
||||
container: {
|
||||
flex: 2,
|
||||
padding: '10px',
|
||||
minWidth: '300px',
|
||||
},
|
||||
iframe: {
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
border: 'none',
|
||||
},
|
||||
},
|
||||
TRANSCRIPT: {
|
||||
container: {
|
||||
flex: 1,
|
||||
padding: '10px',
|
||||
overflowY: 'auto',
|
||||
maxHeight: '100%',
|
||||
minWidth: '200px',
|
||||
backgroundColor: '#f5f5f5',
|
||||
borderLeft: '1px solid #ddd',
|
||||
},
|
||||
title: {
|
||||
margin: '0 0 10px 0',
|
||||
fontSize: '16px',
|
||||
fontWeight: 'bold',
|
||||
color: '#333',
|
||||
},
|
||||
line: {
|
||||
padding: '5px',
|
||||
marginBottom: '5px',
|
||||
borderRadius: '4px',
|
||||
fontSize: '14px',
|
||||
color: '#333',
|
||||
backgroundColor: '#fff',
|
||||
border: '1px solid #eee',
|
||||
},
|
||||
activeLine: {
|
||||
backgroundColor: '#e3f2fd',
|
||||
border: '1px solid #2196f3',
|
||||
color: '#1565c0',
|
||||
fontWeight: 'bold',
|
||||
boxShadow: '0 1px 3px rgba(0,0,0,0.12)',
|
||||
transform: 'scale(1.02)',
|
||||
transition: 'all 0.2s ease-in-out',
|
||||
},
|
||||
timestamp: {
|
||||
color: '#666',
|
||||
marginRight: '8px',
|
||||
fontWeight: 'bold',
|
||||
},
|
||||
loading: {
|
||||
color: '#666',
|
||||
fontStyle: 'italic',
|
||||
},
|
||||
},
|
||||
TOOLS: {
|
||||
container: {
|
||||
padding: '10px',
|
||||
display: 'flex',
|
||||
flexDirection: 'column' as const,
|
||||
gap: '10px',
|
||||
},
|
||||
button: {
|
||||
padding: '8px 12px',
|
||||
backgroundColor: '#f0f0f0',
|
||||
border: '1px solid #ddd',
|
||||
borderRadius: '4px',
|
||||
cursor: 'pointer',
|
||||
color: '#333',
|
||||
fontSize: '14px',
|
||||
'&:hover': {
|
||||
backgroundColor: '#e0e0e0',
|
||||
},
|
||||
},
|
||||
},
|
||||
} as const
|
||||
@@ -0,0 +1,29 @@
|
||||
import axios from './../../../../axiosConfig';
|
||||
|
||||
export interface TranscriptLine {
|
||||
start: number
|
||||
duration: number
|
||||
text: string
|
||||
}
|
||||
|
||||
export async function getYoutubeTranscript(videoUrl: string): Promise<TranscriptLine[]> {
|
||||
try {
|
||||
const videoId = extractVideoId(videoUrl)
|
||||
if (!videoId) {
|
||||
throw new Error('Invalid YouTube URL')
|
||||
}
|
||||
|
||||
const response = await axios.get(`/external/youtube-proxy?videoId=${videoId}`)
|
||||
console.log('Got Youtube video data:', response.data)
|
||||
return response.data.transcript
|
||||
} catch (error) {
|
||||
console.error('Error fetching YouTube video data:', error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
export function extractVideoId(url: string): string | null {
|
||||
const regex = /(?:youtube\.com\/(?:[^\/]+\/.+\/|(?:v|e(?:mbed)?)\/|.*[?&]v=)|youtu\.be\/)([^"&?\/\s]{11})/
|
||||
const match = url.match(regex)
|
||||
return match ? match[1] : null
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
import { Editor, TLShapeId, createShapeId } from '@tldraw/tldraw'
|
||||
import { CC_SHAPE_CONFIGS } from '../cc-configs'
|
||||
|
||||
export const createCalendarShape = (
|
||||
editor: Editor,
|
||||
baseProps: {
|
||||
id: TLShapeId
|
||||
x: number
|
||||
y: number
|
||||
rotation: number
|
||||
isLocked: boolean
|
||||
}
|
||||
) => {
|
||||
const config = CC_SHAPE_CONFIGS['cc-calendar']
|
||||
|
||||
editor.createShape({
|
||||
...baseProps,
|
||||
type: 'cc-calendar',
|
||||
props: {
|
||||
...config.defaultProps,
|
||||
w: config.width,
|
||||
h: config.height,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
export const createCalendarShapeAtCenter = (editor: Editor) => {
|
||||
if (!editor) return;
|
||||
|
||||
const { x, y } = editor.getViewportScreenCenter();
|
||||
const config = CC_SHAPE_CONFIGS['cc-calendar'];
|
||||
const shapeId = createShapeId();
|
||||
|
||||
createCalendarShape(editor, {
|
||||
id: shapeId,
|
||||
x: x - config.xOffset,
|
||||
y: y - config.yOffset,
|
||||
rotation: 0,
|
||||
isLocked: false,
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
import { Editor, createShapeId, TLShapeId, IndexKey } from '@tldraw/tldraw'
|
||||
import { ccGraphShapeProps, getDefaultCCUserNodeProps } from '../cc-graph/cc-graph-props'
|
||||
import { NODE_THEMES, NODE_TYPE_THEMES } from '../cc-graph/cc-graph-styles'
|
||||
import { GraphShapeType, CCUserNodeProps } from '../cc-graph/cc-graph-types'
|
||||
import { logger } from '../../../../debugConfig'
|
||||
|
||||
// Create a graph shape on the canvas
|
||||
export const createGraphShape = (
|
||||
editor: Editor,
|
||||
shapeType: GraphShapeType,
|
||||
point = { x: 0, y: 0 }
|
||||
) => {
|
||||
if (!ccGraphShapeProps || !ccGraphShapeProps[shapeType]) {
|
||||
console.warn(`Invalid shape type: ${shapeType}`);
|
||||
return;
|
||||
}
|
||||
|
||||
editor.createShape({
|
||||
id: createShapeId(),
|
||||
type: shapeType,
|
||||
x: point.x,
|
||||
y: point.y,
|
||||
});
|
||||
}
|
||||
|
||||
export const createUserNodeFromProfile = (
|
||||
editor: Editor,
|
||||
userNode: CCUserNodeProps,
|
||||
x: number = 0,
|
||||
y: number = 0
|
||||
): TLShapeId | null => {
|
||||
try {
|
||||
if (!userNode) {
|
||||
logger.error('graph-shape-user', '❌ Cannot create user node - no user data')
|
||||
return null
|
||||
}
|
||||
|
||||
const id = createShapeId()
|
||||
const theme = NODE_THEMES[NODE_TYPE_THEMES['cc-user-node']]
|
||||
editor.createShape({
|
||||
id,
|
||||
type: 'cc-user-node' as const,
|
||||
x,
|
||||
y,
|
||||
rotation: 0,
|
||||
index: 'a1' as IndexKey,
|
||||
isLocked: false,
|
||||
opacity: 1,
|
||||
props: {
|
||||
...getDefaultCCUserNodeProps(),
|
||||
headerColor: theme.headerColor,
|
||||
title: userNode.user_email,
|
||||
unique_id: userNode.unique_id,
|
||||
user_name: userNode.user_name,
|
||||
user_email: userNode.user_email,
|
||||
user_type: userNode.user_type,
|
||||
user_id: userNode.user_id,
|
||||
path: userNode.tldraw_snapshot,
|
||||
worker_node_data: userNode.worker_node_data,
|
||||
state: {
|
||||
parentId: null,
|
||||
isPageChild: true,
|
||||
hasChildren: null,
|
||||
bindings: null
|
||||
},
|
||||
defaultComponent: true
|
||||
}
|
||||
})
|
||||
|
||||
logger.debug('graph-shape-user', '🔄 Creating user node', {
|
||||
userId: userNode.user_id,
|
||||
email: userNode.user_email,
|
||||
type: userNode.user_type
|
||||
})
|
||||
|
||||
return id
|
||||
} catch (error) {
|
||||
logger.error('graph-shape-user', '❌ Failed to create user node', error)
|
||||
return null
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
import { Editor } from '@tldraw/tldraw'
|
||||
import { CCSearchShape } from '../cc-search/CCSearchShapeUtil'
|
||||
import { SearchService } from '../../../../services/tldraw/searchService'
|
||||
|
||||
export const createSearchShape = async (
|
||||
editor: Editor,
|
||||
options?: {
|
||||
x?: number
|
||||
y?: number
|
||||
query?: string
|
||||
}
|
||||
) => {
|
||||
const { width, height } = editor.getViewportPageBounds()
|
||||
const x = options?.x ?? width / 2 - 200
|
||||
const y = options?.y ?? height / 2 - 250
|
||||
|
||||
const results = options?.query ? await SearchService.search(options.query).catch(() => []) : []
|
||||
|
||||
return editor.createShape<CCSearchShape>({
|
||||
type: 'cc-search',
|
||||
x,
|
||||
y,
|
||||
props: {
|
||||
w: 400,
|
||||
h: 500,
|
||||
title: 'Search',
|
||||
headerColor: '#1a73e8',
|
||||
backgroundColor: '#ffffff',
|
||||
isLocked: false,
|
||||
query: options?.query ?? '',
|
||||
results,
|
||||
isSearching: false,
|
||||
},
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
import { Editor, TLShapeId, createShapeId } from '@tldraw/tldraw'
|
||||
import { CC_SHAPE_CONFIGS } from '../cc-configs'
|
||||
|
||||
const type = 'cc-settings'
|
||||
const config = CC_SHAPE_CONFIGS[type]
|
||||
|
||||
export const createSettingsShape = (
|
||||
editor: Editor,
|
||||
baseProps: {
|
||||
id: TLShapeId
|
||||
x: number
|
||||
y: number
|
||||
rotation: number
|
||||
isLocked: boolean
|
||||
}
|
||||
) => {
|
||||
editor.createShape({
|
||||
...baseProps,
|
||||
type,
|
||||
props: {
|
||||
...config.defaultProps,
|
||||
w: config.width,
|
||||
h: config.height,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
export const createSettingsShapeAtCenter = (editor: Editor) => {
|
||||
if (!editor) return;
|
||||
|
||||
const { x, y } = editor.getViewportScreenCenter();
|
||||
const shapeId = createShapeId();
|
||||
|
||||
createSettingsShape(editor, {
|
||||
id: shapeId,
|
||||
x: x - config.xOffset,
|
||||
y: y - config.yOffset,
|
||||
rotation: 0,
|
||||
isLocked: false,
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,540 @@
|
||||
import { Editor, TLShapeId, createShapeId, createBindingId } from '@tldraw/tldraw'
|
||||
import { CC_SHAPE_CONFIGS } from '../cc-configs'
|
||||
import { CC_BASE_STYLE_CONSTANTS, CC_SLIDESHOW_STYLE_CONSTANTS } from '../cc-styles'
|
||||
import { CCSlideShowShape } from '../cc-slideshow/CCSlideShowShapeUtil'
|
||||
import { CCSlideShape } from '../cc-slideshow/CCSlideShapeUtil'
|
||||
import axios, { isAxiosError } from '../../../../axiosConfig'
|
||||
import { logger } from '../../../../debugConfig'
|
||||
|
||||
export const createSlideshow = (
|
||||
editor: Editor,
|
||||
baseProps: {
|
||||
id: TLShapeId
|
||||
x: number
|
||||
y: number
|
||||
rotation: number
|
||||
isLocked: boolean
|
||||
},
|
||||
slidePattern = 'horizontal',
|
||||
numSlides = 3
|
||||
) => {
|
||||
const config = CC_SHAPE_CONFIGS['cc-slideshow']
|
||||
|
||||
// Create slideshow shape
|
||||
editor.createShape<CCSlideShowShape>({
|
||||
...baseProps,
|
||||
type: 'cc-slideshow',
|
||||
props: {
|
||||
...config.defaultProps,
|
||||
w: config.width,
|
||||
h: config.height,
|
||||
slidePattern,
|
||||
currentSlideIndex: 0,
|
||||
},
|
||||
})
|
||||
|
||||
// Create slides
|
||||
for (let i = 0; i < numSlides; i++) {
|
||||
const slideId = createShapeId()
|
||||
editor.createShape<CCSlideShape>({
|
||||
id: slideId,
|
||||
type: 'cc-slide',
|
||||
x: baseProps.x + CC_SLIDESHOW_STYLE_CONSTANTS.SLIDE_SPACING,
|
||||
y: baseProps.y + CC_BASE_STYLE_CONSTANTS.HEADER.height + CC_SLIDESHOW_STYLE_CONSTANTS.SLIDE_SPACING * 2,
|
||||
rotation: 0,
|
||||
isLocked: false,
|
||||
props: {
|
||||
...CC_SHAPE_CONFIGS['cc-slide'].defaultProps,
|
||||
w: CC_SHAPE_CONFIGS['cc-slide'].width,
|
||||
h: CC_SHAPE_CONFIGS['cc-slide'].height,
|
||||
title: `Slide ${i + 1}`,
|
||||
},
|
||||
})
|
||||
|
||||
// Create binding between slideshow and slide
|
||||
editor.createBinding({
|
||||
id: createBindingId(),
|
||||
type: 'cc-slide-layout',
|
||||
fromId: baseProps.id,
|
||||
toId: slideId,
|
||||
props: {
|
||||
index: `a${i + 1}`,
|
||||
isMovingWithParent: true,
|
||||
placeholder: false,
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
export const createSlideshowAtCenter = (
|
||||
editor: Editor,
|
||||
slidePattern = 'horizontal',
|
||||
numSlides = 3
|
||||
) => {
|
||||
if (!editor) return;
|
||||
|
||||
const { x, y } = editor.getViewportScreenCenter();
|
||||
const config = CC_SHAPE_CONFIGS['cc-slideshow'];
|
||||
const shapeId = createShapeId();
|
||||
|
||||
editor.run(() => {
|
||||
createSlideshow(editor, {
|
||||
id: shapeId,
|
||||
x: x - config.xOffset,
|
||||
y: y - config.yOffset,
|
||||
rotation: 0,
|
||||
isLocked: false,
|
||||
}, slidePattern, numSlides);
|
||||
});
|
||||
}
|
||||
|
||||
export const createPowerPointSlideshow = async (
|
||||
editor: Editor,
|
||||
file: File,
|
||||
x: number,
|
||||
y: number
|
||||
) => {
|
||||
try {
|
||||
// Create form data for file upload
|
||||
const formData = new FormData()
|
||||
formData.append('file', file, file.name)
|
||||
|
||||
logger.debug('slideshow-helpers', 'Uploading PowerPoint file.', {
|
||||
name: file.name,
|
||||
size: file.size,
|
||||
})
|
||||
|
||||
const response = await axios.post('/assets/powerpoint/convert', formData)
|
||||
|
||||
logger.debug('slideshow-helpers', 'Response received.', {
|
||||
status: response.status,
|
||||
data: response.data,
|
||||
})
|
||||
|
||||
const { data } = response
|
||||
|
||||
if (!data || typeof data !== 'object') {
|
||||
throw new Error('Invalid response format from server')
|
||||
}
|
||||
|
||||
if (data.status !== 'success') {
|
||||
throw new Error(data.message || 'Failed to process PowerPoint')
|
||||
}
|
||||
|
||||
if (!data.slides || !Array.isArray(data.slides) || data.slides.length === 0) {
|
||||
throw new Error('No slides found in PowerPoint file')
|
||||
}
|
||||
|
||||
// Create slideshow with the slides from PowerPoint
|
||||
const slideshowId = createShapeId()
|
||||
const baseProps = {
|
||||
id: slideshowId,
|
||||
x,
|
||||
y,
|
||||
rotation: 0,
|
||||
isLocked: false,
|
||||
}
|
||||
|
||||
// Create slideshow in a batch operation
|
||||
editor.batch(() => {
|
||||
const config = CC_SHAPE_CONFIGS['cc-slideshow']
|
||||
|
||||
// Create slideshow shape
|
||||
editor.createShape<CCSlideShowShape>({
|
||||
...baseProps,
|
||||
type: 'cc-slideshow',
|
||||
props: {
|
||||
...config.defaultProps,
|
||||
w: config.width,
|
||||
h: CC_SLIDESHOW_STYLE_CONSTANTS.DEFAULT_SLIDE_HEIGHT +
|
||||
CC_SLIDESHOW_STYLE_CONSTANTS.SLIDE_HEADER_HEIGHT +
|
||||
CC_SLIDESHOW_STYLE_CONSTANTS.SLIDE_SPACING * 2 +
|
||||
CC_SLIDESHOW_STYLE_CONSTANTS.SLIDE_CONTENT_PADDING,
|
||||
slidePattern: 'horizontal',
|
||||
title: file.name.replace('.pptx', ''),
|
||||
currentSlideIndex: 0,
|
||||
},
|
||||
})
|
||||
|
||||
// Create slides with images and meta text
|
||||
data.slides.forEach((slide: {
|
||||
index: number,
|
||||
data: string,
|
||||
meta?: {
|
||||
text: string,
|
||||
format: string
|
||||
}
|
||||
}, i: number) => {
|
||||
const slideId = createShapeId()
|
||||
editor.createShape<CCSlideShape>({
|
||||
id: slideId,
|
||||
type: 'cc-slide',
|
||||
x: x + CC_SLIDESHOW_STYLE_CONSTANTS.SLIDE_SPACING,
|
||||
y: y + CC_SLIDESHOW_STYLE_CONSTANTS.SLIDE_HEADER_HEIGHT +
|
||||
CC_SLIDESHOW_STYLE_CONSTANTS.SLIDE_SPACING,
|
||||
rotation: 0,
|
||||
isLocked: false,
|
||||
props: {
|
||||
...CC_SHAPE_CONFIGS['cc-slide'].defaultProps,
|
||||
w: CC_SHAPE_CONFIGS['cc-slide'].width,
|
||||
h: CC_SHAPE_CONFIGS['cc-slide'].height,
|
||||
title: `Slide ${i + 1}`,
|
||||
imageData: slide.data,
|
||||
meta: slide.meta || { text: '', format: 'markdown' }
|
||||
},
|
||||
})
|
||||
|
||||
editor.createBinding({
|
||||
id: createBindingId(),
|
||||
type: 'cc-slide-layout',
|
||||
fromId: slideshowId,
|
||||
toId: slideId,
|
||||
props: {
|
||||
index: `a${String(i + 1).padStart(3, '0')}`,
|
||||
isMovingWithParent: true,
|
||||
placeholder: false,
|
||||
},
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
return true
|
||||
} catch (error) {
|
||||
if (isAxiosError(error)) {
|
||||
if (!error.response) {
|
||||
logger.error('slideshow-helpers', 'Network error - Failed to reach the server', { error })
|
||||
throw new Error('Network error - Failed to reach the server. Please check your connection.')
|
||||
}
|
||||
|
||||
const {status} = error.response
|
||||
const errorMessage = error.response.data?.message || error.message
|
||||
|
||||
if (status === 404) {
|
||||
logger.error('slideshow-helpers', 'PowerPoint conversion endpoint not found', { error })
|
||||
throw new Error('PowerPoint conversion service is not available. Please check if the backend service is running.')
|
||||
}
|
||||
|
||||
if (status === 413) {
|
||||
logger.error('slideshow-helpers', 'File too large', { error })
|
||||
throw new Error('The PowerPoint file is too large to process.')
|
||||
}
|
||||
|
||||
logger.error('slideshow-helpers', `Server error (${status})`, { error: errorMessage })
|
||||
throw new Error(`Server error (${status}): ${errorMessage}`)
|
||||
}
|
||||
|
||||
// Handle non-Axios errors
|
||||
logger.error('slideshow-helpers', 'Unexpected error creating PowerPoint slideshow', { error })
|
||||
throw new Error('An unexpected error occurred while processing the PowerPoint file.')
|
||||
}
|
||||
}
|
||||
|
||||
export const createWordSlideshow = async (
|
||||
editor: Editor,
|
||||
file: File,
|
||||
x: number,
|
||||
y: number
|
||||
) => {
|
||||
try {
|
||||
// Create form data for file upload
|
||||
const formData = new FormData()
|
||||
formData.append('file', file, file.name)
|
||||
|
||||
logger.debug('slideshow-helpers', 'Uploading Word file.', {
|
||||
name: file.name,
|
||||
size: file.size,
|
||||
})
|
||||
|
||||
const response = await axios.post('/assets/word/convert', formData)
|
||||
|
||||
logger.debug('slideshow-helpers', 'Response received.', {
|
||||
status: response.status,
|
||||
data: response.data,
|
||||
})
|
||||
|
||||
const { data } = response
|
||||
|
||||
if (!data || typeof data !== 'object') {
|
||||
throw new Error('Invalid response format from server')
|
||||
}
|
||||
|
||||
if (data.status !== 'success') {
|
||||
throw new Error(data.message || 'Failed to process Word document')
|
||||
}
|
||||
|
||||
if (!data.slides || !Array.isArray(data.slides) || data.slides.length === 0) {
|
||||
throw new Error('No pages found in Word document')
|
||||
}
|
||||
|
||||
// Create slideshow with the pages from Word
|
||||
const slideshowId = createShapeId()
|
||||
const baseProps = {
|
||||
id: slideshowId,
|
||||
x,
|
||||
y,
|
||||
rotation: 0,
|
||||
isLocked: false,
|
||||
}
|
||||
|
||||
// Create slideshow in a batch operation
|
||||
editor.batch(() => {
|
||||
const config = CC_SHAPE_CONFIGS['cc-slideshow']
|
||||
|
||||
// Create slideshow shape
|
||||
editor.createShape<CCSlideShowShape>({
|
||||
...baseProps,
|
||||
type: 'cc-slideshow',
|
||||
props: {
|
||||
...config.defaultProps,
|
||||
w: config.width,
|
||||
h: CC_SLIDESHOW_STYLE_CONSTANTS.DEFAULT_SLIDE_HEIGHT +
|
||||
CC_SLIDESHOW_STYLE_CONSTANTS.SLIDE_HEADER_HEIGHT +
|
||||
CC_SLIDESHOW_STYLE_CONSTANTS.SLIDE_SPACING * 2 +
|
||||
CC_SLIDESHOW_STYLE_CONSTANTS.SLIDE_CONTENT_PADDING,
|
||||
slidePattern: 'horizontal',
|
||||
title: file.name.replace('.docx', ''),
|
||||
currentSlideIndex: 0,
|
||||
},
|
||||
})
|
||||
|
||||
// Create slides with images and meta text
|
||||
data.slides.forEach((slide: {
|
||||
index: number,
|
||||
data: string,
|
||||
dimensions?: { width: number, height: number },
|
||||
meta?: {
|
||||
text: string,
|
||||
format: string
|
||||
}
|
||||
}, i: number) => {
|
||||
const slideId = createShapeId()
|
||||
editor.createShape<CCSlideShape>({
|
||||
id: slideId,
|
||||
type: 'cc-slide',
|
||||
x: x + CC_SLIDESHOW_STYLE_CONSTANTS.SLIDE_SPACING,
|
||||
y: y + CC_SLIDESHOW_STYLE_CONSTANTS.SLIDE_HEADER_HEIGHT +
|
||||
CC_SLIDESHOW_STYLE_CONSTANTS.SLIDE_SPACING,
|
||||
rotation: 0,
|
||||
isLocked: false,
|
||||
props: {
|
||||
...CC_SHAPE_CONFIGS['cc-slide'].defaultProps,
|
||||
w: slide.dimensions?.width ?? CC_SHAPE_CONFIGS['cc-slide'].width,
|
||||
h: slide.dimensions?.height ?? CC_SHAPE_CONFIGS['cc-slide'].height,
|
||||
title: `Page ${i + 1}`,
|
||||
imageData: slide.data,
|
||||
meta: slide.meta || { text: '', format: 'markdown' }
|
||||
},
|
||||
})
|
||||
|
||||
editor.createBinding({
|
||||
id: createBindingId(),
|
||||
type: 'cc-slide-layout',
|
||||
fromId: slideshowId,
|
||||
toId: slideId,
|
||||
props: {
|
||||
index: `a${String(i + 1).padStart(3, '0')}`,
|
||||
isMovingWithParent: true,
|
||||
placeholder: false,
|
||||
},
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
return true
|
||||
} catch (error) {
|
||||
if (isAxiosError(error)) {
|
||||
if (!error.response) {
|
||||
logger.error('slideshow-helpers', 'Network error - Failed to reach the server', { error })
|
||||
throw new Error('Network error - Failed to reach the server. Please check your connection.')
|
||||
}
|
||||
|
||||
const {status} = error.response
|
||||
const errorMessage = error.response.data?.message || error.message
|
||||
|
||||
if (status === 404) {
|
||||
logger.error('slideshow-helpers', 'Word conversion endpoint not found', { error })
|
||||
throw new Error('Word conversion service is not available. Please check if the backend service is running.')
|
||||
}
|
||||
|
||||
if (status === 413) {
|
||||
logger.error('slideshow-helpers', 'File too large', { error })
|
||||
throw new Error('The Word file is too large to process.')
|
||||
}
|
||||
|
||||
logger.error('slideshow-helpers', `Server error (${status})`, { error: errorMessage })
|
||||
throw new Error(`Server error (${status}): ${errorMessage}`)
|
||||
}
|
||||
|
||||
// Handle non-Axios errors
|
||||
logger.error('slideshow-helpers', 'Unexpected error creating Word slideshow', { error })
|
||||
throw new Error('An unexpected error occurred while processing the Word file.')
|
||||
}
|
||||
}
|
||||
|
||||
export const createPDFSlideshow = async (
|
||||
editor: Editor,
|
||||
file: File,
|
||||
x: number,
|
||||
y: number
|
||||
) => {
|
||||
try {
|
||||
// Create form data for file upload
|
||||
const formData = new FormData()
|
||||
formData.append('file', file, file.name)
|
||||
|
||||
logger.debug('slideshow-helpers', 'Uploading PDF file.', {
|
||||
name: file.name,
|
||||
size: file.size,
|
||||
})
|
||||
|
||||
const response = await axios.post('/assets/pdf/convert', formData)
|
||||
|
||||
logger.debug('slideshow-helpers', 'Response received.', {
|
||||
status: response.status,
|
||||
data: response.data,
|
||||
})
|
||||
|
||||
const { data } = response
|
||||
|
||||
if (!data || typeof data !== 'object') {
|
||||
throw new Error('Invalid response format from server')
|
||||
}
|
||||
|
||||
if (data.status !== 'success') {
|
||||
throw new Error(data.message || 'Failed to process PDF document')
|
||||
}
|
||||
|
||||
if (!data.slides || !Array.isArray(data.slides) || data.slides.length === 0) {
|
||||
throw new Error('No pages found in PDF document')
|
||||
}
|
||||
|
||||
// Create slideshow with the pages from PDF
|
||||
const slideshowId = createShapeId()
|
||||
const baseProps = {
|
||||
id: slideshowId,
|
||||
x,
|
||||
y,
|
||||
rotation: 0,
|
||||
isLocked: false,
|
||||
}
|
||||
|
||||
// Create slideshow in a batch operation
|
||||
editor.batch(() => {
|
||||
const config = CC_SHAPE_CONFIGS['cc-slideshow']
|
||||
|
||||
// Create slideshow shape
|
||||
editor.createShape<CCSlideShowShape>({
|
||||
...baseProps,
|
||||
type: 'cc-slideshow',
|
||||
props: {
|
||||
...config.defaultProps,
|
||||
w: config.width,
|
||||
h: CC_SLIDESHOW_STYLE_CONSTANTS.DEFAULT_SLIDE_HEIGHT +
|
||||
CC_SLIDESHOW_STYLE_CONSTANTS.SLIDE_HEADER_HEIGHT +
|
||||
CC_SLIDESHOW_STYLE_CONSTANTS.SLIDE_SPACING * 2 +
|
||||
CC_SLIDESHOW_STYLE_CONSTANTS.SLIDE_CONTENT_PADDING,
|
||||
slidePattern: 'horizontal',
|
||||
title: file.name.replace('.pdf', ''),
|
||||
currentSlideIndex: 0,
|
||||
},
|
||||
})
|
||||
|
||||
// Create slides with images and meta text
|
||||
data.slides.forEach((slide: {
|
||||
index: number,
|
||||
data: string,
|
||||
dimensions?: { width: number, height: number },
|
||||
meta?: {
|
||||
text: string,
|
||||
format: string
|
||||
}
|
||||
}, i: number) => {
|
||||
const slideId = createShapeId()
|
||||
editor.createShape<CCSlideShape>({
|
||||
id: slideId,
|
||||
type: 'cc-slide',
|
||||
x: x + CC_SLIDESHOW_STYLE_CONSTANTS.SLIDE_SPACING,
|
||||
y: y + CC_SLIDESHOW_STYLE_CONSTANTS.SLIDE_HEADER_HEIGHT +
|
||||
CC_SLIDESHOW_STYLE_CONSTANTS.SLIDE_SPACING,
|
||||
rotation: 0,
|
||||
isLocked: false,
|
||||
props: {
|
||||
...CC_SHAPE_CONFIGS['cc-slide'].defaultProps,
|
||||
w: slide.dimensions?.width ?? CC_SHAPE_CONFIGS['cc-slide'].width,
|
||||
h: slide.dimensions?.height ?? CC_SHAPE_CONFIGS['cc-slide'].height,
|
||||
title: `Page ${i + 1}`,
|
||||
imageData: slide.data,
|
||||
meta: slide.meta || { text: '', format: 'markdown' }
|
||||
},
|
||||
})
|
||||
|
||||
editor.createBinding({
|
||||
id: createBindingId(),
|
||||
type: 'cc-slide-layout',
|
||||
fromId: slideshowId,
|
||||
toId: slideId,
|
||||
props: {
|
||||
index: `a${String(i + 1).padStart(3, '0')}`,
|
||||
isMovingWithParent: true,
|
||||
placeholder: false,
|
||||
},
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
return true
|
||||
} catch (error) {
|
||||
if (isAxiosError(error)) {
|
||||
if (!error.response) {
|
||||
logger.error('slideshow-helpers', 'Network error - Failed to reach the server', { error })
|
||||
throw new Error('Network error - Failed to reach the server. Please check your connection.')
|
||||
}
|
||||
|
||||
const {status} = error.response
|
||||
const errorMessage = error.response.data?.message || error.message
|
||||
|
||||
if (status === 404) {
|
||||
logger.error('slideshow-helpers', 'PDF conversion endpoint not found', { error })
|
||||
throw new Error('PDF conversion service is not available. Please check if the backend service is running.')
|
||||
}
|
||||
|
||||
if (status === 413) {
|
||||
logger.error('slideshow-helpers', 'File too large', { error })
|
||||
throw new Error('The PDF file is too large to process.')
|
||||
}
|
||||
|
||||
logger.error('slideshow-helpers', `Server error (${status})`, { error: errorMessage })
|
||||
throw new Error(`Server error (${status}): ${errorMessage}`)
|
||||
}
|
||||
|
||||
// Handle non-Axios errors
|
||||
logger.error('slideshow-helpers', 'Unexpected error creating PDF slideshow', { error })
|
||||
throw new Error('An unexpected error occurred while processing the PDF file.')
|
||||
}
|
||||
}
|
||||
|
||||
export const handleSlideshowFileUpload = async (
|
||||
editor: Editor,
|
||||
file: File,
|
||||
onComplete?: () => void
|
||||
) => {
|
||||
if (!editor) return;
|
||||
|
||||
const { x, y } = editor.getViewportScreenCenter();
|
||||
|
||||
try {
|
||||
if (file.name.endsWith('.pptx')) {
|
||||
await createPowerPointSlideshow(editor, file, x, y);
|
||||
} else if (file.name.endsWith('.docx')) {
|
||||
await createWordSlideshow(editor, file, x, y);
|
||||
} else if (file.name.endsWith('.pdf')) {
|
||||
await createPDFSlideshow(editor, file, x, y);
|
||||
} else {
|
||||
throw new Error('Please select a PowerPoint (.pptx), Word (.docx), or PDF (.pdf) file');
|
||||
}
|
||||
|
||||
onComplete?.();
|
||||
} catch (error) {
|
||||
throw error instanceof Error ? error : new Error('An unknown error occurred');
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,41 @@
|
||||
import { Editor, TLShapeId, createShapeId } from '@tldraw/tldraw'
|
||||
import { CC_SHAPE_CONFIGS } from '../cc-configs'
|
||||
|
||||
export const createLiveTranscriptionShape = (
|
||||
editor: Editor,
|
||||
baseProps: {
|
||||
id: TLShapeId
|
||||
x: number
|
||||
y: number
|
||||
rotation: number
|
||||
isLocked: boolean
|
||||
}
|
||||
) => {
|
||||
const config = CC_SHAPE_CONFIGS['cc-live-transcription']
|
||||
|
||||
editor.createShape({
|
||||
...baseProps,
|
||||
type: 'cc-live-transcription',
|
||||
props: {
|
||||
...config.defaultProps,
|
||||
w: config.width,
|
||||
h: config.height,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
export const createLiveTranscriptionShapeAtCenter = (editor: Editor) => {
|
||||
if (!editor) return;
|
||||
|
||||
const { x, y } = editor.getViewportScreenCenter();
|
||||
const config = CC_SHAPE_CONFIGS['cc-live-transcription'];
|
||||
const shapeId = createShapeId();
|
||||
|
||||
createLiveTranscriptionShape(editor, {
|
||||
id: shapeId,
|
||||
x: x - config.xOffset,
|
||||
y: y - config.yOffset,
|
||||
rotation: 0,
|
||||
isLocked: false,
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,161 @@
|
||||
import { Editor, createShapeId, TLShapeId, IndexKey, TLParentId } from '@tldraw/tldraw'
|
||||
import { CCWebBrowserShape } from '../cc-web-browser/CCWebBrowserUtil'
|
||||
|
||||
interface WebBrowserShapeOptions {
|
||||
url: string
|
||||
x?: number
|
||||
y?: number
|
||||
w?: number
|
||||
h?: number
|
||||
title?: string
|
||||
isLoading?: boolean
|
||||
}
|
||||
|
||||
interface WebBrowserShapeResult {
|
||||
id: TLShapeId
|
||||
url: string
|
||||
x: number
|
||||
y: number
|
||||
}
|
||||
|
||||
export function createWebBrowserShapeInfo(options: WebBrowserShapeOptions): CCWebBrowserShape {
|
||||
const shapeId = createShapeId()
|
||||
return {
|
||||
id: shapeId,
|
||||
type: 'cc-web-browser',
|
||||
typeName: 'shape',
|
||||
x: options.x ?? 0,
|
||||
y: options.y ?? 0,
|
||||
rotation: 0,
|
||||
index: 'a1' as IndexKey,
|
||||
parentId: 'page:page' as TLParentId,
|
||||
isLocked: false,
|
||||
opacity: 1,
|
||||
meta: {},
|
||||
props: {
|
||||
w: options.w ?? 800,
|
||||
h: options.h ?? 600,
|
||||
title: options.title ?? 'Web Browser',
|
||||
headerColor: '#1a73e8',
|
||||
backgroundColor: '#ffffff',
|
||||
isLocked: false,
|
||||
url: options.url,
|
||||
history: [options.url],
|
||||
currentHistoryIndex: 0,
|
||||
isLoading: options.isLoading ?? true,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
export function createWebBrowserShape(editor: Editor, options: WebBrowserShapeOptions): WebBrowserShapeResult {
|
||||
const shape = createWebBrowserShapeInfo(options)
|
||||
editor.createShape(shape)
|
||||
|
||||
return {
|
||||
id: shape.id,
|
||||
url: options.url,
|
||||
x: shape.x,
|
||||
y: shape.y
|
||||
}
|
||||
}
|
||||
|
||||
interface MultipleWebBrowserOptions {
|
||||
browsers: WebBrowserShapeOptions[]
|
||||
layout?: 'grid' | 'cascade' | 'horizontal' | 'vertical'
|
||||
spacing?: number
|
||||
startX?: number
|
||||
startY?: number
|
||||
}
|
||||
|
||||
export function createMultipleWebBrowsers(
|
||||
editor: Editor,
|
||||
{ browsers, layout = 'cascade', spacing = 20, startX = 0, startY = 0 }: MultipleWebBrowserOptions
|
||||
): WebBrowserShapeResult[] {
|
||||
const results: WebBrowserShapeResult[] = []
|
||||
const baseWidth = 800
|
||||
const baseHeight = 600
|
||||
|
||||
editor.batch(() => {
|
||||
browsers.forEach((browser, index) => {
|
||||
let x = startX
|
||||
let y = startY
|
||||
|
||||
switch (layout) {
|
||||
case 'grid': {
|
||||
const cols = Math.ceil(Math.sqrt(browsers.length))
|
||||
x += (index % cols) * (baseWidth + spacing)
|
||||
y += Math.floor(index / cols) * (baseHeight + spacing)
|
||||
break
|
||||
}
|
||||
case 'cascade':
|
||||
x += index * spacing
|
||||
y += index * spacing
|
||||
break
|
||||
case 'horizontal':
|
||||
x += index * (baseWidth + spacing)
|
||||
break
|
||||
case 'vertical':
|
||||
y += index * (baseHeight + spacing)
|
||||
break
|
||||
}
|
||||
|
||||
const result = createWebBrowserShape(editor, {
|
||||
...browser,
|
||||
x,
|
||||
y,
|
||||
w: baseWidth,
|
||||
h: baseHeight
|
||||
})
|
||||
results.push(result)
|
||||
})
|
||||
})
|
||||
|
||||
return results
|
||||
}
|
||||
|
||||
// Helper function to arrange browser shapes in a specific layout
|
||||
export function arrangeBrowserShapes(
|
||||
editor: Editor,
|
||||
shapeIds: TLShapeId[],
|
||||
layout: 'grid' | 'cascade' | 'horizontal' | 'vertical',
|
||||
options?: { spacing?: number; startX?: number; startY?: number }
|
||||
) {
|
||||
const spacing = options?.spacing ?? 20
|
||||
const startX = options?.startX ?? 0
|
||||
const startY = options?.startY ?? 0
|
||||
const baseWidth = 800
|
||||
const baseHeight = 600
|
||||
|
||||
editor.batch(() => {
|
||||
shapeIds.forEach((id, index) => {
|
||||
let x = startX
|
||||
let y = startY
|
||||
|
||||
switch (layout) {
|
||||
case 'grid': {
|
||||
const cols = Math.ceil(Math.sqrt(shapeIds.length))
|
||||
x += (index % cols) * (baseWidth + spacing)
|
||||
y += Math.floor(index / cols) * (baseHeight + spacing)
|
||||
break
|
||||
}
|
||||
case 'cascade':
|
||||
x += index * spacing
|
||||
y += index * spacing
|
||||
break
|
||||
case 'horizontal':
|
||||
x += index * (baseWidth + spacing)
|
||||
break
|
||||
case 'vertical':
|
||||
y += index * (baseHeight + spacing)
|
||||
break
|
||||
}
|
||||
|
||||
editor.updateShape({
|
||||
id,
|
||||
type: 'cc-web-browser',
|
||||
x,
|
||||
y,
|
||||
})
|
||||
})
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
import { Editor, TLShapeId, createShapeId } from '@tldraw/tldraw'
|
||||
import { CC_SHAPE_CONFIGS } from '../cc-configs'
|
||||
|
||||
export const createYoutubeShape = (
|
||||
editor: Editor,
|
||||
baseProps: {
|
||||
id: TLShapeId
|
||||
x: number
|
||||
y: number
|
||||
rotation: number
|
||||
isLocked: boolean
|
||||
},
|
||||
videoUrl: string
|
||||
) => {
|
||||
const config = CC_SHAPE_CONFIGS['cc-youtube-embed']
|
||||
|
||||
editor.createShape({
|
||||
...baseProps,
|
||||
type: 'cc-youtube-embed',
|
||||
props: {
|
||||
...config.defaultProps,
|
||||
w: config.width,
|
||||
h: config.height,
|
||||
video_url: videoUrl,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
export const createYoutubeShapeAtCenter = (editor: Editor, videoUrl: string) => {
|
||||
if (!editor) return;
|
||||
|
||||
const { x, y } = editor.getViewportScreenCenter();
|
||||
const config = CC_SHAPE_CONFIGS['cc-youtube-embed'];
|
||||
const shapeId = createShapeId();
|
||||
|
||||
createYoutubeShape(editor, {
|
||||
id: shapeId,
|
||||
x: x - config.xOffset,
|
||||
y: y - config.yOffset,
|
||||
rotation: 0,
|
||||
isLocked: false,
|
||||
}, videoUrl);
|
||||
}
|
||||
@@ -0,0 +1,100 @@
|
||||
import {
|
||||
DefaultEmbedDefinitionType,
|
||||
CustomEmbedDefinition,
|
||||
DEFAULT_EMBED_DEFINITIONS,
|
||||
TLEmbedDefinition,
|
||||
} from '@tldraw/tldraw';
|
||||
|
||||
// Define which default embeds we want to keep
|
||||
const defaultEmbedTypesToKeep: DefaultEmbedDefinitionType[] = [
|
||||
'tldraw',
|
||||
'google_slides',
|
||||
];
|
||||
|
||||
// Filter default embeds to keep only the ones we want
|
||||
export const defaultEmbedsToKeep = DEFAULT_EMBED_DEFINITIONS.filter((embed) =>
|
||||
defaultEmbedTypesToKeep.includes(embed.type as DefaultEmbedDefinitionType)
|
||||
) as TLEmbedDefinition[];
|
||||
|
||||
// Helper to create custom embeds
|
||||
const createCustomEmbed = (
|
||||
type: string,
|
||||
title: string,
|
||||
hostnames: string[],
|
||||
icon: string,
|
||||
minWidth = 300,
|
||||
minHeight = 300,
|
||||
width = 720,
|
||||
height = 500
|
||||
): CustomEmbedDefinition => ({
|
||||
type,
|
||||
title,
|
||||
hostnames,
|
||||
minWidth,
|
||||
minHeight,
|
||||
width,
|
||||
height,
|
||||
doesResize: true,
|
||||
toEmbedUrl: (url) => {
|
||||
const urlObj = new URL(url);
|
||||
return `${urlObj.origin}/embed${urlObj.pathname}`;
|
||||
},
|
||||
fromEmbedUrl: (url) => {
|
||||
return url.replace('/embed', '');
|
||||
},
|
||||
icon,
|
||||
});
|
||||
|
||||
// Define custom embeds
|
||||
export const pptEmbed = createCustomEmbed(
|
||||
'ppt',
|
||||
'PowerPoint',
|
||||
['office.live.com'],
|
||||
'https://c1-odc-15.cdn.office.net/start/resources/images/favicon_powerpointcom.ico'
|
||||
);
|
||||
|
||||
export const ccYoutubeEmbed: CustomEmbedDefinition = {
|
||||
type: 'cc-youtube-embed',
|
||||
title: 'YouTube Video',
|
||||
hostnames: ['youtube.com', 'youtu.be'],
|
||||
width: 800,
|
||||
height: 450,
|
||||
doesResize: true,
|
||||
minWidth: 200,
|
||||
minHeight: 113,
|
||||
toEmbedUrl: (url) => {
|
||||
const videoId = url.match(/(?:youtube\.com\/(?:[^\/]+\/.+\/|(?:v|e(?:mbed)?)\/|.*[?&]v=)|youtu\.be\/)([^"&?\/\s]{11})/)?.[1];
|
||||
return `https://www.youtube.com/embed/${videoId}`;
|
||||
},
|
||||
fromEmbedUrl: (url) => {
|
||||
const videoId = url.split('/').pop();
|
||||
return `https://www.youtube.com/watch?v=${videoId}`;
|
||||
},
|
||||
icon: 'https://www.youtube.com/favicon.ico',
|
||||
};
|
||||
|
||||
export const customEmbeds: TLEmbedDefinition[] = [
|
||||
pptEmbed,
|
||||
ccYoutubeEmbed,
|
||||
];
|
||||
|
||||
// Export specific embed sets for different modes
|
||||
export const multiplayerEmbeds: TLEmbedDefinition[] = [
|
||||
...defaultEmbedsToKeep,
|
||||
...customEmbeds,
|
||||
];
|
||||
|
||||
export const singlePlayerEmbeds: TLEmbedDefinition[] = [
|
||||
...defaultEmbedsToKeep,
|
||||
...customEmbeds,
|
||||
];
|
||||
|
||||
export const devEmbeds: TLEmbedDefinition[] = [
|
||||
...defaultEmbedsToKeep,
|
||||
...customEmbeds,
|
||||
];
|
||||
|
||||
// Helper function to create custom embed sets
|
||||
export const createCustomEmbedSet = (...embedSets: TLEmbedDefinition[][]): TLEmbedDefinition[] => {
|
||||
return [...new Set(embedSets.flat())];
|
||||
};
|
||||
@@ -0,0 +1,53 @@
|
||||
import { createTLSchema, defaultShapeSchemas, defaultBindingSchemas } from '@tldraw/tlschema';
|
||||
import { createTLSchemaFromUtils, defaultBindingUtils, defaultShapeUtils } from '@tldraw/tldraw';
|
||||
import { ShapeUtils } from './shapes';
|
||||
import { allBindingUtils } from './bindings';
|
||||
import { ccGraphShapeProps } from './cc-base/cc-graph/cc-graph-props';
|
||||
import { ccGraphMigrations } from './cc-base/cc-graph/cc-graph-migrations';
|
||||
import { GraphShapeType } from './cc-base/cc-graph/cc-graph-types';
|
||||
|
||||
// Create schema with shape definitions
|
||||
export const customSchema = createTLSchema({
|
||||
shapes: {
|
||||
...defaultShapeSchemas,
|
||||
// Dynamically generate shape schemas from ShapeUtils
|
||||
...Object.values(ShapeUtils).reduce((acc, util) => ({
|
||||
...acc,
|
||||
[util.type]: {
|
||||
props: util.props,
|
||||
migrations: util.migrations,
|
||||
}
|
||||
}), {}),
|
||||
// Add graph shapes
|
||||
...(ccGraphShapeProps ? Object.entries(ccGraphShapeProps).reduce((acc, [type, props]) => ({
|
||||
...acc,
|
||||
[type]: {
|
||||
props,
|
||||
migrations: ccGraphMigrations[type as GraphShapeType],
|
||||
}
|
||||
}), {}) : {})
|
||||
},
|
||||
bindings: {
|
||||
...defaultBindingSchemas,
|
||||
// Add binding schemas from our custom binding utils
|
||||
...allBindingUtils.reduce((acc, util) => ({
|
||||
...acc,
|
||||
[util.type]: {
|
||||
props: util.props,
|
||||
migrations: util.migrations,
|
||||
}
|
||||
}), {})
|
||||
},
|
||||
});
|
||||
|
||||
// Create schema from utils (alternative approach)
|
||||
export const schemaFromUtils = createTLSchemaFromUtils({
|
||||
shapeUtils: [
|
||||
...defaultShapeUtils,
|
||||
...Object.values(ShapeUtils)
|
||||
],
|
||||
bindingUtils: [
|
||||
...defaultBindingUtils,
|
||||
...allBindingUtils
|
||||
],
|
||||
});
|
||||
@@ -0,0 +1,96 @@
|
||||
// Custom tldraw utils
|
||||
import { CCSlideShowShapeUtil } from './cc-base/cc-slideshow/CCSlideShowShapeUtil'
|
||||
import { CCSlideShapeUtil } from './cc-base/cc-slideshow/CCSlideShapeUtil'
|
||||
import { CCCalendarShapeUtil } from './cc-base/cc-calendar/CCCalendarShapeUtil'
|
||||
import { CCSettingsShapeUtil } from './cc-base/cc-settings/CCSettingsShapeUtil'
|
||||
import { CCLiveTranscriptionShapeUtil } from './cc-base/cc-transcription/CCLiveTranscriptionShapeUtil'
|
||||
import { CCYoutubeEmbedShapeUtil } from './cc-base/cc-youtube-embed/CCYoutubeEmbedShapeUtil'
|
||||
import { CCUserNodeShapeUtil } from './cc-base/cc-graph/CCUserNodeShapeUtil'
|
||||
import { CCTeacherNodeShapeUtil } from './cc-base/cc-graph/CCTeacherNodeShapeUtil'
|
||||
import { CCStudentNodeShapeUtil } from './cc-base/cc-graph/CCStudentNodeShapeUtil'
|
||||
import { CCCalendarNodeShapeUtil } from './cc-base/cc-graph/CCCalendarNodeShapeUtil'
|
||||
import { CCCalendarYearNodeShapeUtil } from './cc-base/cc-graph/CCCalendarYearNodeShapeUtil'
|
||||
import { CCCalendarMonthNodeShapeUtil } from './cc-base/cc-graph/CCCalendarMonthNodeShapeUtil'
|
||||
import { CCCalendarWeekNodeShapeUtil } from './cc-base/cc-graph/CCCalendarWeekNodeShapeUtil'
|
||||
import { CCCalendarDayNodeShapeUtil } from './cc-base/cc-graph/CCCalendarDayNodeShapeUtil'
|
||||
import { CCCalendarTimeChunkNodeShapeUtil } from './cc-base/cc-graph/CCCalendarTimeChunkNodeShapeUtil'
|
||||
import { CCSchoolNodeShapeUtil } from './cc-base/cc-graph/CCSchoolNodeShapeUtil'
|
||||
import { CCDepartmentNodeShapeUtil } from './cc-base/cc-graph/CCDepartmentNodeShapeUtil'
|
||||
import { CCRoomNodeShapeUtil } from './cc-base/cc-graph/CCRoomNodeShapeUtil'
|
||||
import { CCSubjectClassNodeShapeUtil } from './cc-base/cc-graph/CCSubjectClassNodeShapeUtil'
|
||||
import { CCPastoralStructureNodeShapeUtil } from './cc-base/cc-graph/CCPastoralStructureNodeShapeUtil'
|
||||
import { CCYearGroupNodeShapeUtil } from './cc-base/cc-graph/CCYearGroupNodeShapeUtil'
|
||||
import { CCCurriculumStructureNodeShapeUtil } from './cc-base/cc-graph/CCCurriculumStructureNodeShapeUtil'
|
||||
import { CCKeyStageNodeShapeUtil } from './cc-base/cc-graph/CCKeyStageNodeShapeUtil'
|
||||
import { CCKeyStageSyllabusNodeShapeUtil } from './cc-base/cc-graph/CCKeyStageSyllabusNodeShapeUtil'
|
||||
import { CCYearGroupSyllabusNodeShapeUtil } from './cc-base/cc-graph/CCYearGroupSyllabusNodeShapeUtil'
|
||||
import { CCSubjectNodeShapeUtil } from './cc-base/cc-graph/CCSubjectNodeShapeUtil'
|
||||
import { CCTopicNodeShapeUtil } from './cc-base/cc-graph/CCTopicNodeShapeUtil'
|
||||
import { CCTopicLessonNodeShapeUtil } from './cc-base/cc-graph/CCTopicLessonNodeShapeUtil'
|
||||
import { CCLearningStatementNodeShapeUtil } from './cc-base/cc-graph/CCLearningStatementNodeShapeUtil'
|
||||
import { CCScienceLabNodeShapeUtil } from './cc-base/cc-graph/CCScienceLabNodeShapeUtil'
|
||||
import { CCTeacherTimetableNodeShapeUtil } from './cc-base/cc-graph/CCTeacherTimetableNodeShapeUtil'
|
||||
import { CCTimetableLessonNodeShapeUtil } from './cc-base/cc-graph/CCTimetableLessonNodeShapeUtil'
|
||||
import { CCPlannedLessonNodeShapeUtil } from './cc-base/cc-graph/CCPlannedLessonNodeShapeUtil'
|
||||
import { CCSchoolTimetableNodeShapeUtil } from './cc-base/cc-graph/CCSchoolTimetableNodeShapeUtil'
|
||||
import { CCAcademicYearNodeShapeUtil } from './cc-base/cc-graph/CCAcademicYearNodeShapeUtil'
|
||||
import { CCAcademicTermNodeShapeUtil } from './cc-base/cc-graph/CCAcademicTermNodeShapeUtil'
|
||||
import { CCAcademicWeekNodeShapeUtil } from './cc-base/cc-graph/CCAcademicWeekNodeShapeUtil'
|
||||
import { CCAcademicDayNodeShapeUtil } from './cc-base/cc-graph/CCAcademicDayNodeShapeUtil'
|
||||
import { CCAcademicPeriodNodeShapeUtil } from './cc-base/cc-graph/CCAcademicPeriodNodeShapeUtil'
|
||||
import { CCRegistrationPeriodNodeShapeUtil } from './cc-base/cc-graph/CCRegistrationPeriodNodeShapeUtil'
|
||||
import { CCDepartmentStructureNodeShapeUtil } from './cc-base/cc-graph/CCDepartmentStructureNodeShapeUtil'
|
||||
import { CCUserTeacherTimetableNodeShapeUtil } from './cc-base/cc-graph/CCUserTeacherTimetableNodeShapeUtil'
|
||||
import { CCUserTimetableLessonNodeShapeUtil } from './cc-base/cc-graph/CCUserTimetableLessonNodeShapeUtil'
|
||||
import { CCSearchShapeUtil } from './cc-base/cc-search/CCSearchShapeUtil'
|
||||
import { CCWebBrowserShapeUtil } from './cc-base/cc-web-browser/CCWebBrowserUtil'
|
||||
// Define all shape utils in a single object for easy maintenance
|
||||
export const ShapeUtils = {
|
||||
CCSlideShow: CCSlideShowShapeUtil,
|
||||
CCSlide: CCSlideShapeUtil,
|
||||
CCCalendar: CCCalendarShapeUtil,
|
||||
CCSettings: CCSettingsShapeUtil,
|
||||
CCLiveTranscription: CCLiveTranscriptionShapeUtil,
|
||||
CCYoutubeEmbed: CCYoutubeEmbedShapeUtil,
|
||||
CCUserNode: CCUserNodeShapeUtil,
|
||||
CCTeacherNode: CCTeacherNodeShapeUtil,
|
||||
CCStudentNode: CCStudentNodeShapeUtil,
|
||||
CCCalendarNode: CCCalendarNodeShapeUtil,
|
||||
CCCalendarYearNode: CCCalendarYearNodeShapeUtil,
|
||||
CCCalendarMonthNode: CCCalendarMonthNodeShapeUtil,
|
||||
CCCalendarWeekNode: CCCalendarWeekNodeShapeUtil,
|
||||
CCCalendarDayNode: CCCalendarDayNodeShapeUtil,
|
||||
CCCalendarTimeChunkNode: CCCalendarTimeChunkNodeShapeUtil,
|
||||
CCSchoolNode: CCSchoolNodeShapeUtil,
|
||||
CCDepartmentNode: CCDepartmentNodeShapeUtil,
|
||||
CCRoomNode: CCRoomNodeShapeUtil,
|
||||
CCSubjectClassNode: CCSubjectClassNodeShapeUtil,
|
||||
CCPastoralStructureNode: CCPastoralStructureNodeShapeUtil,
|
||||
CCYearGroupNode: CCYearGroupNodeShapeUtil,
|
||||
CCCurriculumStructureNode: CCCurriculumStructureNodeShapeUtil,
|
||||
CCKeyStageNode: CCKeyStageNodeShapeUtil,
|
||||
CCKeyStageSyllabusNode: CCKeyStageSyllabusNodeShapeUtil,
|
||||
CCYearGroupSyllabusNode: CCYearGroupSyllabusNodeShapeUtil,
|
||||
CCSubjectNode: CCSubjectNodeShapeUtil,
|
||||
CCTopicNode: CCTopicNodeShapeUtil,
|
||||
CCTopicLessonNode: CCTopicLessonNodeShapeUtil,
|
||||
CCLearningStatementNode: CCLearningStatementNodeShapeUtil,
|
||||
CCScienceLabNode: CCScienceLabNodeShapeUtil,
|
||||
CCTeacherTimetableNode: CCTeacherTimetableNodeShapeUtil,
|
||||
CCTimetableLessonNode: CCTimetableLessonNodeShapeUtil,
|
||||
CCPlannedLessonNode: CCPlannedLessonNodeShapeUtil,
|
||||
CCSchoolTimetableNode: CCSchoolTimetableNodeShapeUtil,
|
||||
CCAcademicYearNode: CCAcademicYearNodeShapeUtil,
|
||||
CCAcademicTermNode: CCAcademicTermNodeShapeUtil,
|
||||
CCAcademicWeekNode: CCAcademicWeekNodeShapeUtil,
|
||||
CCAcademicDayNode: CCAcademicDayNodeShapeUtil,
|
||||
CCAcademicPeriodNode: CCAcademicPeriodNodeShapeUtil,
|
||||
CCRegistrationPeriodNode: CCRegistrationPeriodNodeShapeUtil,
|
||||
CCDepartmentStructureNode: CCDepartmentStructureNodeShapeUtil,
|
||||
CCUserTeacherTimetableNode: CCUserTeacherTimetableNodeShapeUtil,
|
||||
CCUserTimetableLessonNode: CCUserTimetableLessonNodeShapeUtil,
|
||||
CCSearch: CCSearchShapeUtil,
|
||||
CCWebBrowser: CCWebBrowserShapeUtil,
|
||||
}
|
||||
|
||||
export const allShapeUtils = Object.values(ShapeUtils)
|
||||
@@ -0,0 +1,2 @@
|
||||
@import url("https://fonts.googleapis.com/css2?family=Inter:wght@500;700&display=swap");
|
||||
@import url("@tldraw/tldraw/tldraw.css");
|
||||
@@ -0,0 +1,31 @@
|
||||
import { CCSlideShowShapeTool, CCSlideShapeTool } from './cc-base/cc-slideshow/CCSlideShapeTool';
|
||||
import { HeartStickerTool, SmileyStickerTool, StarStickerTool } from './tools/sticker-tool';
|
||||
|
||||
// Base tools that are common across all modes
|
||||
export const baseTools = [
|
||||
CCSlideShowShapeTool,
|
||||
CCSlideShapeTool,
|
||||
] as const;
|
||||
|
||||
// Sticker tools that can be added to any mode
|
||||
export const stickerTools = [
|
||||
HeartStickerTool,
|
||||
SmileyStickerTool,
|
||||
StarStickerTool,
|
||||
] as const;
|
||||
|
||||
// Specific tool sets for different modes
|
||||
export const multiplayerTools = [
|
||||
...baseTools,
|
||||
...stickerTools,
|
||||
] as const;
|
||||
|
||||
export const singlePlayerTools = [
|
||||
...baseTools,
|
||||
...stickerTools,
|
||||
] as const;
|
||||
|
||||
export const devTools = [
|
||||
...baseTools,
|
||||
...stickerTools,
|
||||
] as const;
|
||||
@@ -0,0 +1,75 @@
|
||||
import { StateNode, TLPointerEventInfo } from '@tldraw/tldraw'
|
||||
|
||||
const OFFSET = 12
|
||||
const STICKER_INTERVAL = 20 // Adjust this value to change the spacing between stickers
|
||||
|
||||
class BaseStickerTool extends StateNode {
|
||||
isPointerDown = false
|
||||
lastStickerPosition = { x: 0, y: 0 }
|
||||
|
||||
override onEnter() {
|
||||
this.editor.setCursor({ type: 'cross', rotation: 0 })
|
||||
}
|
||||
|
||||
override onPointerDown(info: TLPointerEventInfo) {
|
||||
this.isPointerDown = true
|
||||
this.createSticker(info)
|
||||
}
|
||||
|
||||
override onPointerMove(info: TLPointerEventInfo) {
|
||||
if (this.isPointerDown) {
|
||||
const { x, y } = info.point
|
||||
const dx = x - this.lastStickerPosition.x
|
||||
const dy = y - this.lastStickerPosition.y
|
||||
if (Math.sqrt(dx * dx + dy * dy) >= STICKER_INTERVAL) {
|
||||
this.createSticker(info)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override onPointerUp() {
|
||||
this.isPointerDown = false
|
||||
}
|
||||
|
||||
createSticker(info: TLPointerEventInfo) {
|
||||
const { x, y } = info.point
|
||||
this.editor.createShape({
|
||||
type: 'text',
|
||||
x: x - OFFSET,
|
||||
y: y - OFFSET,
|
||||
props: { text: this.getStickerEmoji() },
|
||||
})
|
||||
this.lastStickerPosition = { x, y }
|
||||
}
|
||||
|
||||
getStickerEmoji(): string {
|
||||
throw new Error('getStickerEmoji must be implemented in subclasses')
|
||||
}
|
||||
}
|
||||
|
||||
export class HeartStickerTool extends BaseStickerTool {
|
||||
static override id = 'heartSticker'
|
||||
static override name = 'Heart Sticker'
|
||||
|
||||
getStickerEmoji() {
|
||||
return '❤️'
|
||||
}
|
||||
}
|
||||
|
||||
export class StarStickerTool extends BaseStickerTool {
|
||||
static override id = 'starSticker'
|
||||
static override name = 'Star Sticker'
|
||||
|
||||
getStickerEmoji() {
|
||||
return '⭐'
|
||||
}
|
||||
}
|
||||
|
||||
export class SmileyStickerTool extends BaseStickerTool {
|
||||
static override id = 'smileySticker'
|
||||
static override name = 'Smiley Sticker'
|
||||
|
||||
getStickerEmoji() {
|
||||
return '😊'
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
import {
|
||||
TLComponents,
|
||||
TLUiOverrides,
|
||||
TldrawUiToastsProvider,
|
||||
TLUiToast,
|
||||
TLUiToastsContextType,
|
||||
Atom,
|
||||
atom
|
||||
} from '@tldraw/tldraw';
|
||||
import { ReactNode } from 'react';
|
||||
|
||||
import { regularComponentsIndex, presentationComponentsIndex } from './ui-overrides/components';
|
||||
import { presentationUiOverridesIndex, regularUiOverridesIndex } from './ui-overrides/overrides';
|
||||
|
||||
// Toast Wrapper Component
|
||||
const ToastWrapper = ({ children }: { children: ReactNode }) => {
|
||||
// Create custom toast overrides
|
||||
const toastOverrides = (): TLUiToastsContextType => {
|
||||
const toasts: Atom<TLUiToast[]> = atom('toasts', []);
|
||||
|
||||
return {
|
||||
addToast: (toast) => {
|
||||
const id = toast.id || Math.random().toString();
|
||||
const newToast: TLUiToast = { ...toast, id };
|
||||
toasts.set([...toasts.get(), newToast]);
|
||||
return id;
|
||||
},
|
||||
removeToast: (id) => {
|
||||
toasts.set(toasts.get().filter((t: TLUiToast) => t.id !== id));
|
||||
return id;
|
||||
},
|
||||
clearToasts: () => {
|
||||
toasts.set([]);
|
||||
},
|
||||
toasts
|
||||
};
|
||||
};
|
||||
|
||||
return (
|
||||
<TldrawUiToastsProvider overrides={toastOverrides}>
|
||||
{children}
|
||||
</TldrawUiToastsProvider>
|
||||
);
|
||||
};
|
||||
|
||||
// Function to get the appropriate UI overrides
|
||||
export const getUiOverrides = (presentationMode: boolean): TLUiOverrides => {
|
||||
return presentationMode ? presentationUiOverridesIndex : regularUiOverridesIndex;
|
||||
};
|
||||
|
||||
// Function to get the appropriate UI configuration
|
||||
export const getUiComponents = (presentationMode: boolean): TLComponents => {
|
||||
return presentationMode ? presentationComponents : regularComponents;
|
||||
};
|
||||
|
||||
// Regular components configuration
|
||||
const regularComponents: TLComponents = {
|
||||
Toolbar: regularComponentsIndex.Toolbar,
|
||||
InFrontOfTheCanvas: regularComponentsIndex.InFrontOfTheCanvas,
|
||||
KeyboardShortcutsDialog: regularComponentsIndex.KeyboardShortcutsDialog,
|
||||
HelperButtons: regularComponentsIndex.HelperButtons,
|
||||
ActionsMenu: regularComponentsIndex.ActionsMenu,
|
||||
ContextMenu: regularComponentsIndex.ContextMenu,
|
||||
DebugMenu: regularComponentsIndex.DebugMenu,
|
||||
HelpMenu: regularComponentsIndex.HelpMenu,
|
||||
MainMenu: regularComponentsIndex.MainMenu,
|
||||
NavigationPanel: regularComponentsIndex.NavigationPanel,
|
||||
PageMenu: regularComponentsIndex.PageMenu,
|
||||
QuickActions: regularComponentsIndex.QuickActions,
|
||||
StylePanel: regularComponentsIndex.StylePanel,
|
||||
ZoomMenu: regularComponentsIndex.ZoomMenu
|
||||
};
|
||||
|
||||
// Presentation components configuration
|
||||
const presentationComponents: TLComponents = {
|
||||
Toolbar: presentationComponentsIndex.Toolbar,
|
||||
InFrontOfTheCanvas: presentationComponentsIndex.InFrontOfTheCanvas,
|
||||
KeyboardShortcutsDialog: presentationComponentsIndex.KeyboardShortcutsDialog,
|
||||
HelperButtons: presentationComponentsIndex.HelperButtons,
|
||||
ActionsMenu: presentationComponentsIndex.ActionsMenu,
|
||||
ContextMenu: presentationComponentsIndex.ContextMenu,
|
||||
DebugMenu: presentationComponentsIndex.DebugMenu,
|
||||
HelpMenu: presentationComponentsIndex.HelpMenu,
|
||||
MainMenu: presentationComponentsIndex.MainMenu,
|
||||
NavigationPanel: presentationComponentsIndex.NavigationPanel,
|
||||
PageMenu: presentationComponentsIndex.PageMenu,
|
||||
QuickActions: presentationComponentsIndex.QuickActions,
|
||||
StylePanel: presentationComponentsIndex.StylePanel,
|
||||
ZoomMenu: presentationComponentsIndex.ZoomMenu
|
||||
};
|
||||
|
||||
export { ToastWrapper };
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
import { useEditor, TldrawUiButton } from '@tldraw/tldraw';
|
||||
|
||||
export function StickerDropdown() {
|
||||
const editor = useEditor()
|
||||
|
||||
const handleToolSelect = (toolId: string) => {
|
||||
editor.setCurrentTool(toolId)
|
||||
}
|
||||
|
||||
return (
|
||||
<div style={{
|
||||
position: 'absolute',
|
||||
bottom: '100%',
|
||||
left: '50%',
|
||||
transform: 'translateX(-50%)',
|
||||
backgroundColor: 'white',
|
||||
border: '1px solid #ccc',
|
||||
borderRadius: '4px',
|
||||
padding: '8px',
|
||||
zIndex: 1000,
|
||||
display: 'flex',
|
||||
gap: '8px',
|
||||
}}>
|
||||
<TldrawUiButton type="tool" onClick={() => handleToolSelect('heartSticker')} style={{ fontSize: '1.5rem' }}>❤️</TldrawUiButton>
|
||||
<TldrawUiButton type="tool" onClick={() => handleToolSelect('starSticker')} style={{ fontSize: '1.5rem' }}>⭐</TldrawUiButton>
|
||||
<TldrawUiButton type="tool" onClick={() => handleToolSelect('smileySticker')} style={{ fontSize: '1.5rem' }}>😊</TldrawUiButton>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
import React, { useState } from 'react';
|
||||
import { BasePanel } from './shared/BasePanel';
|
||||
import { CCExamMarkerPanel } from './shared/CCExamMarkerPanel';
|
||||
import { BaseContext, ViewContext } from '../../../../types/navigation';
|
||||
|
||||
interface CCPanelProps {
|
||||
examMarkerProps?: React.ComponentProps<typeof CCExamMarkerPanel>;
|
||||
isExpanded?: boolean;
|
||||
isPinned?: boolean;
|
||||
onExpandedChange?: (expanded: boolean) => void;
|
||||
onPinnedChange?: (pinned: boolean) => void;
|
||||
}
|
||||
|
||||
export const CCPanel: React.FC<CCPanelProps> = ({
|
||||
examMarkerProps,
|
||||
isExpanded,
|
||||
isPinned,
|
||||
onExpandedChange,
|
||||
onPinnedChange,
|
||||
}) => {
|
||||
const [currentContext, setCurrentContext] = useState<BaseContext>('profile');
|
||||
const [currentExtendedContext, setCurrentExtendedContext] = useState<ViewContext>('overview');
|
||||
const [isMenuOpen, setIsMenuOpen] = useState(false);
|
||||
|
||||
// Reset menu state when panel is closed
|
||||
const handleExpandedChange = (expanded: boolean) => {
|
||||
if (!expanded) {
|
||||
setIsMenuOpen(false);
|
||||
}
|
||||
onExpandedChange?.(expanded);
|
||||
};
|
||||
|
||||
return (
|
||||
<BasePanel
|
||||
examMarkerProps={examMarkerProps}
|
||||
isExpanded={isExpanded}
|
||||
isPinned={isPinned}
|
||||
onExpandedChange={handleExpandedChange}
|
||||
onPinnedChange={onPinnedChange}
|
||||
currentContext={currentContext}
|
||||
onContextChange={setCurrentContext}
|
||||
currentExtendedContext={currentExtendedContext}
|
||||
onExtendedContextChange={setCurrentExtendedContext}
|
||||
isMenuOpen={isMenuOpen}
|
||||
onMenuOpenChange={setIsMenuOpen}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,65 @@
|
||||
import { TLComponents } from '@tldraw/tldraw';
|
||||
|
||||
import { RegularToolbar } from './regular/toolbar';
|
||||
import { RegularHelperButtons } from './regular/helperButton';
|
||||
// import { RegularHelpMenu } from './regular/helpMenu';
|
||||
// import { RegularMainMenu } from './regular/mainMenu';
|
||||
import { RegularNavigationPanel } from './regular/navigationPanel';
|
||||
// import { RegularPageMenu } from './regular/pageMenu';
|
||||
// import { RegularQuickActions } from './regular/quickActions';
|
||||
import { RegularStylePanel } from './regular/stylePanel';
|
||||
import { RegularZoomMenu } from './regular/zoomMenu';
|
||||
import { RegularKeyboardShortcutsDialog } from './regular/keyboardShortcutsDialog';
|
||||
// import { RegularActionsMenu } from './regular/actionsMenu';
|
||||
import { RegularContextMenu } from './regular/contextMenu';
|
||||
import { RegularDebugMenu } from './regular/debugMenu';
|
||||
|
||||
import { PresentationToolbar } from './presentation/toolbar';
|
||||
import { PresentationHelperButtons } from './presentation/helperButton';
|
||||
// import { PresentationHelpMenu } from './presentation/helpMenu';
|
||||
// import { PresentationMainMenu } from './presentation/mainMenu';
|
||||
import { PresentationNavigationPanel } from './presentation/navigationPanel';
|
||||
// import { PresentationPageMenu } from './presentation/pageMenu';
|
||||
import { PresentationQuickActions } from './presentation/quickActions';
|
||||
import { PresentationStylePanel } from './presentation/stylePanel';
|
||||
import { PresentationZoomMenu } from './presentation/zoomMenu';
|
||||
import { PresentationKeyboardShortcutsDialog } from './presentation/keyboardShortcutsDialog';
|
||||
import { PresentationActionsMenu } from './presentation/actionsMenu';
|
||||
import { PresentationContextMenu } from './presentation/contextMenu';
|
||||
import { PresentationDebugMenu } from './presentation/debugMenu';
|
||||
import { CCPanel } from './CCPanel';
|
||||
|
||||
export const regularComponentsIndex: TLComponents = {
|
||||
Toolbar: RegularToolbar,
|
||||
InFrontOfTheCanvas: CCPanel,
|
||||
HelperButtons: RegularHelperButtons,
|
||||
// HelpMenu: RegularHelpMenu,
|
||||
// MainMenu: RegularMainMenu,
|
||||
NavigationPanel: RegularNavigationPanel,
|
||||
// PageMenu: RegularPageMenu,
|
||||
// QuickActions: RegularQuickActions,
|
||||
StylePanel: RegularStylePanel,
|
||||
ZoomMenu: RegularZoomMenu,
|
||||
KeyboardShortcutsDialog: RegularKeyboardShortcutsDialog,
|
||||
// ActionsMenu: RegularActionsMenu,
|
||||
ContextMenu: RegularContextMenu,
|
||||
DebugMenu: RegularDebugMenu,
|
||||
};
|
||||
|
||||
export const presentationComponentsIndex: TLComponents = {
|
||||
Toolbar: PresentationToolbar,
|
||||
InFrontOfTheCanvas: CCPanel,
|
||||
HelperButtons: PresentationHelperButtons,
|
||||
// HelpMenu: PresentationHelpMenu,
|
||||
// MainMenu: PresentationMainMenu,
|
||||
NavigationPanel: PresentationNavigationPanel,
|
||||
// PageMenu: PresentationPageMenu,
|
||||
QuickActions: PresentationQuickActions,
|
||||
StylePanel: PresentationStylePanel,
|
||||
ZoomMenu: PresentationZoomMenu,
|
||||
KeyboardShortcutsDialog: PresentationKeyboardShortcutsDialog,
|
||||
ActionsMenu: PresentationActionsMenu,
|
||||
ContextMenu: PresentationContextMenu,
|
||||
DebugMenu: PresentationDebugMenu,
|
||||
};
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
import {
|
||||
DefaultActionsMenu,
|
||||
DefaultActionsMenuContent,
|
||||
} from '@tldraw/tldraw';
|
||||
|
||||
export const PresentationActionsMenu = () => {
|
||||
return (
|
||||
<DefaultActionsMenu>
|
||||
<DefaultActionsMenuContent />
|
||||
</DefaultActionsMenu>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,15 @@
|
||||
import {
|
||||
DefaultContextMenu,
|
||||
DefaultContextMenuContent,
|
||||
TLUiContextMenuProps,
|
||||
} from '@tldraw/tldraw';
|
||||
|
||||
export const PresentationContextMenu = (props: TLUiContextMenuProps) => {
|
||||
return (
|
||||
<div>
|
||||
<DefaultContextMenu {...props}>
|
||||
<DefaultContextMenuContent />
|
||||
</DefaultContextMenu>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,15 @@
|
||||
import {
|
||||
DefaultDebugMenu,
|
||||
DefaultDebugMenuContent,
|
||||
TLUiDebugMenuProps,
|
||||
} from '@tldraw/tldraw';
|
||||
|
||||
export const PresentationDebugMenu = (props: TLUiDebugMenuProps) => {
|
||||
return (
|
||||
<div>
|
||||
<DefaultDebugMenu {...props}>
|
||||
<DefaultDebugMenuContent />
|
||||
</DefaultDebugMenu>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,15 @@
|
||||
import {
|
||||
DefaultHelpMenu,
|
||||
DefaultHelpMenuContent,
|
||||
TLUiHelpMenuProps,
|
||||
} from '@tldraw/tldraw';
|
||||
|
||||
export const PresentationHelpMenu = (props: TLUiHelpMenuProps) => {
|
||||
return (
|
||||
<div>
|
||||
<DefaultHelpMenu {...props}>
|
||||
<DefaultHelpMenuContent />
|
||||
</DefaultHelpMenu>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,15 @@
|
||||
import {
|
||||
DefaultHelperButtons,
|
||||
DefaultHelperButtonsContent,
|
||||
TLUiHelperButtonsProps,
|
||||
} from '@tldraw/tldraw';
|
||||
|
||||
export const PresentationHelperButtons = (props: TLUiHelperButtonsProps) => {
|
||||
return (
|
||||
<div>
|
||||
<DefaultHelperButtons {...props}>
|
||||
<DefaultHelperButtonsContent />
|
||||
</DefaultHelperButtons>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user