moving v2 to top level

This commit is contained in:
2024-12-17 09:19:21 -07:00
parent 5f0b3554dc
commit 576ee02afb
468 changed files with 79 additions and 15430 deletions

View 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>
);
}

View 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}</>;
}

View 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
View 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>
</>
);
}

View 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>
);
};

View 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>
);
};

View 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>
</>
);
};

View 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>
);
}

View 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}
/>
</>
);
}

View 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;
}

View 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} />;
};

View 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>
);
}

View 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>
);
}

View 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>
);
}

View 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>
);
}

View 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>
);
}

View 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,
]
);
};

View 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);
}
}