Switched to using Verbum (https://github.com/ozanyurtsever/verbum) which builds on Lexical to create a rich text editor; Isolated the rich text editor into a component so it can be easily swapped out; Added a higher resolution background for the student login page; Still a lot of bugs in the workshop list view.

This commit is contained in:
2022-10-27 08:56:26 -07:00
parent 77b420ea6f
commit cab09e59b9
10 changed files with 433 additions and 24 deletions

View File

@@ -14,7 +14,7 @@ standard-minifier-css@1.8.1 # CSS minifier run for production mode
standard-minifier-js@2.8.0 # JS minifier run for production mode standard-minifier-js@2.8.0 # JS minifier run for production mode
es5-shim@4.8.0 # ECMAScript 5 compatibility for older browsers es5-shim@4.8.0 # ECMAScript 5 compatibility for older browsers
ecmascript@0.16.2 # Enable ECMAScript2015+ syntax in app code ecmascript@0.16.2 # Enable ECMAScript2015+ syntax in app code
typescript@4.5.4 # Enable TypeScript syntax in .ts and .tsx modules typescript # Enable TypeScript syntax in .ts and .tsx modules
shell-server@0.5.0 # Server-side component of the `meteor shell` command shell-server@0.5.0 # Server-side component of the `meteor shell` command
static-html@1.3.2 # Define static page content in .html files static-html@1.3.2 # Define static page content in .html files

View File

@@ -7,5 +7,6 @@ import "./sites.js";
import "./asset-types.js"; import "./asset-types.js";
import "./assets.js"; import "./assets.js";
import "./asset-assignment-history.js"; import "./asset-assignment-history.js";
import "./workshops.js";
// console.log("Finished setting up server side models."); // console.log("Finished setting up server side models.");

View File

@@ -29,7 +29,7 @@ Meteor.methods({
let signupSheet = []; let signupSheet = [];
check(name, String); check(name, String);
check(description, String); check(description, Match.Maybe(String));
// Match a positive integer or undefined/null. // Match a positive integer or undefined/null.
check(signupLimit, Match.Where((x) => { check(signupLimit, Match.Where((x) => {
check(x, Match.Maybe(Match.Integer)); check(x, Match.Maybe(Match.Integer));

View File

@@ -14,7 +14,7 @@ import Users from './pages/Users'
import Admin from './pages/Admin' import Admin from './pages/Admin'
import Home from './pages/Home' import Home from './pages/Home'
import {StudentPage} from './pages/Student/StudentPage' import {StudentPage} from './pages/Student/StudentPage'
import {Workshops} from './pages/Student/Workshops' import {WorkshopList} from './pages/Student/WorkshopList'
const appTheme = createTheme({ const appTheme = createTheme({
components: { components: {
@@ -92,7 +92,7 @@ export const App = (props) => {
}/> }/>
<Route path="/student" element={ <Route path="/student" element={
<StudentPage> <StudentPage>
{user && <Workshops/>} {user && <WorkshopList/>}
</StudentPage> </StudentPage>
}/> }/>
<Route path="/assignments/*" element={ <Route path="/assignments/*" element={

View File

@@ -0,0 +1,122 @@
// import {RichTextPlugin} from "@lexical/react/LexicalRichTextPlugin";
// import {ContentEditable} from "@lexical/react/LexicalContentEditable";
// import {OnChangePlugin} from "@lexical/react/LexicalOnChangePlugin";
// import {HistoryPlugin} from "@lexical/react/LexicalHistoryPlugin";
// import {LinkPlugin} from "@lexical/react/LexicalLinkPlugin";
// import {MarkdownShortcutPlugin} from "@lexical/react/LexicalMarkdownShortcutPlugin";
// import {LexicalComposer} from "@lexical/react/LexicalComposer";
// import {AutoFocusPlugin} from '@lexical/react/LexicalAutoFocusPlugin';
// import {AutoScrollPlugin} from '@lexical/react/LexicalAutoScrollPlugin';
// import {CharacterLimitPlugin} from '@lexical/react/LexicalCharacterLimitPlugin';
// import {CheckListPlugin} from '@lexical/react/LexicalCheckListPlugin';
// import {ClearEditorPlugin} from '@lexical/react/LexicalClearEditorPlugin';
// import {CollaborationPlugin} from '@lexical/react/LexicalCollaborationPlugin';
// import {HashtagPlugin} from '@lexical/react/LexicalHashtagPlugin';
// import {ListPlugin} from '@lexical/react/LexicalListPlugin';
// // import {PlainTextPlugin} from '@lexical/react/LexicalPlainTextPlugin';
// import {TablePlugin} from '@lexical/react/LexicalTablePlugin';
// import {createWebsocketProvider} from './collaboration'
// import {useSettings} from './context/SettingsContext'
// import {useSharedHistoryContext} from './context/SharedHistoryContext'
// import TableCellNodes from './nodes/TableCellNodes'
// import ActionsPlugin from './plugins/ActionsPlugin'
// import AutocompletePlugin from './plugins/AutocompletePlugin'
// import AutoEmbedPlugin from './plugins/AutoEmbedPlugin'
// import AutoLinkPlugin from './plugins/AutoLinkPlugin'
// import ClickableLinkPlugin from './plugins/ClickableLinkPlugin'
// import CodeActionMenuPlugin from './plugins/CodeActionMenuPlugin';
// import CodeHighlightPlugin from './plugins/CodeHighlightPlugin'
// import CollapsiblePlugin from './plugins/CollapsiblePlugin'
// import CommentPlugin from './plugins/CommentPlugin'
// import ComponentPickerPlugin from './plugins/ComponentPickerPlugin'
// import DraggableBlockPlugin from './plugins/DraggableBlockPlugin'
// import EmojiPickerPlugin from './plugins/EmojiPickerPlugin'
// import EmojisPlugin from './plugins/EmojisPlugin'
// import EquationsPlugin from './plugins/EquationsPlugin'
// import ExcalidrawPlugin from './plugins/ExcalidrawPlugin'
// import FigmaPlugin from './plugins/FigmaPlugin'
// import FloatingLinkEditorPlugin from './plugins/FloatingLinkEditorPlugin'
// import FloatingTextFormatToolbarPlugin from './plugins/FloatingTextFormatToolbarPlugin'
// import HorizontalRulePlugin from './plugins/HorizontalRulePlugin'
// import ImagesPlugin from './plugins/ImagesPlugin'
// import KeywordsPlugin from './plugins/KeywordsPlugin'
// import ListMaxIndentLevelPlugin from './plugins/ListMaxIndentLevelPlugin'
// import MarkdownShortcutPlugin from './plugins/MarkdownShortcutPlugin'
// import {MaxLengthPlugin} from './plugins/MaxLengthPlugin'
// import MentionsPlugin from './plugins/MentionsPlugin'
// import PollPlugin from './plugins/PollPlugin'
// import SpeechToTextPlugin from './plugins/SpeechToTextPlugin'
// import TabFocusPlugin from './plugins/TabFocusPlugin'
// import TableCellActionMenuPlugin from './plugins/TableActionMenuPlugin'
// import TableCellResizer from './plugins/TableCellResizer'
// import TableOfContentsPlugin from './plugins/TableOfContentsPlugin'
// import {TablePlugin as NewTablePlugin} from './plugins/TablePlugin'
// import ToolbarPlugin from './plugins/ToolbarPlugin'
// import TreeViewPlugin from './plugins/TreeViewPlugin'
// import TwitterPlugin from './plugins/TwitterPlugin'
// import YouTubePlugin from './plugins/YouTubePlugin'
// import PlaygroundEditorTheme from './themes/PlaygroundEditorTheme'
// import ContentEditable from './ui/ContentEditable'
// import ErrorBoundary from './ui/ErrorBoundary'
// import Placeholder from './ui/Placeholder'
import * as React from 'react';
import {useRef, useState} from 'react';
import {
EditorComposer,
Editor as InternalEditor,
ToolbarPlugin,
AlignDropdown,
BackgroundColorPicker,
BoldButton,
CodeFormatButton,
FloatingLinkEditor,
FontFamilyDropdown,
FontSizeDropdown,
InsertDropdown,
InsertLinkButton,
ItalicButton,
TextColorPicker,
TextFormatDropdown,
UnderlineButton,
Divider,
} from 'verbum';
// import nodes from './Nodes'
export const Editor = (props) => {
return (
// <LexicalComposer initialConfig={{editorState: props.state, namespace: props.namespace, onError: props.onError, theme: props.theme, nodes}}>
// <ToolbarPlugin/>
// <RichTextPlugin contentEditable={<ContentEditable/>} placeholder={<div>Sample...</div>}/>
// {props.onChange && <OnChangePlugin onChange={props.onChange} ignoreSelectionChange={true}/>}
// <HistoryPlugin/>
// <LinkPlugin/>
// <MarkdownShortcutPlugin/>
// </LexicalComposer>
<EditorComposer>
<InternalEditor hashtagsEnabled={true} onChange={props.onChange} initialEditorState={props.state}>
<ToolbarPlugin defaultFontSize="20px">
<FontFamilyDropdown />
<FontSizeDropdown />
<Divider />
<BoldButton />
<ItalicButton />
<UnderlineButton />
<CodeFormatButton />
<InsertLinkButton />
<TextColorPicker />
<BackgroundColorPicker />
<TextFormatDropdown />
<Divider />
<InsertDropdown enablePoll={true} enableTable={true} />
<Divider />
<AlignDropdown />
</ToolbarPlugin>
</InternalEditor>
</EditorComposer>
)
}

View File

@@ -0,0 +1,65 @@
/*
import {CodeHighlightNode, CodeNode} from '@lexical/code';
import {HashtagNode} from '@lexical/hashtag';
import {AutoLinkNode, LinkNode} from '@lexical/link';
import {ListItemNode, ListNode} from '@lexical/list';
import {MarkNode} from '@lexical/mark';
import {OverflowNode} from '@lexical/overflow';
import {HorizontalRuleNode} from '@lexical/react/LexicalHorizontalRuleNode';
import {HeadingNode, QuoteNode} from '@lexical/rich-text';
import {TableCellNode, TableNode, TableRowNode} from '@lexical/table';
// import {CollapsibleContainerNode} from '../plugins/CollapsiblePlugin/CollapsibleContainerNode';
// import {CollapsibleContentNode} from '../plugins/CollapsiblePlugin/CollapsibleContentNode';
// import {CollapsibleTitleNode} from '../plugins/CollapsiblePlugin/CollapsibleTitleNode';
// import {AutocompleteNode} from './AutocompleteNode';
// import {EmojiNode} from './EmojiNode';
// import {EquationNode} from './EquationNode';
// import {ExcalidrawNode} from './ExcalidrawNode';
// import {FigmaNode} from './FigmaNode';
// import {ImageNode} from './ImageNode';
// import {KeywordNode} from './KeywordNode';
// import {MentionNode} from './MentionNode';
// import {PollNode} from './PollNode';
// import {StickyNode} from './StickyNode';
// import {TableNode as NewTableNode} from './TableNode';
// import {TweetNode} from './TweetNode';
// import {YouTubeNode} from './YouTubeNode';
const Nodes = [
HeadingNode,
ListNode,
ListItemNode,
QuoteNode,
CodeNode,
TableNode,
TableCellNode,
TableRowNode,
HashtagNode,
CodeHighlightNode,
AutoLinkNode,
LinkNode,
OverflowNode,
HorizontalRuleNode,
MarkNode,
// NewTableNode,
// PollNode,
// StickyNode,
// ImageNode,
// MentionNode,
// EmojiNode,
// ExcalidrawNode,
// EquationNode,
// AutocompleteNode,
// KeywordNode,
// TweetNode,
// YouTubeNode,
// FigmaNode,
// CollapsibleContainerNode,
// CollapsibleContentNode,
// CollapsibleTitleNode,
];
export default Nodes;
*/

View File

@@ -1,4 +1,4 @@
import React, { useState } from 'react'; import React, { useState, useEffect } from 'react';
import {Meteor} from "meteor/meteor"; import {Meteor} from "meteor/meteor";
import {Roles} from 'meteor/alanning:roles'; import {Roles} from 'meteor/alanning:roles';
import { useTracker } from 'meteor/react-meteor-data'; import { useTracker } from 'meteor/react-meteor-data';
@@ -16,11 +16,23 @@ import DialogActions from '@mui/material/DialogActions';
import DialogContent from '@mui/material/DialogContent'; import DialogContent from '@mui/material/DialogContent';
import DialogContentText from '@mui/material/DialogContentText'; import DialogContentText from '@mui/material/DialogContentText';
import DialogTitle from '@mui/material/DialogTitle'; import DialogTitle from '@mui/material/DialogTitle';
import {Editor} from 'react-draft-wysiwyg'
import 'react-draft-wysiwyg/dist/react-draft-wysiwyg.css'
import {Students} from "/imports/api/students"; import {Students} from "/imports/api/students";
import {Staff} from "/imports/api/staff"; import {Staff} from "/imports/api/staff";
import {conditions} from "/imports/api/assets"; import {conditions} from "/imports/api/assets";
import {Workshops} from "/imports/api/workshops";
import {$getRoot, $getSelection} from 'lexical';
import {LexicalComposer} from '@lexical/react/LexicalComposer';
import {PlainTextPlugin} from '@lexical/react/LexicalPlainTextPlugin';
import {ContentEditable} from '@lexical/react/LexicalContentEditable';
import {HistoryPlugin} from '@lexical/react/LexicalHistoryPlugin';
import {OnChangePlugin} from '@lexical/react/LexicalOnChangePlugin';
import {useLexicalComposerContext} from '@lexical/react/LexicalComposerContext';
import {RichTextPlugin} from "@lexical/react/LexicalRichTextPlugin";
import {LinkPlugin} from "@lexical/react/LexicalLinkPlugin";
import {MarkdownShortcutPlugin} from "@lexical/react/LexicalMarkdownShortcutPlugin";
import {AutoLinkNode, LinkNode} from '@lexical/link';
import {Editor} from "/imports/ui/components/RichText/Editor";
const cssTwoColumnContainer = { const cssTwoColumnContainer = {
display: 'grid', display: 'grid',
@@ -29,7 +41,7 @@ const cssTwoColumnContainer = {
rowGap: '0.4rem', rowGap: '0.4rem',
} }
export const Workshops = () => { export const WorkshopList = () => {
Meteor.subscribe('students'); Meteor.subscribe('students');
Meteor.subscribe('staff'); Meteor.subscribe('staff');
Meteor.subscribe('workshops'); Meteor.subscribe('workshops');
@@ -59,45 +71,65 @@ export const Workshops = () => {
} }
} }
const [openWorkshopEditor, setOpenWorkshopEditor] = useState(false)
const [editedWorkshop, setEditedWorkshop] = useState({})
const [editedName, setEditedName] = useState("")
const [editedDescription, setEditedDescription] = useState("")
const [editedSignupLimit, setEditedSignupLimit] = useState("")
const newWorkshop = () => { const newWorkshop = () => {
if(isAdmin) { if(isAdmin) {
setEditedDescription("")
setEditedWorkshop({}) setEditedWorkshop({})
setEditedDescription("")
setEditedName("")
setEditedSignupLimit("")
setOpenWorkshopEditor(true) setOpenWorkshopEditor(true)
} }
} }
const editWorkshop = () => { const editWorkshop = () => {
if(isAdmin && selectedWorkshop) { if(isAdmin && selectedWorkshop) {
setEditedWorkshop({...selectedWorkshop}) setEditedWorkshop({...selectedWorkshop})
setEditedDescription(selectedWorkshop.description ? selectedWorkshop.description : "")
setEditedName(selectedWorkshop.name ? selectedWorkshop.name : "")
setEditedSignupLimit(selectedWorkshop.signupLimit ? selectedWorkshop.signupLimit : "")
setOpenWorkshopEditor(true) setOpenWorkshopEditor(true)
} }
} }
const [openWorkshopEditor, setOpenWorkshopEditor] = useState(false)
const [editedWorkshop, setEditedWorkshop] = useState(false)
const workshopEditorClosed = (save) => { const workshopEditorClosed = (save) => {
const completeHandler = (err, result) => { const completeHandler = (err, result) => {
if(err) console.error(err) if(err) console.error(err)
else { else {
setOpenWorkshopEditor(false) setOpenWorkshopEditor(false)
setEditedWorkshop(null)
} }
} }
if(save) { if(save) {
if(editedWorkshop._id) Meteor.call('workshops.update', editedWorkshop._id, editedWorkshop.name, editedWorkshop.description, editedWorkshop.signupLimit, completeHandler) if(editedWorkshop._id) Meteor.call('workshops.update', editedWorkshop._id, editedName, editedDescription, editedSignupLimit, completeHandler)
else Meteor.call('workshops.add', editedWorkshop.name, editedWorkshop.description, editedWorkshop.signupLimit, completeHandler) else Meteor.call('workshops.add', editedName, editedDescription, editedSignupLimit, completeHandler)
} }
else completeHandler() else completeHandler()
} }
const onLexicalComposerError = (err) => {
console.error(err)
}
const lexicalComposerTheme = {
}
const lexicalComposerChanged = (e) => {
console.log(e)
}
const descriptionEditorNodes = [LinkNode]
return ( return (
<> <>
<Dialog open={openWorkshopEditor} onClose={workshopEditorClosed}> <Dialog open={openWorkshopEditor} onClose={workshopEditorClosed}>
<DialogTitle>Workshop Editor</DialogTitle> <DialogTitle>Workshop Editor</DialogTitle>
<DialogContent style={{display: 'flex', flexDirection: 'column'}}> <DialogContent style={{display: 'flex', flexDirection: 'column'}}>
<TextField style={{marginTop: '1rem',minWidth: '30rem'}} variant="standard" label="Name" value={editedWorkshop.name} onChange={(e) => {editedWorkshop.name = e.target.value; setEditedWorkshop(editedWorkshop)}}/> <TextField style={{marginTop: '1rem',minWidth: '30rem'}} variant="standard" label="Name" value={editedName} onChange={(e) => {setEditedName(e.target.value)}}/>
<Editor editorState={selectedWorkshop.description} toolbarClassName="editorToolbar" wrapperClassName="editorWrapper" editorClassName="editor" onEditorStateChange={(e) => {selectedWorkshop.description = e.target.value; setEditedWorkshop(editedWorkshop)}}/> <Editor config={{namespace: 'WorkshopDescriptionEditor', state: selectedWorkshop ? selectedWorkshop.description : "", theme: lexicalComposerTheme}}></Editor>
<TextField style={{marginTop: '1rem',minWidth: '30rem'}} variant="standard" type="number" label="Signup Limit" value={editedWorkshop.signupLimit} onChange={(e) => {editedWorkshop.signupLimit = e.target.value; setEditedWorkshop(editedWorkshop)}}/> <TextField style={{marginTop: '1rem',minWidth: '30rem'}} variant="standard" type="number" label="Signup Limit" value={editedSignupLimit} onChange={(e) => {setEditedSignupLimit(e.target.value)}}/>
</DialogContent> </DialogContent>
<DialogActions> <DialogActions>
<Button onClick={() => workshopEditorClosed(true)}>Save</Button> <Button onClick={() => workshopEditorClosed(true)}>Save</Button>
@@ -121,10 +153,10 @@ export const Workshops = () => {
<Box style={{...cssTwoColumnContainer}}> <Box style={{...cssTwoColumnContainer}}>
<Paper> <Paper>
{/*<Editor editorState={selectedWorkshop.description} toolbarClassName="editorToolbar" wrapperClassName="editorWrapper" editorClassName="editor" onEditorStateChange={(e) => {selectedWorkshop.description = ""}}/>*/} {/*<Editor editorState={selectedWorkshop.description} toolbarClassName="editorToolbar" wrapperClassName="editorWrapper" editorClassName="editor" onEditorStateChange={(e) => {selectedWorkshop.description = ""}}/>*/}
{`${selectedWorkshop.description}`} {selectedWorkshop && `${selectedWorkshop.description}`}
</Paper> </Paper>
<List> <List>
{selectedWorkshop.signupSheet.map((next, i) => { {selectedWorkshop && selectedWorkshop.signupSheet.map((next, i) => {
return ( return (
<ListItem key={next._id}> <ListItem key={next._id}>
<ListItemText primary={next.data.firstName + " " + next.data.lastName} secondary={next.data.email}/> <ListItemText primary={next.data.firstName + " " + next.data.lastName} secondary={next.data.email}/>

View File

@@ -10,25 +10,29 @@
"@babel/runtime": "^7.16.7", "@babel/runtime": "^7.16.7",
"@emotion/react": "^11.10.0", "@emotion/react": "^11.10.0",
"@emotion/styled": "^11.10.0", "@emotion/styled": "^11.10.0",
"@lexical/headless": "^0.5.0",
"@lexical/link": "^0.5.0",
"@lexical/react": "^0.5.0",
"@mui/icons-material": "^5.10.2", "@mui/icons-material": "^5.10.2",
"@mui/material": "^5.10.2", "@mui/material": "^5.10.2",
"bcrypt": "^5.0.1", "bcrypt": "^5.0.1",
"classnames": "^2.2.6", "classnames": "^2.2.6",
"csv-parse": "^5.3.0", "csv-parse": "^5.3.0",
"dayjs": "^1.11.3", "dayjs": "^1.11.3",
"draft-js": "^0.11.7",
"html5-qrcode": "^2.2.0", "html5-qrcode": "^2.2.0",
"jquery": "^3.6.0", "jquery": "^3.6.0",
"lexical": "^0.5.0",
"lodash": "^4.17.15", "lodash": "^4.17.15",
"meteor-node-stubs": "^1.0.0", "meteor-node-stubs": "^1.0.0",
"moment": "^2.29.2", "moment": "^2.29.2",
"mongodb": "^4.4.1", "mongodb": "^4.4.1",
"react": "^18.2.0", "react": "^18.2.0",
"react-dom": "^18.2.0", "react-dom": "^18.2.0",
"react-draft-wysiwyg": "^1.15.0",
"react-router-dom": "^6.3.0", "react-router-dom": "^6.3.0",
"typescript": "^4.8.4",
"umbrellajs": "^3.3.1", "umbrellajs": "^3.3.1",
"underscore": "^1.13.2", "underscore": "^1.13.2",
"verbum": "^0.4.0",
"winston": "^3.7.2", "winston": "^3.7.2",
"winston-daily-rotate-file": "^4.6.1", "winston-daily-rotate-file": "^4.6.1",
"ws": "^8.4.2" "ws": "^8.4.2"

182
public/images/student2.svg Normal file

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 8.3 MiB

View File

@@ -1,7 +1,7 @@
{ {
"compilerOptions": { "compilerOptions": {
/* Basic Options */ /* Basic Options */
"target": "es2018", "target": "es2019",
"module": "esNext", "module": "esNext",
"lib": ["esnext", "dom"], "lib": ["esnext", "dom"],
"allowJs": true, "allowJs": true,
@@ -25,7 +25,7 @@
"baseUrl": ".", "baseUrl": ".",
"paths": { "paths": {
/* Support absolute /imports/* with a leading '/' */ /* Support absolute /imports/* with a leading '/' */
"/*": ["*"] "/*": ["*"],
}, },
"moduleResolution": "node", "moduleResolution": "node",
"resolveJsonModule": true, "resolveJsonModule": true,
@@ -33,6 +33,9 @@
"esModuleInterop": true, "esModuleInterop": true,
"preserveSymlinks": true "preserveSymlinks": true
}, },
"include": [
"./imports/**"
],
"exclude": [ "exclude": [
"./.meteor/**", "./.meteor/**",
"./packages/**" "./packages/**"