mirror of
https://github.com/alexmickelson/canvasManagement.git
synced 2026-03-26 07:38:33 -06:00
moving v2 to top level
This commit is contained in:
30
src/components/ButtonSelect.tsx
Normal file
30
src/components/ButtonSelect.tsx
Normal file
@@ -0,0 +1,30 @@
|
||||
import React from "react";
|
||||
|
||||
export default function ButtonSelect<T>({
|
||||
options,
|
||||
getName,
|
||||
setSelectedOption,
|
||||
selectedOption,
|
||||
}: {
|
||||
options: T[];
|
||||
getName: (value: T | undefined) => string;
|
||||
setSelectedOption: (value: T | undefined) => void;
|
||||
selectedOption: T | undefined;
|
||||
}) {
|
||||
return (
|
||||
<div className="flex flex-row gap-3">
|
||||
{options.map((o) => (
|
||||
<button
|
||||
type="button"
|
||||
key={getName(o)}
|
||||
className={
|
||||
getName(o) === getName(selectedOption) ? " " : "unstyled btn-outline"
|
||||
}
|
||||
onClick={() => setSelectedOption(o)}
|
||||
>
|
||||
{getName(o)}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
13
src/components/ClientOnly.tsx
Normal file
13
src/components/ClientOnly.tsx
Normal file
@@ -0,0 +1,13 @@
|
||||
"use client";
|
||||
import React, { ReactNode, useEffect, useState } from "react";
|
||||
|
||||
export default function ClientOnly({ children }: { children: ReactNode }) {
|
||||
const [isClient, setIsClient] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
setIsClient(true);
|
||||
}, []);
|
||||
|
||||
if (!isClient) return <></>;
|
||||
return <>{children}</>;
|
||||
}
|
||||
36
src/components/Expandable.tsx
Normal file
36
src/components/Expandable.tsx
Normal file
@@ -0,0 +1,36 @@
|
||||
"use client";
|
||||
import { ReactNode, Dispatch, SetStateAction, useState, useRef } from "react";
|
||||
|
||||
export function Expandable({
|
||||
children,
|
||||
ExpandableElement,
|
||||
defaultExpanded = false,
|
||||
}: {
|
||||
children: ReactNode;
|
||||
ExpandableElement: (props: {
|
||||
setIsExpanded: Dispatch<SetStateAction<boolean>>;
|
||||
isExpanded: boolean;
|
||||
}) => ReactNode;
|
||||
defaultExpanded?: boolean;
|
||||
}) {
|
||||
const [isExpanded, setIsExpanded] = useState(defaultExpanded);
|
||||
const expandRef = useRef<HTMLDivElement | null>(null);
|
||||
|
||||
return (
|
||||
<>
|
||||
<ExpandableElement
|
||||
setIsExpanded={setIsExpanded}
|
||||
isExpanded={isExpanded}
|
||||
/>
|
||||
<div
|
||||
ref={expandRef}
|
||||
className={` overflow-hidden transition-all `}
|
||||
style={{
|
||||
maxHeight: isExpanded ? expandRef?.current?.scrollHeight : "0",
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
73
src/components/Modal.tsx
Normal file
73
src/components/Modal.tsx
Normal file
@@ -0,0 +1,73 @@
|
||||
"use client";
|
||||
import React, { ReactNode, useCallback, useMemo, useState } from "react";
|
||||
|
||||
export interface ModalControl {
|
||||
isOpen: boolean;
|
||||
openModal: () => void;
|
||||
closeModal: () => void;
|
||||
}
|
||||
|
||||
export function useModal() {
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
|
||||
const openModal = useCallback(() => setIsOpen(true), []);
|
||||
const closeModal = useCallback(() => setIsOpen(false), []);
|
||||
|
||||
return useMemo(
|
||||
() => ({
|
||||
isOpen,
|
||||
openModal,
|
||||
closeModal,
|
||||
}),
|
||||
[closeModal, isOpen, openModal]
|
||||
);
|
||||
}
|
||||
|
||||
export default function Modal({
|
||||
children,
|
||||
buttonText,
|
||||
buttonClass = "",
|
||||
modalWidth = "w-1/3",
|
||||
modalControl,
|
||||
}: {
|
||||
children: (props: { closeModal: () => void }) => ReactNode;
|
||||
buttonText: string;
|
||||
buttonClass?: string;
|
||||
modalWidth?: string;
|
||||
modalControl: ModalControl;
|
||||
}) {
|
||||
return (
|
||||
<>
|
||||
<button onClick={modalControl.openModal} className={buttonClass}>
|
||||
{buttonText}
|
||||
</button>
|
||||
|
||||
<div
|
||||
className={
|
||||
" fixed inset-0 flex items-center justify-center transition-all duration-400 h-screen " +
|
||||
" bg-black" +
|
||||
(modalControl.isOpen
|
||||
? " bg-opacity-50 z-50 "
|
||||
: " bg-opacity-0 -z-50 ")
|
||||
}
|
||||
onClick={modalControl.closeModal}
|
||||
>
|
||||
<div
|
||||
onClick={(e) => {
|
||||
// e.preventDefault();
|
||||
e.stopPropagation();
|
||||
}}
|
||||
className={
|
||||
` bg-slate-800 p-6 rounded-lg shadow-lg ` +
|
||||
modalWidth +
|
||||
` transition-all duration-400 ` +
|
||||
` ${modalControl.isOpen ? "opacity-100" : "opacity-0"}`
|
||||
}
|
||||
>
|
||||
{modalControl.isOpen &&
|
||||
children({ closeModal: modalControl.closeModal })}
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
10
src/components/Spinner.tsx
Normal file
10
src/components/Spinner.tsx
Normal file
@@ -0,0 +1,10 @@
|
||||
import React from "react";
|
||||
import "./spinner.css"
|
||||
|
||||
export const Spinner = () => {
|
||||
return (
|
||||
<div className="text-center h-full ">
|
||||
<span className="loader my-auto "></span>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
39
src/components/SuspenseAndErrorHandling.tsx
Normal file
39
src/components/SuspenseAndErrorHandling.tsx
Normal file
@@ -0,0 +1,39 @@
|
||||
import { getErrorMessage } from "@/services/utils/queryClient";
|
||||
import { QueryErrorResetBoundary } from "@tanstack/react-query";
|
||||
import { FC, ReactNode, Suspense } from "react";
|
||||
import { ErrorBoundary } from "react-error-boundary";
|
||||
import { Spinner } from "./Spinner";
|
||||
import toast from "react-hot-toast";
|
||||
|
||||
export const SuspenseAndErrorHandling: FC<{
|
||||
children: ReactNode;
|
||||
showToast?: boolean;
|
||||
}> = ({ children, showToast = true }) => {
|
||||
return (
|
||||
<QueryErrorResetBoundary>
|
||||
{({ reset }) => (
|
||||
<ErrorBoundary
|
||||
onReset={reset}
|
||||
fallbackRender={(props) => {
|
||||
if (showToast) {
|
||||
toast.error(getErrorMessage(props.error));
|
||||
}
|
||||
return (
|
||||
<div className="text-center">
|
||||
<div className="p-3">{getErrorMessage(props.error)}</div>
|
||||
<button
|
||||
className="btn btn-outline-secondary"
|
||||
onClick={() => props.resetErrorBoundary()}
|
||||
>
|
||||
Try again
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}}
|
||||
>
|
||||
<Suspense fallback={<Spinner />}>{children}</Suspense>
|
||||
</ErrorBoundary>
|
||||
)}
|
||||
</QueryErrorResetBoundary>
|
||||
);
|
||||
};
|
||||
99
src/components/TimePicker.tsx
Normal file
99
src/components/TimePicker.tsx
Normal file
@@ -0,0 +1,99 @@
|
||||
"use client";
|
||||
import { SimpleTimeOnly } from "@/models/local/localCourseSettings";
|
||||
import { FC, useState, useEffect } from "react";
|
||||
|
||||
export const TimePicker: FC<{
|
||||
setChosenTime: (simpleTime: SimpleTimeOnly) => void;
|
||||
time: SimpleTimeOnly;
|
||||
}> = ({ setChosenTime, time }) => {
|
||||
const adjustedHour = time.hour % 12 === 0 ? 12 : time.hour % 12;
|
||||
const partOfDay = time.hour < 12 ? "AM" : "PM";
|
||||
|
||||
// useEffect(() => {
|
||||
// const newtime = {
|
||||
// hour: partOfDay === "PM" ? hour + 12 : hour,
|
||||
// minute: minute,
|
||||
// };
|
||||
// if (
|
||||
// newtime.hour != startingTime.hour ||
|
||||
// newtime.minute != startingTime.minute
|
||||
// ) {
|
||||
// setChosenTime(newtime);
|
||||
// }
|
||||
// }, [
|
||||
// hour,
|
||||
// minute,
|
||||
// partOfDay,
|
||||
// setChosenTime,
|
||||
// startingTime.hour,
|
||||
// startingTime.minute,
|
||||
// ]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<form
|
||||
onSubmit={(e) => e.preventDefault()}
|
||||
className="flex flex-row gap-3"
|
||||
>
|
||||
<div className="">
|
||||
<label>
|
||||
Hour
|
||||
<select
|
||||
value={adjustedHour}
|
||||
onChange={(e) => {
|
||||
const newHours = parseInt(e.target.value);
|
||||
setChosenTime({
|
||||
...time,
|
||||
hour: partOfDay === "PM" ? newHours + 12 : newHours,
|
||||
});
|
||||
}}
|
||||
>
|
||||
{Array.from({ length: 12 }, (_, i) => i).map((o) => (
|
||||
<option key={o.toString()}>{o}</option>
|
||||
))}
|
||||
</select>
|
||||
</label>
|
||||
</div>
|
||||
<div className="">
|
||||
<label>
|
||||
Minute
|
||||
<select
|
||||
value={time.minute}
|
||||
onChange={(e) => {
|
||||
const newMinute = parseInt(e.target.value);
|
||||
setChosenTime({
|
||||
...time,
|
||||
minute: newMinute,
|
||||
});
|
||||
}}
|
||||
>
|
||||
{[0, 15, 30, 45, 59].map((o) => (
|
||||
<option key={o.toString()}>{o}</option>
|
||||
))}
|
||||
</select>
|
||||
</label>
|
||||
</div>
|
||||
<div className="">
|
||||
<label>
|
||||
Part of Day
|
||||
<select
|
||||
value={partOfDay}
|
||||
onChange={(e) => {
|
||||
const newPartOfDay = e.target.value;
|
||||
|
||||
setChosenTime({
|
||||
...time,
|
||||
hour: newPartOfDay === "PM" ? time.hour + 12 : time.hour,
|
||||
});
|
||||
}}
|
||||
>
|
||||
{["AM", "PM"].map((o) => (
|
||||
<option key={o.toString()}>{o}</option>
|
||||
))}
|
||||
</select>
|
||||
</label>
|
||||
</div>
|
||||
</form>
|
||||
</>
|
||||
);
|
||||
};
|
||||
60
src/components/editor/InnerMonacoEditor.tsx
Normal file
60
src/components/editor/InnerMonacoEditor.tsx
Normal file
@@ -0,0 +1,60 @@
|
||||
"use client";
|
||||
import React, { useRef, useEffect } from "react";
|
||||
import loader from "@monaco-editor/loader";
|
||||
import { editor } from "monaco-editor/esm/vs/editor/editor.api";
|
||||
|
||||
|
||||
export default function InnerMonacoEditor({
|
||||
value,
|
||||
onChange,
|
||||
}: {
|
||||
value: string;
|
||||
onChange: (value: string) => void; // must be memoized
|
||||
}) {
|
||||
const editorRef = useRef<editor.IStandaloneCodeEditor | null>(null);
|
||||
const divRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (divRef.current && !editorRef.current) {
|
||||
loader.init().then((monaco) => {
|
||||
console.log("in init", monaco, divRef.current, editorRef.current);
|
||||
if (divRef.current && !editorRef.current) {
|
||||
const properties: editor.IStandaloneEditorConstructionOptions = {
|
||||
value: value,
|
||||
language: "markdown",
|
||||
tabSize: 3,
|
||||
theme: "vs-dark",
|
||||
minimap: {
|
||||
enabled: false,
|
||||
},
|
||||
lineNumbers: "off",
|
||||
wordWrap: "on",
|
||||
automaticLayout: true,
|
||||
fontFamily: "Roboto-mono",
|
||||
fontSize: 16,
|
||||
padding: {
|
||||
top: 10,
|
||||
},
|
||||
};
|
||||
|
||||
editorRef.current = monaco.editor.create(divRef.current, properties);
|
||||
editorRef.current.onDidChangeModelContent((e) => {
|
||||
console.log("in on change", onChange);
|
||||
onChange(editorRef.current?.getModel()?.getValue() ?? "");
|
||||
});
|
||||
} else {
|
||||
console.log("second render of init");
|
||||
}
|
||||
});
|
||||
} else if (!divRef.current) {
|
||||
}
|
||||
}, [onChange, value]);
|
||||
|
||||
return (
|
||||
<div
|
||||
className="Editor"
|
||||
ref={divRef}
|
||||
style={{ height: "100%", overflow: "hidden" }}
|
||||
></div>
|
||||
);
|
||||
}
|
||||
49
src/components/editor/InnerMonacoEditorOther.tsx
Normal file
49
src/components/editor/InnerMonacoEditorOther.tsx
Normal file
@@ -0,0 +1,49 @@
|
||||
"use client";
|
||||
import React, { useRef } from "react";
|
||||
import { editor } from "monaco-editor/esm/vs/editor/editor.api";
|
||||
|
||||
import Editor from "@monaco-editor/react";
|
||||
|
||||
export default function InnerMonacoEditorOther({
|
||||
value,
|
||||
onChange,
|
||||
}: {
|
||||
value: string;
|
||||
onChange: (value: string) => void; // must be memoized
|
||||
}) {
|
||||
const editorRef = useRef<editor.IStandaloneCodeEditor | null>(null);
|
||||
|
||||
function handleEditorDidMount(editor: editor.IStandaloneCodeEditor) {
|
||||
editorRef.current = editor;
|
||||
editor.onDidChangeModelContent((e) => {
|
||||
onChange(editorRef.current?.getModel()?.getValue() ?? "");
|
||||
});
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Editor
|
||||
height="100%"
|
||||
options={{
|
||||
value: value,
|
||||
tabSize: 3,
|
||||
minimap: {
|
||||
enabled: false,
|
||||
},
|
||||
lineNumbers: "off",
|
||||
wordWrap: "on",
|
||||
automaticLayout: true,
|
||||
fontFamily: "Roboto-mono",
|
||||
fontSize: 16,
|
||||
padding: {
|
||||
top: 10,
|
||||
},
|
||||
}}
|
||||
defaultLanguage="markdown"
|
||||
theme="vs-dark"
|
||||
defaultValue={value}
|
||||
onMount={handleEditorDidMount}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
15
src/components/editor/MonacoEditor.css
Normal file
15
src/components/editor/MonacoEditor.css
Normal file
@@ -0,0 +1,15 @@
|
||||
|
||||
|
||||
/* monaco editor */
|
||||
.monaco-editor-background,
|
||||
.monaco-editor .margin {
|
||||
background-color: #020617 !important;
|
||||
/* background-color: #18181b !important; */
|
||||
}
|
||||
.monaco-editor {
|
||||
position: absolute !important;
|
||||
}
|
||||
|
||||
.monaco-editor .mtk1 {
|
||||
@apply text-slate-300;
|
||||
}
|
||||
19
src/components/editor/MonacoEditor.tsx
Executable file
19
src/components/editor/MonacoEditor.tsx
Executable file
@@ -0,0 +1,19 @@
|
||||
"use client";
|
||||
import dynamic from "next/dynamic";
|
||||
import { useEffect, useState } from "react";
|
||||
import "./MonacoEditor.css";
|
||||
|
||||
const InnerMonacoEditor = dynamic(() => import("./InnerMonacoEditorOther"), {
|
||||
ssr: false,
|
||||
});
|
||||
|
||||
export const MonacoEditor: React.FC<{
|
||||
value: string;
|
||||
onChange: (value: string) => void;
|
||||
}> = ({ value, onChange }) => {
|
||||
const [salt, setSalt] = useState(Date.now());
|
||||
useEffect(() => {
|
||||
setSalt(Date.now());
|
||||
}, [onChange]);
|
||||
return <InnerMonacoEditor key={salt} value={value} onChange={onChange} />;
|
||||
};
|
||||
27
src/components/form/DayOfWeekInput.tsx
Normal file
27
src/components/form/DayOfWeekInput.tsx
Normal file
@@ -0,0 +1,27 @@
|
||||
import { DayOfWeek } from "@/models/local/localCourseSettings";
|
||||
|
||||
export function DayOfWeekInput({
|
||||
selectedDays,
|
||||
updateSettings,
|
||||
}: {
|
||||
selectedDays: DayOfWeek[];
|
||||
updateSettings: (day: DayOfWeek) => void;
|
||||
}) {
|
||||
return (
|
||||
<div className="flex flex-row gap-3">
|
||||
{Object.values(DayOfWeek).map((day) => {
|
||||
const hasDay = selectedDays.includes(day);
|
||||
return (
|
||||
<button
|
||||
role="button"
|
||||
key={day}
|
||||
className={hasDay ? "" : "unstyled btn-outline "}
|
||||
onClick={() => updateSettings(day)}
|
||||
>
|
||||
{day}
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
37
src/components/form/SelectInput.tsx
Normal file
37
src/components/form/SelectInput.tsx
Normal file
@@ -0,0 +1,37 @@
|
||||
export default function SelectInput<T>({
|
||||
value,
|
||||
setValue,
|
||||
label,
|
||||
options,
|
||||
getOptionName,
|
||||
emptyOptionText,
|
||||
}: {
|
||||
value: T | undefined;
|
||||
setValue: (newValue: T | undefined) => void;
|
||||
label: string;
|
||||
options: T[];
|
||||
getOptionName: (item: T) => string;
|
||||
emptyOptionText?: string;
|
||||
}) {
|
||||
return (
|
||||
<label className="block">
|
||||
{label}
|
||||
<br />
|
||||
<select
|
||||
className="bg-slate-800 rounded-md px-1"
|
||||
value={value ? getOptionName(value) : ""}
|
||||
onChange={(e) => {
|
||||
const optionName = e.target.value;
|
||||
const option = options.find((o) => getOptionName(o) === optionName);
|
||||
setValue(option);
|
||||
}}
|
||||
>
|
||||
<option></option>
|
||||
{emptyOptionText && <option>{emptyOptionText}</option>}
|
||||
{options.map((o) => (
|
||||
<option key={getOptionName(o)}>{getOptionName(o)}</option>
|
||||
))}
|
||||
</select>
|
||||
</label>
|
||||
);
|
||||
}
|
||||
36
src/components/form/TextInput.tsx
Normal file
36
src/components/form/TextInput.tsx
Normal file
@@ -0,0 +1,36 @@
|
||||
import React from "react";
|
||||
|
||||
export default function TextInput({
|
||||
value,
|
||||
setValue,
|
||||
label,
|
||||
className,
|
||||
isTextArea = false,
|
||||
}: {
|
||||
value: string;
|
||||
setValue: (newValue: string) => void;
|
||||
label: string;
|
||||
className?: string;
|
||||
isTextArea?: boolean;
|
||||
}) {
|
||||
return (
|
||||
<label className={"flex flex-col " + className}>
|
||||
{label}
|
||||
<br />
|
||||
{!isTextArea && (
|
||||
<input
|
||||
className="bg-slate-800 border border-slate-500 rounded-md w-full px-1"
|
||||
value={value}
|
||||
onChange={(e) => setValue(e.target.value)}
|
||||
/>
|
||||
)}
|
||||
{isTextArea && (
|
||||
<textarea
|
||||
className="bg-slate-800 border border-slate-500 rounded-md w-full px-1 flex-grow"
|
||||
value={value}
|
||||
onChange={(e) => setValue(e.target.value)}
|
||||
/>
|
||||
)}
|
||||
</label>
|
||||
);
|
||||
}
|
||||
15
src/components/icons/CheckIcon.tsx
Normal file
15
src/components/icons/CheckIcon.tsx
Normal file
@@ -0,0 +1,15 @@
|
||||
import React from "react";
|
||||
|
||||
export default function CheckIcon() {
|
||||
return (
|
||||
<svg className="h-6" viewBox="0 0 24 24" fill="none">
|
||||
<path
|
||||
d="M4 12.6111L8.92308 17.5L20 6.5"
|
||||
stroke="green"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
28
src/components/icons/ExpandIcon.tsx
Normal file
28
src/components/icons/ExpandIcon.tsx
Normal file
@@ -0,0 +1,28 @@
|
||||
import React from "react";
|
||||
|
||||
export default function ExpandIcon({style}: {
|
||||
style?: React.CSSProperties | undefined;
|
||||
}) {
|
||||
const size = "24px";
|
||||
return (
|
||||
<svg
|
||||
style={style}
|
||||
|
||||
width={size}
|
||||
height={size}
|
||||
viewBox="0 0 17 17"
|
||||
version="1.1"
|
||||
className="si-glyph si-glyph-triangle-left transition-all ms-1"
|
||||
>
|
||||
<g stroke="none" strokeWidth="1" fill="none" fillRule="evenodd">
|
||||
<path
|
||||
d="M3.446,10.052 C2.866,9.471 2.866,8.53 3.446,7.948 L9.89,1.506 C10.471,0.924 11.993,0.667 11.993,2.506 L11.993,15.494 C11.993,17.395 10.472,17.076 9.89,16.495 L3.446,10.052 L3.446,10.052 Z"
|
||||
className="si-glyph-fill"
|
||||
style={{
|
||||
fill: "rgb(148 163 184 / var(--tw-text-opacity))",
|
||||
}}
|
||||
></path>
|
||||
</g>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
137
src/components/realtime/ClientCacheInvalidation.tsx
Normal file
137
src/components/realtime/ClientCacheInvalidation.tsx
Normal file
@@ -0,0 +1,137 @@
|
||||
"use client";
|
||||
|
||||
import { trpc } from "@/services/serverFunctions/trpcClient";
|
||||
import React, { useCallback, useEffect, useState } from "react";
|
||||
import { io, Socket } from "socket.io-client";
|
||||
|
||||
interface ServerToClientEvents {
|
||||
message: (data: string) => void;
|
||||
fileChanged: (filePath: string) => void;
|
||||
}
|
||||
|
||||
interface ClientToServerEvents {
|
||||
sendMessage: (data: string) => void;
|
||||
}
|
||||
|
||||
const socket: Socket<ServerToClientEvents, ClientToServerEvents> = io("/");
|
||||
|
||||
function removeFileExtension(fileName: string): string {
|
||||
const lastDotIndex = fileName.lastIndexOf(".");
|
||||
if (lastDotIndex > 0) {
|
||||
return fileName.substring(0, lastDotIndex);
|
||||
}
|
||||
return fileName;
|
||||
}
|
||||
|
||||
export function ClientCacheInvalidation() {
|
||||
const invalidateCache = useFilePathInvalidation();
|
||||
const [connectionAttempted, setConnectionAttempted] = useState(false);
|
||||
useEffect(() => {
|
||||
if (!connectionAttempted) {
|
||||
socket.connect();
|
||||
setConnectionAttempted(true);
|
||||
}
|
||||
|
||||
socket.on("connect", () => {
|
||||
console.log("Socket connected successfully.");
|
||||
});
|
||||
|
||||
socket.on("message", (data) => {
|
||||
console.log("Received message:", data);
|
||||
});
|
||||
|
||||
socket.on("fileChanged", invalidateCache);
|
||||
|
||||
socket.on("connect_error", (error) => {
|
||||
console.error("Connection error:", error);
|
||||
console.error("File system real time updates disabled");
|
||||
socket.disconnect();
|
||||
});
|
||||
|
||||
return () => {
|
||||
socket.off("message");
|
||||
socket.off("fileChanged");
|
||||
socket.off("connect_error");
|
||||
};
|
||||
}, [connectionAttempted, invalidateCache]);
|
||||
|
||||
return <></>;
|
||||
}
|
||||
|
||||
const useFilePathInvalidation = () => {
|
||||
const utils = trpc.useUtils();
|
||||
return useCallback(
|
||||
(filePath: string) => {
|
||||
const [courseName, moduleOrLectures, itemType, itemFile] =
|
||||
filePath.split("/");
|
||||
|
||||
const itemName = itemFile ? removeFileExtension(itemFile) : undefined;
|
||||
const allParts = [courseName, moduleOrLectures, itemType, itemName];
|
||||
|
||||
if (moduleOrLectures === "settings.yml") {
|
||||
utils.settings.allCoursesSettings.invalidate();
|
||||
utils.settings.courseSettings.invalidate({ courseName });
|
||||
return;
|
||||
}
|
||||
|
||||
if (moduleOrLectures === "00 - lectures") {
|
||||
console.log("lecture updated on FS ", allParts);
|
||||
utils.lectures.getLectures.invalidate({ courseName });
|
||||
return;
|
||||
}
|
||||
|
||||
if (itemType === "assignments") {
|
||||
console.log("assignment updated on FS ", allParts);
|
||||
utils.assignment.getAllAssignments.invalidate({
|
||||
courseName,
|
||||
moduleName: moduleOrLectures,
|
||||
});
|
||||
utils.assignment.getAssignment.invalidate({
|
||||
courseName,
|
||||
moduleName: moduleOrLectures,
|
||||
assignmentName: itemName,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (itemType === "quizzes") {
|
||||
console.log("quiz updated on FS ", allParts);
|
||||
utils.quiz.getAllQuizzes.invalidate({
|
||||
courseName,
|
||||
moduleName: moduleOrLectures,
|
||||
});
|
||||
utils.quiz.getQuiz.invalidate({
|
||||
courseName,
|
||||
moduleName: moduleOrLectures,
|
||||
quizName: itemName,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (itemType === "pages") {
|
||||
console.log("page updated on FS ", allParts);
|
||||
utils.page.getAllPages.invalidate({
|
||||
courseName,
|
||||
moduleName: moduleOrLectures,
|
||||
});
|
||||
utils.page.getPage.invalidate({
|
||||
courseName,
|
||||
moduleName: moduleOrLectures,
|
||||
pageName: itemName,
|
||||
});
|
||||
return;
|
||||
}
|
||||
},
|
||||
[
|
||||
utils.assignment.getAllAssignments,
|
||||
utils.assignment.getAssignment,
|
||||
utils.lectures.getLectures,
|
||||
utils.page.getAllPages,
|
||||
utils.page.getPage,
|
||||
utils.quiz.getAllQuizzes,
|
||||
utils.quiz.getQuiz,
|
||||
utils.settings.allCoursesSettings,
|
||||
utils.settings.courseSettings,
|
||||
]
|
||||
);
|
||||
};
|
||||
69
src/components/spinner.css
Normal file
69
src/components/spinner.css
Normal file
@@ -0,0 +1,69 @@
|
||||
:root {
|
||||
/* --spinner-size-1: 48px;
|
||||
--spinner-size-2: 32px; */
|
||||
--spinner-size-1: 33px;
|
||||
--spinner-size-2: 22px;
|
||||
/* --spinner-size-3: 40px; */
|
||||
|
||||
--spinner-color-1: #305576;
|
||||
--spinner-color-2: #714199;
|
||||
}
|
||||
|
||||
.loader {
|
||||
width: var(--spinner-size-1);
|
||||
height: var(--spinner-size-1);
|
||||
border-radius: 50%;
|
||||
display: inline-block;
|
||||
position: relative;
|
||||
border: 3px solid;
|
||||
border-color: var(--spinner-color-1) var(--spinner-color-1) transparent
|
||||
transparent;
|
||||
box-sizing: border-box;
|
||||
animation: rotation 2s linear infinite;
|
||||
}
|
||||
.loader::after,
|
||||
.loader::before {
|
||||
content: "";
|
||||
box-sizing: border-box;
|
||||
position: absolute;
|
||||
left: 0;
|
||||
right: 0;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
margin: auto;
|
||||
border: 3px solid;
|
||||
border-color: transparent transparent var(--spinner-color-2)
|
||||
var(--spinner-color-2);
|
||||
/* width: var(--spinner-size-3);
|
||||
height: var(--spinner-size-3); */
|
||||
border-radius: 50%;
|
||||
box-sizing: border-box;
|
||||
animation: rotationBack 1s linear infinite;
|
||||
transform-origin: center center;
|
||||
}
|
||||
/* var(--spinner-color-2) */
|
||||
/* #3a0647 */
|
||||
.loader::before {
|
||||
width: var(--spinner-size-2);
|
||||
height: var(--spinner-size-2);
|
||||
border-color: var(--spinner-color-1) var(--spinner-color-1) transparent
|
||||
transparent;
|
||||
animation: rotation 3s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes rotation {
|
||||
0% {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
100% {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
@keyframes rotationBack {
|
||||
0% {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
100% {
|
||||
transform: rotate(-360deg);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user