3 Commits

Author SHA1 Message Date
copilot-swe-agent[bot]
5359c7a4be Implement syntax highlighting for markdown code blocks using Prism.js
Co-authored-by: alexmickelson <43245625+alexmickelson@users.noreply.github.com>
2025-07-18 15:48:05 +00:00
copilot-swe-agent[bot]
6db2c5b66b Initial exploration and planning for syntax highlighting
Co-authored-by: alexmickelson <43245625+alexmickelson@users.noreply.github.com>
2025-07-18 15:40:53 +00:00
copilot-swe-agent[bot]
4cdafb5ffc Initial plan 2025-07-18 15:32:08 +00:00
279 changed files with 13237 additions and 6557 deletions

View File

@@ -1,27 +0,0 @@
name: Deploy to Docker Hub
on:
push:
branches: [ main ]
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v3
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2
- name: Log in to Docker Hub
uses: docker/login-action@v2
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Build and push Docker image
run: |
chmod +x ./build.sh
./build.sh -t -p

1
.gitignore vendored
View File

@@ -44,4 +44,3 @@ next-env.d.ts
storage/ storage/
temp/ temp/
.claude/

View File

@@ -4,40 +4,12 @@
<https://nowucca.com/2020/07/04/working-around-canvas-limitations.html> <https://nowucca.com/2020/07/04/working-around-canvas-limitations.html>
## Getting Started and Usage (v3) ## Getting Started and Usage
All class data files are stored in markdown files in a folder. I recommend making this folder a git repo. Here's an example docker compose: <!-- draft -->
```yml
services:
canvas_manager:
image: alexmickelson/canvas_management:3
user: "1000:1000"
container_name: canvas-manager
ports:
- 3000:3000
env_file:
- .env
environment:
- storageDirectory=/app/storage
- TZ=America/Denver
- NEXT_PUBLIC_ENABLE_FILE_SYNC=true
volumes:
- ./globalSettings.yml:/app/globalSettings.yml
- ~/projects/faculty:/app/storage
- ~/projects/facultyFiles:/app/public/images/facultyFiles
```
The `globalSettings.yml` file specifies which folders in your storage directory you want to display in the UI. This way you can have old classes files stored, but not bring them into the UI unless you need to (like when you are planning a new semester, you might want to see the old semester).
`globalSettings.yml` can start like this, this file will be edited as you add classes to manage.
```yml
courses: []
```
## Enable Image Support ### Enable Image Support
You must set the `NEXT_PUBLIC_ENABLE_FILE_SYNC` environment variable to true. Images need to be available in the `/app/public/` directory in the container so that nextjs will serve them as static files. Images can also be set to public URL's on the web. You must set the `NEXT_PUBLIC_ENABLE_FILE_SYNC` environment variable to true. Images need to be available in the `/app/public/` directory in the container so that nextjs will serve them as static files. Images can also be set to public URL's on the web.

View File

@@ -1,7 +1,7 @@
#!/bin/bash #!/bin/bash
MAJOR_VERSION="3" MAJOR_VERSION="2"
MINOR_VERSION="0" MINOR_VERSION="8"
VERSION="$MAJOR_VERSION.$MINOR_VERSION" VERSION="$MAJOR_VERSION.$MINOR_VERSION"
TAG_FLAG=false TAG_FLAG=false
@@ -44,9 +44,9 @@ if [ "$PUSH_FLAG" = true ]; then
echo "alexmickelson/canvas_management:$MAJOR_VERSION" echo "alexmickelson/canvas_management:$MAJOR_VERSION"
echo "alexmickelson/canvas_management:latest" echo "alexmickelson/canvas_management:latest"
docker push -q alexmickelson/canvas_management:"$VERSION" docker push alexmickelson/canvas_management:"$VERSION"
docker push -q alexmickelson/canvas_management:"$MAJOR_VERSION" docker push alexmickelson/canvas_management:"$MAJOR_VERSION"
docker push -q alexmickelson/canvas_management:latest docker push alexmickelson/canvas_management:latest
fi fi
if [ "$TAG_FLAG" = false ] && [ "$PUSH_FLAG" = false ]; then if [ "$TAG_FLAG" = false ] && [ "$PUSH_FLAG" = false ]; then
@@ -59,7 +59,7 @@ if [ "$TAG_FLAG" = false ] && [ "$PUSH_FLAG" = false ]; then
echo "docker image tag canvas_management:$VERSION alexmickelson/canvas_management:$VERSION" echo "docker image tag canvas_management:$VERSION alexmickelson/canvas_management:$VERSION"
echo "docker image tag canvas_management:$VERSION alexmickelson/canvas_management:$MAJOR_VERSION" echo "docker image tag canvas_management:$VERSION alexmickelson/canvas_management:$MAJOR_VERSION"
echo "docker image tag canvas_management:latest alexmickelson/canvas_management:latest" echo "docker image tag canvas_management:latest alexmickelson/canvas_management:latest"
echo "docker push -q alexmickelson/canvas_management:$VERSION" echo "docker push alexmickelson/canvas_management:$VERSION"
echo "docker push -q alexmickelson/canvas_management:$MAJOR_VERSION" echo "docker push alexmickelson/canvas_management:$MAJOR_VERSION"
echo "docker push -q alexmickelson/canvas_management:latest" echo "docker push alexmickelson/canvas_management:latest"
fi fi

View File

@@ -15,20 +15,17 @@ services:
- NEXT_PUBLIC_ENABLE_FILE_SYNC=true - NEXT_PUBLIC_ENABLE_FILE_SYNC=true
- REDIS_URL=redis://redis:6379 - REDIS_URL=redis://redis:6379
volumes: volumes:
# - ./globalSettings.dev.yml:/app/globalSettings.yml
- ./globalSettings.yml:/app/globalSettings.yml
- .:/app - .:/app
- ~/projects/faculty:/app/storage - ~/projects/faculty/1810/2025-spring-alex/in-person:/app/storage/intro_to_web_old
# - ~/projects/faculty/1810/2025-spring-alex/in-person:/app/storage/intro_to_web_old - ~/projects/faculty/1810/2025-fall-alex/modules:/app/storage/intro_to_web
# - ~/projects/faculty/1810/2025-fall-alex/modules:/app/storage/intro_to_web - ~/projects/faculty/4850_AdvancedFE/2025-fall-alex/modules:/app/storage/advanced_frontend
# - ~/projects/faculty/4850_AdvancedFE/2025-fall-alex/modules:/app/storage/advanced_frontend - ~/projects/faculty/4850_AdvancedFE/2024-fall-alex/modules:/app/storage/advanced_frontend_old
# - ~/projects/faculty/4850_AdvancedFE/2024-fall-alex/modules:/app/storage/advanced_frontend_old - ~/projects/faculty/1430/2024-fall-alex/modules:/app/storage/ux_old
# - ~/projects/faculty/1430/2024-fall-alex/modules:/app/storage/ux_old - ~/projects/faculty/1430/2025-fall-alex/modules:/app/storage/ux
# - ~/projects/faculty/1430/2025-fall-alex/modules:/app/storage/ux - ~/projects/faculty/1420/2024-fall/Modules:/app/storage/1420_old
# - ~/projects/faculty/1420/2024-fall/Modules:/app/storage/1420_old - ~/projects/faculty/1420/2025-fall-alex/modules:/app/storage/1420
# - ~/projects/faculty/1420/2025-fall-alex/modules:/app/storage/1420 - ~/projects/faculty/1425/2024-fall/Modules:/app/storage/1425_old
# - ~/projects/faculty/1425/2024-fall/Modules:/app/storage/1425_old - ~/projects/faculty/1425/2025-fall-alex/modules:/app/storage/1425
# - ~/projects/faculty/1425/2025-fall-alex/modules:/app/storage/1425
- ~/projects/facultyFiles:/app/public/images/facultyFiles - ~/projects/facultyFiles:/app/public/images/facultyFiles
redis: redis:
@@ -50,7 +47,7 @@ services:
--api-key "$MCP_TOKEN" \ --api-key "$MCP_TOKEN" \
--server-type "streamable_http" \ --server-type "streamable_http" \
--cors-allow-origins "*" \ --cors-allow-origins "*" \
-- http://canvas-dev:3000/api/mcp/mcp/ -- http://canvas-dev:3000/api/mcp
' '
working_dir: /app working_dir: /app
ports: ports:

View File

@@ -1,8 +1,8 @@
services: services:
canvas_manager: canvas_manager:
image: alexmickelson/canvas_management:3 image: alexmickelson/canvas_management:2.7
user: "1000:1000" user: "1000:1000"
container_name: canvas-manager container_name: canvas-manager-2
ports: ports:
- 3000:3000 - 3000:3000
env_file: env_file:
@@ -14,20 +14,42 @@ services:
- REDIS_URL=redis://redis:6379 - REDIS_URL=redis://redis:6379
# - FILE_POLLING=true # - FILE_POLLING=true
volumes: volumes:
- ./globalSettings.yml:/app/globalSettings.yml # - ~/projects/faculty/3840_Telemetry/2024Spring_alex/modules:/app/storage/spring_2024_telemetry
- ~/projects/faculty:/app/storage
# - ~/projects/faculty/1400/2025_spring_alex/modules:/app/storage/1400
# - ~/projects/faculty/1405/2025_spring_alex:/app/storage/1405
# - ~/projects/faculty/3840_Telemetry/2025_spring_alex/modules:/app/storage/telemetry
# - ~/projects/faculty/4620_Distributed/2025Spring/modules:/app/storage/distributed
# - ~/projects/faculty/4620_Distributed/2024Spring/modules:/app/storage/distributed_old
- ~/projects/faculty/1810/2025-spring-alex/in-person:/app/storage/intro_to_web_old
- ~/projects/faculty/1810/2025-fall-alex/modules:/app/storage/intro_to_web
- ~/projects/faculty/4850_AdvancedFE/2025-fall-alex/modules:/app/storage/advanced_frontend
- ~/projects/faculty/4850_AdvancedFE/2024-fall-alex/modules:/app/storage/advanced_frontend_old
- ~/projects/faculty/1430/2024-fall-alex/modules:/app/storage/ux_old
- ~/projects/faculty/1430/2025-fall-alex/modules:/app/storage/ux
- ~/projects/faculty/1420/2024-fall/Modules:/app/storage/1420_old
- ~/projects/faculty/1420/2025-fall-alex/modules:/app/storage/1420
- ~/projects/faculty/1425/2024-fall/Modules:/app/storage/1425_old
- ~/projects/faculty/1425/2025-fall-alex/modules:/app/storage/1425
- ~/projects/facultyFiles:/app/public/images/facultyFiles - ~/projects/facultyFiles:/app/public/images/facultyFiles
# redis: redis:
# image: redis image: redis
# container_name: redis container_name: redis
# volumes: volumes:
# - redis-data:/data - redis-data:/data
# restart: unless-stopped restart: unless-stopped
# volumes: volumes:
# redis-data: redis-data:
# https://developers.cloudflare.com/cloudflare-one/connections/connect-networks/ # https://developers.cloudflare.com/cloudflare-one/connections/connect-networks/
# https://github.com/jonas-merkle/container-cloudflare-tunnel # https://github.com/jonas-merkle/container-cloudflare-tunnel

View File

@@ -10,11 +10,13 @@ const compat = new FlatCompat({
}); });
const eslintConfig = [ const eslintConfig = [
{
ignores: ["**/node_modules/**", "**/.next/**", "storage/**"],
},
...compat.config({ ...compat.config({
extends: ["next/core-web-vitals", "next/typescript", "prettier"], extends: ["next/core-web-vitals", "next/typescript", "prettier"],
ignores: [
"**/node_modules/**",
"**/.next/**",
"storage/**"
],
rules: { rules: {
"react-refresh/only-export-components": "off", // Disabled the rule "react-refresh/only-export-components": "off", // Disabled the rule
"@typescript-eslint/no-unused-vars": [ "@typescript-eslint/no-unused-vars": [

View File

@@ -1,5 +0,0 @@
courses:
- path: ./3820_BackEnd/2025-fall/Modules/
name: Back-End
- path: ./3820_BackEnd/2024-fall/Modules/
name: Back-End

View File

@@ -1,23 +0,0 @@
courses:
- path: ./1420/2025-fall-alex/modules/
name: "1420"
- path: ./1425/2025-fall-alex/modules/
name: "1425"
- path: ./4850_AdvancedFE/2026-spring-alex/modules
name: Adv Frontend
- path: ./1400/2026_spring_alex/modules
name: "1400"
- path: ./1405/2026_spring_alex
name: "1405"
- path: ./3840_Telemetry/2026_spring_alex
name: Telem and Ops
- path: ./4620_Distributed/2026-spring-alex/modules
name: Distributed
- path: ./4620_Distributed/2025Spring/modules/
name: distributed-old
- path: ./3840_Telemetry/2025_spring_alex/modules/
name: telemetry-old
- path: ./4850_AdvancedFE/2025-fall-alex/modules/
name: adv-frontend-old
- path: ./1810/2026-spring-alex/modules/
name: Web Intro

10212
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -23,6 +23,7 @@
"@trpc/react-query": "11.4.3", "@trpc/react-query": "11.4.3",
"@trpc/server": "11.4.3", "@trpc/server": "11.4.3",
"@trpc/tanstack-react-query": "^11.4.3", "@trpc/tanstack-react-query": "^11.4.3",
"@types/prismjs": "^1.26.5",
"@types/ws": "^8.18.1", "@types/ws": "^8.18.1",
"@typescript-eslint/parser": "^8.37.0", "@typescript-eslint/parser": "^8.37.0",
"chokidar": "^4.0.3", "chokidar": "^4.0.3",
@@ -32,7 +33,7 @@
"marked-katex-extension": "^5.1.5", "marked-katex-extension": "^5.1.5",
"mcp-handler": "^1.0.0", "mcp-handler": "^1.0.0",
"next": "^15.3.5", "next": "^15.3.5",
"pako": "^2.1.0", "prismjs": "^1.30.0",
"react": "^19.1.0", "react": "^19.1.0",
"react-dom": "^19.1.0", "react-dom": "^19.1.0",
"socket.io": "^4.8.1", "socket.io": "^4.8.1",

8
pnpm-lock.yaml generated
View File

@@ -65,9 +65,6 @@ importers:
next: next:
specifier: ^15.3.5 specifier: ^15.3.5
version: 15.3.5(@babel/core@7.28.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) version: 15.3.5(@babel/core@7.28.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
pako:
specifier: ^2.1.0
version: 2.1.0
react: react:
specifier: ^19.1.0 specifier: ^19.1.0
version: 19.1.0 version: 19.1.0
@@ -2631,9 +2628,6 @@ packages:
resolution: {integrity: sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==} resolution: {integrity: sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==}
engines: {node: '>=10'} engines: {node: '>=10'}
pako@2.1.0:
resolution: {integrity: sha512-w+eufiZ1WuJYgPXbV/PO3NCMEc3xqylkKHzp8bxp1uW4qaSNQUkwmLLEc3kKsfz8lpV1F8Ht3U1Cm+9Srog2ug==}
parent-module@1.0.1: parent-module@1.0.1:
resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==} resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==}
engines: {node: '>=6'} engines: {node: '>=6'}
@@ -5859,8 +5853,6 @@ snapshots:
dependencies: dependencies:
p-limit: 3.1.0 p-limit: 3.1.0
pako@2.1.0: {}
parent-module@1.0.1: parent-module@1.0.1:
dependencies: dependencies:
callsites: 3.1.0 callsites: 3.1.0

View File

@@ -1,12 +0,0 @@
### doesn't work, there is a form request that does thi son the site, but this isn't it
POST https://snow.instructure.com/api/v1/courses/1154759/modules/3965250/reorder
Authorization: Bearer {{$dotenv CANVAS_TOKEN}}
Content-Type: application/json
{
"order": [
29273428, 29274455, 29272498, 29274867, 29272743, 29273425
]
}

View File

@@ -30,11 +30,6 @@ Content-Type: application/json
GET https://snow.instructure.com/api/v1/courses/958185/assignments GET https://snow.instructure.com/api/v1/courses/958185/assignments
Authorization: Bearer {{$dotenv CANVAS_TOKEN}} Authorization: Bearer {{$dotenv CANVAS_TOKEN}}
###
GET https://snow.instructure.com/api/v1/courses/1155293/quizzes/4366122/questions
Authorization: Bearer {{$dotenv CANVAS_TOKEN}}
### ###
POST https://snow.instructure.com/api/v1/courses/958185/quizzes/3358912/questions POST https://snow.instructure.com/api/v1/courses/958185/quizzes/3358912/questions
Authorization: Bearer {{$dotenv CANVAS_TOKEN}} Authorization: Bearer {{$dotenv CANVAS_TOKEN}}

View File

@@ -1,132 +1,41 @@
"use client"; "use client";
import { Toggle } from "@/components/form/Toggle"; import { useLocalCoursesSettingsQuery } from "@/hooks/localCourse/localCoursesHooks";
import { useLocalCoursesSettingsQuery } from "@/features/local/course/localCoursesHooks"; import { getDateKey, getTermName, groupByStartDate } from "@/models/local/utils/timeUtils";
import {
useGlobalSettingsQuery,
useUpdateGlobalSettingsMutation,
} from "@/features/local/globalSettings/globalSettingsHooks";
import {
getDateKey,
getTermName,
groupByStartDate,
} from "@/features/local/utils/timeUtils";
import { getCourseUrl } from "@/services/urlUtils"; import { getCourseUrl } from "@/services/urlUtils";
import Link from "next/link"; import Link from "next/link";
import { useState } from "react";
import Modal, { useModal } from "@/components/Modal";
import { Spinner } from "@/components/Spinner";
export default function CourseList() { export default function CourseList() {
const { data: allSettings } = useLocalCoursesSettingsQuery(); const { data: allSettings } = useLocalCoursesSettingsQuery();
const [isDeleting, setIsDeleting] = useState(false);
const coursesByStartDate = groupByStartDate(allSettings); const coursesByStartDate = groupByStartDate(allSettings);
const sortedDates = Object.keys(coursesByStartDate).sort(); const sortedDates = Object.keys(coursesByStartDate).sort();
return ( return (
<div> <div className="flex flex-row ">
<Toggle {sortedDates.map((startDate) => (
label={"Delete Mode"} <div
value={isDeleting} key={startDate}
onChange={(set) => setIsDeleting(set)} className=" border-4 border-slate-800 rounded p-3 m-3"
/>
<div className="flex flex-row ">
{sortedDates.map((startDate) => (
<div
key={startDate}
className=" border-4 border-slate-800 rounded p-3 m-3"
>
<div className="text-center">{getTermName(startDate)}</div>
{coursesByStartDate[getDateKey(startDate)].map((settings) => (
<CourseItem
key={settings.name}
courseName={settings.name}
isDeleting={isDeleting}
/>
))}
</div>
))}
</div>
</div>
);
}
function CourseItem({
courseName,
isDeleting,
}: {
courseName: string;
isDeleting: boolean;
}) {
const { data: globalSettings } = useGlobalSettingsQuery();
const updateSettingsMutation = useUpdateGlobalSettingsMutation();
const modal = useModal();
return (
<div className="flex items-center justify-start gap-2">
{isDeleting && (
<Modal
modalControl={modal}
buttonText="X"
buttonClass="
unstyled
text-red-200 hover:text-red-400
bg-red-950/50 hover:bg-red-950/70
transition-all hover:scale-110
mb-3
"
modalWidth="w-1/3"
> >
{({ closeModal }) => ( <div className="text-center">{getTermName(startDate)}</div>
<div> {coursesByStartDate[getDateKey(startDate)].map((settings) => (
<div className="text-center"> <div key={settings.name}>
Are you sure you want to remove {courseName} from global <Link
settings? href={getCourseUrl(settings.name)}
</div> shallow={true}
<br /> className="
<div className="flex justify-around gap-3"> font-bold text-xl block
<button transition-all hover:scale-105 hover:underline hover:text-slate-200
onClick={async () => { mb-3
await updateSettingsMutation.mutateAsync({ "
globalSettings: { >
...globalSettings, {settings.name}
courses: globalSettings.courses.filter( </Link>
(course) => course.name !== courseName
),
},
});
closeModal();
}}
disabled={updateSettingsMutation.isPending}
className="btn-danger"
>
Yes
</button>
<button
onClick={closeModal}
disabled={updateSettingsMutation.isPending}
>
No
</button>
</div>
{updateSettingsMutation.isPending && <Spinner />}
</div> </div>
)} ))}
</Modal> </div>
)} ))}
<Link
href={getCourseUrl(courseName)}
shallow={true}
prefetch={true}
className="
font-bold text-xl block
transition-all hover:scale-105 hover:underline hover:text-slate-200
mb-3
"
>
{courseName}
</Link>
</div> </div>
); );
} }

View File

@@ -1,80 +0,0 @@
import { fileStorageService } from "@/features/local/utils/fileStorageService";
import { trpcAppRouter } from "@/services/serverFunctions/appRouter";
import { createTrpcContext } from "@/services/serverFunctions/context";
import { dehydrate, HydrationBoundary } from "@tanstack/react-query";
import { createServerSideHelpers } from "@trpc/react-query/server";
import superjson from "superjson";
export default async function DataHydration({
children,
}: Readonly<{
children: React.ReactNode;
}>) {
console.log("starting hydration");
const trpcHelper = createServerSideHelpers({
router: trpcAppRouter,
ctx: createTrpcContext(),
transformer: superjson,
queryClientConfig: {
defaultOptions: {
queries: {
staleTime: Infinity,
},
},
},
});
const allSettings = await fileStorageService.settings.getAllCoursesSettings();
await Promise.all(
allSettings.map(async (settings) => {
const courseName = settings.name;
const moduleNames = await trpcHelper.module.getModuleNames.fetch({
courseName,
});
await Promise.all([
// assignments
...moduleNames.map(
async (moduleName) =>
await trpcHelper.assignment.getAllAssignments.prefetch({
courseName,
moduleName,
})
),
// quizzes
...moduleNames.map(
async (moduleName) =>
await trpcHelper.quiz.getAllQuizzes.prefetch({
courseName,
moduleName,
})
),
// pages
...moduleNames.map(
async (moduleName) =>
await trpcHelper.page.getAllPages.prefetch({
courseName,
moduleName,
})
),
]);
})
);
// lectures
await Promise.all(
allSettings.map(
async (settings) =>
await trpcHelper.lectures.getLectures.fetch({
courseName: settings.name,
})
)
);
const dehydratedState = dehydrate(trpcHelper.queryClient);
console.log("ran hydration");
return (
<HydrationBoundary state={dehydratedState}>{children}</HydrationBoundary>
);
}

View File

@@ -1,114 +0,0 @@
"use client";
import ClientOnly from "@/components/ClientOnly";
import { StoragePathSelector } from "@/components/form/StoragePathSelector";
import TextInput from "@/components/form/TextInput";
import { SuspenseAndErrorHandling } from "@/components/SuspenseAndErrorHandling";
import {
useGlobalSettingsQuery,
useUpdateGlobalSettingsMutation,
} from "@/features/local/globalSettings/globalSettingsHooks";
import { useDirectoryIsCourseQuery } from "@/features/local/utils/storageDirectoryHooks";
import { FC, useEffect, useRef, useState } from "react";
export const AddExistingCourseToGlobalSettings = () => {
const [showForm, setShowForm] = useState(false);
return (
<div>
<div className="flex justify-center">
<button className="" onClick={() => setShowForm((i) => !i)}>
{showForm ? "Hide Form" : "Import Existing Course"}
</button>
</div>
<div className={" collapsible " + (showForm && "expand")}>
<div className="border rounded-md p-3 m-3">
<SuspenseAndErrorHandling>
<ClientOnly>{showForm && <ExistingCourseForm />}</ClientOnly>
</SuspenseAndErrorHandling>
</div>
</div>
</div>
);
};
const ExistingCourseForm: FC<object> = () => {
const [path, setPath] = useState("./");
const [name, setName] = useState("");
const nameInputRef = useRef<HTMLInputElement>(null);
const directoryIsCourseQuery = useDirectoryIsCourseQuery(path);
const { data: globalSettings } = useGlobalSettingsQuery();
const updateSettingsMutation = useUpdateGlobalSettingsMutation();
// Focus name input when directory becomes a valid course
useEffect(() => {
console.log("Checking directory:", directoryIsCourseQuery.data);
if (directoryIsCourseQuery.data) {
console.log("Focusing name input");
nameInputRef.current?.focus();
}
}, [directoryIsCourseQuery.data]);
return (
<form
onSubmit={async (e) => {
e.preventDefault();
console.log(path);
await updateSettingsMutation.mutateAsync({
globalSettings: {
...globalSettings,
courses: [
...globalSettings.courses,
{
name,
path,
},
],
},
});
setName("");
setPath("./");
}}
className="min-w-3xl"
>
<h2>Add Existing Course</h2>
<div className="flex items-center mt-2 text-slate-500">
{directoryIsCourseQuery.isLoading ? (
<>
<span className="animate-spin mr-2"></span>
<span>Checking directory...</span>
</>
) : directoryIsCourseQuery.data ? (
<>
<span className="text-green-600 mr-2"></span>
<span>This is a valid course directory.</span>
</>
) : (
<>
<span className="text-red-600 mr-2"></span>
<span>Not a course directory.</span>
</>
)}
</div>
<StoragePathSelector
value={path}
setValue={setPath}
label={"Course Directory Path"}
/>
{directoryIsCourseQuery.data && (
<>
<TextInput
value={name}
setValue={setName}
label={"Display Name"}
inputRef={nameInputRef}
/>
<div className="text-center">
<button className="text-center mt-3">Save</button>
</div>
</>
)}
</form>
);
};

View File

@@ -1,79 +0,0 @@
export const githubClassroomUrlPrompt = `
## getting github classroom link
## Preparation
- Always ask for key information if not provided:
- "Which classroom would you like to create an assignment in?"
- "What would you like to name the assignment?"
## Step-by-Step Process
1. **Navigate to GitHub Classroom**
\`\`\`javascript
// Navigate to GitHub Classroom
await page.goto('https://classroom.github.com');
\`\`\`
2. **Select the Target Classroom**
\`\`\`javascript
// Click on the desired classroom
await page.getByRole('link', { name: 'your-classroom-name' }).click();
\`\`\`
3. **Click the "New assignment" button**
\`\`\`javascript
// Click New assignment
await page.getByRole('link', { name: 'New assignment' }).click();
\`\`\`
4. **Enter the assignment title**
\`\`\`javascript
// Fill in the assignment title
await page.getByRole('textbox', { name: 'Assignment title' }).fill('your-assignment-name');
\`\`\`
5. **Click "Continue" to move to the next step**
\`\`\`javascript
// Click Continue
await page.getByRole('button', { name: 'Continue' }).click();
\`\`\`
6. **Configure starter code (optional) and click "Continue" to proceed**
\`\`\`javascript
// Optional: Select a starter code repository if needed
// await page.getByRole('combobox', { name: 'Find a GitHub repository' }).fill('your-repo');
// Click Continue
await page.getByRole('button', { name: 'Continue' }).click();
\`\`\`
7. **Set up autograding (optional) and click "Create assignment"**
\`\`\`javascript
// Optional: Set up autograding tests if needed
// await page.getByRole('button', { name: 'Add test' }).click();
// Click Create assignment
await page.getByRole('button', { name: 'Create assignment' }).click();
\`\`\`
8. **Copy the assignment invitation link to share with students**
\`\`\`javascript
// Click Copy assignment invitation link
await page.getByRole('button', { name: 'Copy assignment invitation' }).first().click();
// The link is now in clipboard and ready to share with students
\`\`\`
## Tips and Best Practices
- Always verify with the user if specific settings are needed (like deadlines, visibility, etc.)
- Test the invitation link by opening it in another browser to ensure it works correctly
- Consider optional features like starter code and autograding tests for more complex assignments
- Remember to communicate the invitation link to students via email, LMS, or other channels
## Common Issues and Solutions
- If authentication is needed, prompt the user to authenticate in their browser
- If a classroom isn't visible, ensure the user has proper permissions
- For starter code issues, verify that the repository is properly set as a template
- If GitHub Classroom is slow, advise patience during high-traffic periods
`

View File

@@ -1,11 +1,10 @@
import { assignmentMarkdownSerializer } from "@/features/local/assignments/models/utils/assignmentMarkdownSerializer"; import { assignmentMarkdownSerializer } from "@/models/local/assignment/utils/assignmentMarkdownSerializer";
import { groupByStartDate } from "@/features/local/utils/timeUtils"; import { LocalCourseSettings } from "@/models/local/localCourseSettings";
import { groupByStartDate } from "@/models/local/utils/timeUtils";
import { fileStorageService } from "@/services/fileStorage/fileStorageService";
import { ResourceTemplate } from "@modelcontextprotocol/sdk/server/mcp.js";
import { createMcpHandler } from "mcp-handler"; import { createMcpHandler } from "mcp-handler";
import { z } from "zod"; import { z } from "zod";
import { githubClassroomUrlPrompt } from "./github-classroom-prompt";
import { courseItemFileStorageService } from "@/features/local/course/courseItemFileStorageService";
import { fileStorageService } from "@/features/local/utils/fileStorageService";
import { getModuleNamesFromFiles } from "@/features/local/modules/moduleRouter";
const handler = createMcpHandler( const handler = createMcpHandler(
(server) => { (server) => {
@@ -43,17 +42,17 @@ const handler = createMcpHandler(
courseName: z.string(), courseName: z.string(),
}, },
async ({ courseName }) => { async ({ courseName }) => {
const modules = await getModuleNamesFromFiles( const modules = await fileStorageService.modules.getModuleNames(
courseName courseName
); );
const assignments = ( const assignments = (
await Promise.all( await Promise.all(
modules.map(async (moduleName) => { modules.map(async (moduleName) => {
const assignments = await courseItemFileStorageService.getItems({ const assignments =
courseName, await fileStorageService.assignments.getAssignments(
moduleName, courseName,
type: "Assignment", moduleName
}); );
return assignments.map((assignment) => ({ return assignments.map((assignment) => ({
assignmentName: assignment.name, assignmentName: assignment.name,
moduleName, moduleName,
@@ -102,12 +101,11 @@ const handler = createMcpHandler(
"courseName, moduleName, and assignmentName must be strings" "courseName, moduleName, and assignmentName must be strings"
); );
} }
const assignment = await courseItemFileStorageService.getItem({ const assignment = await fileStorageService.assignments.getAssignment(
courseName, courseName,
moduleName, moduleName,
name: assignmentName, assignmentName
type: "Assignment", );
});
console.log("mcp assignment", assignment); console.log("mcp assignment", assignment);
return { return {
@@ -120,58 +118,42 @@ const handler = createMcpHandler(
}; };
} }
); );
server.tool(
"get_github_classroom_url_instructions", server.registerResource(
"gets instructions for creating a GitHub Classroom assignment, call this to get a prompt showing how to create a GitHub Classroom assignment", "course_assignment",
{}, new ResourceTemplate(
async () => { "canvas:///courses/{courseName}/module/{moduleName}/assignments/{assignmentName}",
{ list: undefined }
),
{
title: "Course Assignment",
description: "Markdown representation of a course assignment",
},
async (uri, { courseName, moduleName, assignmentName }) => {
if (
typeof courseName !== "string" ||
typeof moduleName !== "string" ||
typeof assignmentName !== "string"
) {
throw new Error(
"courseName, moduleName, and assignmentName must be strings"
);
}
const assignment = await fileStorageService.assignments.getAssignment(
courseName,
moduleName,
assignmentName
);
return { return {
content: [ contents: [
{ {
type: "text", uri: uri.href,
text: githubClassroomUrlPrompt, text: assignment.description,
}, },
], ],
}; };
} }
); );
// resources dont integrate well right now
// server.registerResource(
// "course_assignment",
// new ResourceTemplate(
// "canvas:///courses/{courseName}/module/{moduleName}/assignments/{assignmentName}",
// { list: undefined }
// ),
// {
// title: "Course Assignment",
// description: "Markdown representation of a course assignment",
// },
// async (uri, { courseName, moduleName, assignmentName }) => {
// if (
// typeof courseName !== "string" ||
// typeof moduleName !== "string" ||
// typeof assignmentName !== "string"
// ) {
// throw new Error(
// "courseName, moduleName, and assignmentName must be strings"
// );
// }
// const assignment = await fileStorageService.assignments.getAssignment(
// courseName,
// moduleName,
// assignmentName
// );
// return {
// contents: [
// {
// uri: uri.href,
// text: assignment.description,
// },
// ],
// };
// }
// );
}, },
{ {
serverInfo: { serverInfo: {

View File

@@ -1,8 +1,10 @@
import { createTrpcContext } from "@/services/serverFunctions/context"; import { createTrpcContext } from "@/services/serverFunctions/context";
import { trpcAppRouter } from "@/services/serverFunctions/appRouter"; import { trpcAppRouter } from "@/services/serverFunctions/router/app";
import { fetchRequestHandler } from "@trpc/server/adapters/fetch"; import { fetchRequestHandler } from "@trpc/server/adapters/fetch";
const handler = async (request: Request) => { const handler = async (request: Request) => {
// await new Promise(r => setTimeout(r, 1000)); // delay for testing
return fetchRequestHandler({ return fetchRequestHandler({
endpoint: "/api/trpc", endpoint: "/api/trpc",
req: request, req: request,

View File

@@ -1,63 +1,25 @@
"use client"; "use client";
import { useState, useEffect } from "react"; import { useState } from "react";
import CourseSettingsLink from "./CourseSettingsLink"; import CourseSettingsLink from "./CourseSettingsLink";
import ModuleList from "./modules/ModuleList"; import ModuleList from "./modules/ModuleList";
import LeftChevron from "@/components/icons/LeftChevron"; import LeftChevron from "@/components/icons/LeftChevron";
import RightChevron from "@/components/icons/RightChevron"; import RightChevron from "@/components/icons/RightChevron";
const collapseThreshold = 1400;
export default function CollapsableSidebar() { export default function CollapsableSidebar() {
const [windowCollapseRecommended, setWindowCollapseRecommended] = const [isCollapsed, setIsCollapsed] = useState(false);
useState(false);
const [userCollapsed, setUserCollapsed] = useState<
"unset" | "collapsed" | "uncollapsed"
>("unset");
useEffect(() => {
// Initialize on mount
setWindowCollapseRecommended(window.innerWidth <= collapseThreshold);
function handleResize() {
if (window.innerWidth <= collapseThreshold) {
setWindowCollapseRecommended(true);
} else {
setWindowCollapseRecommended(false);
}
}
window.addEventListener("resize", handleResize);
return () => window.removeEventListener("resize", handleResize);
}, []);
let collapsed;
if (userCollapsed === "unset") {
collapsed = windowCollapseRecommended;
} else {
collapsed = userCollapsed === "collapsed";
}
const widthClass = collapsed ? "w-0" : "w-96";
const visibilityClass = collapsed ? "invisible " : "visible";
const widthClass = isCollapsed ? "w-0" : "w-96";
const visibilityClass = isCollapsed ? "invisible " : "visible";
return ( return (
<div className="h-full flex flex-col"> <div className="h-full flex flex-col">
<div className="flex flex-row justify-between mb-2"> <div className="flex flex-row justify-between mb-2">
<div className="visible mx-3 mt-2"> <div className="visible mx-3 mt-2">
<button <button onClick={() => setIsCollapsed((i) => !i)}>
onClick={() => { {isCollapsed ? <LeftChevron /> : <RightChevron />}
setUserCollapsed((prev) => {
if (prev === "unset") {
return collapsed ? "uncollapsed" : "collapsed";
}
return prev === "collapsed" ? "uncollapsed" : "collapsed";
});
}}
>
{collapsed ? <LeftChevron /> : <RightChevron />}
</button> </button>
</div> </div>
<div className={" " + (collapsed ? "w-0 invisible hidden" : "")}> <div className={" " + (isCollapsed ? "w-0 invisible hidden" : "")}>
<CourseSettingsLink /> <CourseSettingsLink />
</div> </div>
</div> </div>

View File

@@ -1,25 +1,25 @@
"use client"; "use client";
import { BreadCrumbs } from "@/components/BreadCrumbs";
import { Spinner } from "@/components/Spinner"; import { Spinner } from "@/components/Spinner";
import { import {
useCanvasAssignmentsQuery,
canvasAssignmentKeys, canvasAssignmentKeys,
} from "@/features/canvas/hooks/canvasAssignmentHooks"; useCanvasAssignmentsQuery,
import { canvasCourseKeys } from "@/features/canvas/hooks/canvasCourseHooks"; } from "@/hooks/canvas/canvasAssignmentHooks";
import { canvasCourseKeys } from "@/hooks/canvas/canvasCourseHooks";
import { import {
useCanvasModulesQuery,
canvasCourseModuleKeys, canvasCourseModuleKeys,
} from "@/features/canvas/hooks/canvasModuleHooks"; useCanvasModulesQuery,
} from "@/hooks/canvas/canvasModuleHooks";
import { import {
useCanvasPagesQuery,
canvasPageKeys, canvasPageKeys,
} from "@/features/canvas/hooks/canvasPageHooks"; useCanvasPagesQuery,
} from "@/hooks/canvas/canvasPageHooks";
import { import {
useCanvasQuizzesQuery,
canvasQuizKeys, canvasQuizKeys,
} from "@/features/canvas/hooks/canvasQuizHooks"; useCanvasQuizzesQuery,
import { useLocalCourseSettingsQuery } from "@/features/local/course/localCoursesHooks"; } from "@/hooks/canvas/canvasQuizHooks";
import { useLocalCourseSettingsQuery } from "@/hooks/localCourse/localCoursesHooks";
import { useQueryClient } from "@tanstack/react-query"; import { useQueryClient } from "@tanstack/react-query";
import Link from "next/link";
export function CourseNavigation() { export function CourseNavigation() {
const { data: settings } = useLocalCourseSettingsQuery(); const { data: settings } = useLocalCourseSettingsQuery();
@@ -33,8 +33,9 @@ export function CourseNavigation() {
return ( return (
<div className="pb-1 flex flex-row gap-3"> <div className="pb-1 flex flex-row gap-3">
<BreadCrumbs /> <Link href={"/"} className="btn" shallow={true}>
Back to Course List
</Link>
<a <a
href={`https://snow.instructure.com/courses/${settings.canvasId}`} href={`https://snow.instructure.com/courses/${settings.canvasId}`}
className="btn" className="btn"

View File

@@ -1,6 +1,6 @@
"use client"; "use client";
import { useLocalCourseSettingsQuery } from "@/features/local/course/localCoursesHooks"; import { useLocalCourseSettingsQuery } from "@/hooks/localCourse/localCoursesHooks";
import Link from "next/link"; import Link from "next/link";
import { useCourseContext } from "./context/courseContext"; import { useCourseContext } from "./context/courseContext";
import { getCourseSettingsUrl } from "@/services/urlUtils"; import { getCourseSettingsUrl } from "@/services/urlUtils";

View File

@@ -1,12 +1,12 @@
"use client"; "use client";
import { CalendarMonthModel, getWeekNumber } from "./calendarMonthUtils"; import { CalendarMonthModel, getWeekNumber } from "./calendarMonthUtils";
import { DayOfWeek } from "@/models/local/localCourseSettings";
import { Expandable } from "@/components/Expandable"; import { Expandable } from "@/components/Expandable";
import { CalendarWeek } from "./CalendarWeek"; import { CalendarWeek } from "./CalendarWeek";
import { useLocalCourseSettingsQuery } from "@/features/local/course/localCoursesHooks"; import { useLocalCourseSettingsQuery } from "@/hooks/localCourse/localCoursesHooks";
import { getDateFromStringOrThrow } from "@/features/local/utils/timeUtils"; import { getDateFromStringOrThrow } from "@/models/local/utils/timeUtils";
import UpChevron from "@/components/icons/UpChevron"; import UpChevron from "@/components/icons/UpChevron";
import DownChevron from "@/components/icons/DownChevron"; import DownChevron from "@/components/icons/DownChevron";
import { DayOfWeek } from "@/features/local/course/localCourseSettings";
export const CalendarMonth = ({ month }: { month: CalendarMonthModel }) => { export const CalendarMonth = ({ month }: { month: CalendarMonthModel }) => {
// const weekInMilliseconds = 604_800_000; // const weekInMilliseconds = 604_800_000;
@@ -29,8 +29,7 @@ export const CalendarMonth = ({ month }: { month: CalendarMonthModel }) => {
new Date(month.year, month.month, 1) new Date(month.year, month.month, 1)
); );
const shouldCollapse = const shouldCollapse = (pastWeekNumber >= startOfMonthWeekNumber) && !isPastSemester;
pastWeekNumber >= startOfMonthWeekNumber && !isPastSemester;
const monthName = new Date(month.year, month.month - 1, 1).toLocaleString( const monthName = new Date(month.year, month.month - 1, 1).toLocaleString(
"default", "default",
@@ -47,7 +46,7 @@ export const CalendarMonth = ({ month }: { month: CalendarMonthModel }) => {
className={ className={
"text-2xl transition-all duration-500 " + "text-2xl transition-all duration-500 " +
"hover:text-slate-50 underline hover:scale-105 " + "hover:text-slate-50 underline hover:scale-105 " +
"flex cursor-pointer" "flex "
} }
onClick={() => setIsExpanded((e) => !e)} onClick={() => setIsExpanded((e) => !e)}
role="button" role="button"

View File

@@ -1,6 +1,6 @@
"use client"; "use client";
import { useLocalCourseSettingsQuery } from "@/features/local/course/localCoursesHooks"; import { useLocalCourseSettingsQuery } from "@/hooks/localCourse/localCoursesHooks";
import { getDateFromStringOrThrow } from "@/features/local/utils/timeUtils"; import { getDateFromStringOrThrow } from "@/models/local/utils/timeUtils";
import { getWeekNumber } from "./calendarMonthUtils"; import { getWeekNumber } from "./calendarMonthUtils";
import Day from "./day/Day"; import Day from "./day/Day";

View File

@@ -1,8 +1,8 @@
"use client"; "use client";
import { getDateFromStringOrThrow } from "@/features/local/utils/timeUtils"; import { getDateFromStringOrThrow } from "@/models/local/utils/timeUtils";
import { getMonthsBetweenDates } from "./calendarMonthUtils"; import { getMonthsBetweenDates } from "./calendarMonthUtils";
import { CalendarMonth } from "./CalendarMonth"; import { CalendarMonth } from "./CalendarMonth";
import { useLocalCourseSettingsQuery } from "@/features/local/course/localCoursesHooks"; import { useLocalCourseSettingsQuery } from "@/hooks/localCourse/localCoursesHooks";
import { useEffect, useMemo, useRef } from "react"; import { useEffect, useMemo, useRef } from "react";
import CalendarItemsContextProvider from "../context/CalendarItemsContextProvider"; import CalendarItemsContextProvider from "../context/CalendarItemsContextProvider";
@@ -13,11 +13,10 @@ export default function CourseCalendar() {
() => getDateFromStringOrThrow(settings.startDate, "course start date"), () => getDateFromStringOrThrow(settings.startDate, "course start date"),
[settings.startDate] [settings.startDate]
); );
const endDateTime = useMemo(() => { const endDateTime = useMemo(
const date = getDateFromStringOrThrow(settings.endDate, "course end date"); () => getDateFromStringOrThrow(settings.endDate, "course end date"),
date.setDate(date.getDate() + 14); // buffer to make sure calendar shows week of finals and grades due [settings.endDate]
return date; );
}, [settings.endDate]);
const months = useMemo( const months = useMemo(
() => getMonthsBetweenDates(startDateTime, endDateTime), () => getMonthsBetweenDates(startDateTime, endDateTime),
[endDateTime, startDateTime] [endDateTime, startDateTime]
@@ -44,15 +43,15 @@ export default function CourseCalendar() {
return ( return (
<div <div
className=" className="
min-h-0 min-h-0
flex-grow flex-grow
border-2 border-2
border-gray-900 border-gray-900
rounded-lg rounded-lg
bg-linear-to-br bg-linear-to-br
from-blue-950/30 from-blue-950/30
to-fuchsia-950/10 to-60% to-fuchsia-950/10 to-60%
sm:p-1 sm:p-1
" "
> >
<div <div

View File

@@ -1,7 +1,7 @@
import { import {
dateToMarkdownString, dateToMarkdownString,
getDateFromStringOrThrow, getDateFromStringOrThrow,
} from "@/features/local/utils/timeUtils"; } from "@/models/local/utils/timeUtils";
export interface CalendarMonthModel { export interface CalendarMonthModel {
year: number; year: number;

View File

@@ -2,18 +2,18 @@
import { import {
getDateFromStringOrThrow, getDateFromStringOrThrow,
getDateOnlyMarkdownString, getDateOnlyMarkdownString,
} from "@/features/local/utils/timeUtils"; } from "@/models/local/utils/timeUtils";
import { useDraggingContext } from "../../context/drag/draggingContext"; import { useDraggingContext } from "../../context/drag/draggingContext";
import { useLocalCourseSettingsQuery } from "@/features/local/course/localCoursesHooks"; import { useLocalCourseSettingsQuery } from "@/hooks/localCourse/localCoursesHooks";
import { ItemInDay } from "./itemInDay/ItemInDay"; import { getDayOfWeek } from "@/models/local/localCourseSettings";
import { ItemInDay } from "./ItemInDay";
import { useTodaysItems } from "./useTodaysItems"; import { useTodaysItems } from "./useTodaysItems";
import { DayTitle } from "./DayTitle"; import { DayTitle } from "./DayTitle";
import { getDayOfWeek } from "@/features/local/course/localCourseSettings";
export default function Day({ day, month }: { day: string; month: number }) { export default function Day({ day, month }: { day: string; month: number }) {
const dayAsDate = getDateFromStringOrThrow( const dayAsDate = getDateFromStringOrThrow(
day, day,
"calculating same month in day", "calculating same month in day"
); );
const isToday = const isToday =
getDateOnlyMarkdownString(new Date()) === getDateOnlyMarkdownString(new Date()) ===
@@ -31,8 +31,8 @@ export default function Day({ day, month }: { day: string; month: number }) {
(holidaysHappeningToday, holiday) => { (holidaysHappeningToday, holiday) => {
const holidayDates = holiday.days.map((d) => const holidayDates = holiday.days.map((d) =>
getDateOnlyMarkdownString( getDateOnlyMarkdownString(
getDateFromStringOrThrow(d, "holiday date in day component"), getDateFromStringOrThrow(d, "holiday date in day component")
), )
); );
const today = getDateOnlyMarkdownString(dayAsDate); const today = getDateOnlyMarkdownString(dayAsDate);
@@ -40,16 +40,16 @@ export default function Day({ day, month }: { day: string; month: number }) {
return [...holidaysHappeningToday, holiday.name]; return [...holidaysHappeningToday, holiday.name];
return holidaysHappeningToday; return holidaysHappeningToday;
}, },
[] as string[], [] as string[]
); );
const semesterStart = getDateFromStringOrThrow( const semesterStart = getDateFromStringOrThrow(
settings.startDate, settings.startDate,
"comparing start date in day", "comparing start date in day"
); );
const semesterEnd = getDateFromStringOrThrow( const semesterEnd = getDateFromStringOrThrow(
settings.endDate, settings.endDate,
"comparing end date in day", "comparing end date in day"
); );
const isInSemester = semesterStart < dayAsDate && semesterEnd > dayAsDate; const isInSemester = semesterStart < dayAsDate && semesterEnd > dayAsDate;
@@ -90,7 +90,7 @@ export default function Day({ day, month }: { day: string; month: number }) {
status={status} status={status}
message={message} message={message}
/> />
), )
)} )}
{todaysQuizzes.map(({ quiz, moduleName, status, message }) => ( {todaysQuizzes.map(({ quiz, moduleName, status, message }) => (
<ItemInDay <ItemInDay

View File

@@ -5,11 +5,11 @@ import { useCourseContext } from "../../context/courseContext";
import NewItemForm from "../../modules/NewItemForm"; import NewItemForm from "../../modules/NewItemForm";
import { DraggableItem } from "../../context/drag/draggingContext"; import { DraggableItem } from "../../context/drag/draggingContext";
import { useDragStyleContext } from "../../context/drag/dragStyleContext"; import { useDragStyleContext } from "../../context/drag/dragStyleContext";
import { getLectureForDay } from "@/features/local/utils/lectureUtils"; import { getLectureForDay } from "@/models/local/utils/lectureUtils";
import { useLecturesSuspenseQuery } from "@/features/local/lectures/lectureHooks"; import { useLecturesSuspenseQuery } from "@/hooks/localCourse/lectureHooks";
import ClientOnly from "@/components/ClientOnly"; import ClientOnly from "@/components/ClientOnly";
import { Tooltip } from "@/components/Tooltip"; import { Tooltip } from "@/components/Tooltip";
import { useTooltip } from "@/components/useTooltip"; import { useRef, useState } from "react";
export function DayTitle({ day, dayAsDate }: { day: string; dayAsDate: Date }) { export function DayTitle({ day, dayAsDate }: { day: string; dayAsDate: Date }) {
const { courseName } = useCourseContext(); const { courseName } = useCourseContext();
@@ -17,7 +17,8 @@ export function DayTitle({ day, dayAsDate }: { day: string; dayAsDate: Date }) {
const { setIsDragging } = useDragStyleContext(); const { setIsDragging } = useDragStyleContext();
const todaysLecture = getLectureForDay(weeks, dayAsDate); const todaysLecture = getLectureForDay(weeks, dayAsDate);
const modal = useModal(); const modal = useModal();
const { visible, targetRef, showTooltip, hideTooltip } = useTooltip(); const linkRef = useRef<HTMLAnchorElement>(null);
const [tooltipVisible, setTooltipVisible] = useState(false);
const lectureName = todaysLecture && (todaysLecture.name || "lecture"); const lectureName = todaysLecture && (todaysLecture.name || "lecture");
@@ -43,9 +44,9 @@ export function DayTitle({ day, dayAsDate }: { day: string; dayAsDate: Date }) {
setIsDragging(true); setIsDragging(true);
} }
}} }}
ref={targetRef} ref={linkRef}
onMouseEnter={showTooltip} onMouseEnter={() => setTooltipVisible(true)}
onMouseLeave={hideTooltip} onMouseLeave={() => setTooltipVisible(false)}
> >
{dayAsDate.getDate()} {lectureName} {dayAsDate.getDate()} {lectureName}
</Link> </Link>
@@ -64,40 +65,15 @@ export function DayTitle({ day, dayAsDate }: { day: string; dayAsDate: Date }) {
)} )}
</div> </div>
} }
targetRef={targetRef} targetRef={linkRef}
visible={visible} visible={tooltipVisible}
/> />
)} )}
</ClientOnly> </ClientOnly>
<Modal <Modal
buttonComponent={({ openModal }) => (
<svg
viewBox="0 0 24 24"
width={22}
height={22}
className="cursor-pointer hover:scale-125 hover:stroke-slate-400 stroke-slate-500 transition-all m-0.5"
fill="none"
xmlns="http://www.w3.org/2000/svg"
onClick={openModal}
>
<g id="SVGRepo_bgCarrier" strokeWidth="0"></g>
<g
id="SVGRepo_tracerCarrier"
strokeLinecap="round"
strokeLinejoin="round"
></g>
<g id="SVGRepo_iconCarrier">
<path
d="M6 12H18M12 6V18"
className=" "
strokeWidth="3"
strokeLinecap="round"
strokeLinejoin="round"
></path>
</g>
</svg>
)}
modalControl={modal} modalControl={modal}
buttonText="+"
buttonClass="unstyled hover:font-bold hover:scale-125 px-1 mb-auto mt-0 pt-0"
modalWidth="w-135" modalWidth="w-135"
> >
{({ closeModal }) => ( {({ closeModal }) => (

View File

@@ -0,0 +1,70 @@
import { IModuleItem } from "@/models/local/IModuleItem";
import { getModuleItemUrl } from "@/services/urlUtils";
import Link from "next/link";
import { ReactNode, useRef, useState } from "react";
import { useCourseContext } from "../../context/courseContext";
import { DraggableItem } from "../../context/drag/draggingContext";
import ClientOnly from "@/components/ClientOnly";
import { useDragStyleContext } from "../../context/drag/dragStyleContext";
import { Tooltip } from "../../../../../components/Tooltip";
export function ItemInDay({
type,
moduleName,
status,
item,
message,
}: {
type: "assignment" | "page" | "quiz";
status: "localOnly" | "incomplete" | "published";
moduleName: string;
item: IModuleItem;
message: ReactNode;
}) {
const { courseName } = useCourseContext();
const { setIsDragging } = useDragStyleContext();
const linkRef = useRef<HTMLAnchorElement>(null);
const [tooltipVisible, setTooltipVisible] = useState(false);
return (
<div className={" relative group "}>
<Link
href={getModuleItemUrl(courseName, moduleName, type, item.name)}
shallow={true}
className={
" border rounded-sm sm:px-1 sm:mx-1 break-words mb-1 truncate sm:text-wrap text-nowrap " +
" bg-slate-800 " +
" block " +
(status === "localOnly" && " text-slate-500 border-slate-600 ") +
(status === "incomplete" && " border-rose-900 ") +
(status === "published" && " border-green-800 ")
}
role="button"
draggable="true"
onDragStart={(e) => {
const draggableItem: DraggableItem = {
type,
item,
sourceModuleName: moduleName,
};
e.dataTransfer.setData(
"draggableItem",
JSON.stringify(draggableItem)
);
setIsDragging(true);
}}
onMouseEnter={() => setTooltipVisible(true)}
onMouseLeave={() => setTooltipVisible(false)}
ref={linkRef}
>
{item.name}
</Link>
<ClientOnly>
<Tooltip
message={message}
targetRef={linkRef}
visible={tooltipVisible && status === "incomplete"}
/>
</ClientOnly>
</div>
);
}

View File

@@ -1,18 +1,18 @@
"use client"; "use client";
import { CanvasAssignment } from "@/features/canvas/models/assignments/canvasAssignment"; import { CanvasAssignment } from "@/models/canvas/assignments/canvasAssignment";
import { CanvasPage } from "@/features/canvas/models/pages/canvasPageModel"; import { CanvasPage } from "@/models/canvas/pages/canvasPageModel";
import { CanvasQuiz } from "@/features/canvas/models/quizzes/canvasQuizModel"; import { CanvasQuiz } from "@/models/canvas/quizzes/canvasQuizModel";
import { LocalAssignment } from "@/features/local/assignments/models/localAssignment"; import { LocalAssignment } from "@/models/local/assignment/localAssignment";
import { LocalCourseSettings } from "@/models/local/localCourseSettings";
import { LocalCoursePage } from "@/models/local/page/localCoursePage";
import { LocalQuiz } from "@/models/local/quiz/localQuiz";
import { import {
dateToMarkdownString, dateToMarkdownString,
getDateFromStringOrThrow, getDateFromStringOrThrow,
} from "@/features/local/utils/timeUtils"; } from "@/models/local/utils/timeUtils";
import { markdownToHTMLSafe } from "@/services/htmlMarkdownUtils"; import { markdownToHTMLSafe } from "@/services/htmlMarkdownUtils";
import { htmlIsCloseEnough } from "@/services/utils/htmlIsCloseEnough"; import { htmlIsCloseEnough } from "@/services/utils/htmlIsCloseEnough";
import { ReactNode } from "react"; import { ReactNode } from "react";
import { LocalCoursePage } from "@/features/local/pages/localCoursePageModels";
import { LocalQuiz } from "@/features/local/quizzes/models/localQuiz";
import { LocalCourseSettings } from "@/features/local/course/localCourseSettings";
export const getStatus = ({ export const getStatus = ({
item, item,
@@ -105,16 +105,7 @@ export const getStatus = ({
try { try {
const htmlIsSame = htmlIsCloseEnough( const htmlIsSame = htmlIsCloseEnough(
markdownToHTMLSafe({ markdownToHTMLSafe(assignment.description, settings),
markdownString: assignment.description,
settings,
replaceText: [
{
source: "insert_github_classroom_url",
destination: assignment.githubClassroomAssignmentShareLink || "",
},
],
}),
canvasAssignment.description canvasAssignment.description
); );
if (!htmlIsSame) if (!htmlIsSame)

View File

@@ -1,213 +0,0 @@
"use client";
import { useEffect, FC, useState } from "react";
import { IModuleItem } from "@/features/local/modules/IModuleItem";
import { LocalAssignment } from "@/features/local/assignments/models/localAssignment";
import { useCalendarItemsContext } from "../../../context/calendarItemsContext";
import {
useCreateAssignmentMutation,
useDeleteAssignmentMutation,
} from "@/features/local/assignments/assignmentHooks";
import {
useCanvasAssignmentsQuery,
useUpdateAssignmentInCanvasMutation,
useDeleteAssignmentFromCanvasMutation,
useAddAssignmentToCanvasMutation,
} from "@/features/canvas/hooks/canvasAssignmentHooks";
import { useLocalCourseSettingsQuery } from "@/features/local/course/localCoursesHooks";
import { baseCanvasUrl } from "@/features/canvas/services/canvasServiceUtils";
import { useCourseContext } from "../../../context/courseContext";
import Modal, { ModalControl } from "@/components/Modal";
function getDuplicateName(name: string, existingNames: string[]): string {
const match = name.match(/^(.*)\s+(\d+)$/);
const baseName = match ? match[1] : name;
const startNum = match ? parseInt(match[2]) + 1 : 2;
let num = startNum;
while (existingNames.includes(`${baseName} ${num}`)) {
num++;
}
return `${baseName} ${num}`;
}
export const AssignmentDayItemContextMenu: FC<{
modalControl: ModalControl;
item: IModuleItem;
moduleName: string;
}> = ({ modalControl, item, moduleName }) => {
const { courseName } = useCourseContext();
const calendarItems = useCalendarItemsContext();
const createAssignmentMutation = useCreateAssignmentMutation();
const deleteLocalMutation = useDeleteAssignmentMutation();
const updateInCanvasMutation = useUpdateAssignmentInCanvasMutation();
const deleteFromCanvasMutation = useDeleteAssignmentFromCanvasMutation();
const addToCanvasMutation = useAddAssignmentToCanvasMutation();
const { data: canvasAssignments } = useCanvasAssignmentsQuery();
const { data: settings } = useLocalCourseSettingsQuery();
const [confirmingDelete, setConfirmingDelete] = useState(false);
const assignmentInCanvas = canvasAssignments?.find(
(a) => a.name === item.name,
);
const canvasUrl = assignmentInCanvas
? `${baseCanvasUrl}/courses/${settings.canvasId}/assignments/${assignmentInCanvas.id}`
: undefined;
useEffect(() => {
const handleEscape = (e: KeyboardEvent) => {
if (e.key === "Escape") {
setConfirmingDelete(false);
modalControl.closeModal();
}
};
document.addEventListener("keydown", handleEscape);
return () => {
document.removeEventListener("keydown", handleEscape);
};
}, [modalControl]);
const handleClose = () => {
setConfirmingDelete(false);
modalControl.closeModal();
};
const handleDuplicate = () => {
const assignment = item as LocalAssignment;
const existingNames = Object.values(calendarItems).flatMap((modules) =>
(modules[moduleName]?.assignments ?? []).map((a) => a.name),
);
const newName = getDuplicateName(item.name, existingNames);
createAssignmentMutation.mutate({
courseName,
moduleName,
assignmentName: newName,
assignment: { ...assignment, name: newName },
});
handleClose();
};
const handleDelete = () => {
deleteLocalMutation.mutate({ courseName, moduleName, assignmentName: item.name });
handleClose();
};
const handleUpdateCanvas = () => {
if (assignmentInCanvas) {
updateInCanvasMutation.mutate({
canvasAssignmentId: assignmentInCanvas.id,
assignment: item as LocalAssignment,
});
handleClose();
}
};
const handleDeleteFromCanvas = () => {
if (assignmentInCanvas) {
deleteFromCanvasMutation.mutate({
canvasAssignmentId: assignmentInCanvas.id,
assignmentName: item.name,
});
handleClose();
}
};
const handleAddToCanvas = () => {
addToCanvasMutation.mutate({
assignment: item as LocalAssignment,
moduleName,
});
handleClose();
};
const baseButtonClasses = " font-bold text-left py-1";
const normalButtonClass =
"hover:bg-blue-900 disabled:opacity-50 bg-blue-900/50 text-blue-50 border border-blue-800/70 rounded ";
const dangerClasses =
"bg-rose-900/30 hover:bg-rose-950 disabled:opacity-50 text-rose-50 border border-rose-900/40 rounded";
return (
<Modal modalControl={modalControl} backgroundCoverColor="bg-black/30">
{() => (
<div className="p-2">
<div className="text-center p-1 text-slate-200 ">{item.name}</div>
<div className="flex flex-col gap-2">
{confirmingDelete ? (
<>
<div
className={``}
>
Delete from disk?
</div>
<button
onClick={handleClose}
className={`unstyled ${baseButtonClasses} ${normalButtonClass}`}
>
Cancel
</button>
<button
onClick={handleDelete}
className={`unstyled ${baseButtonClasses} ${dangerClasses}`}
>
Yes, delete
</button>
</>
) : (
<>
{canvasUrl && (
<>
<a
href={canvasUrl}
target="_blank"
rel="noreferrer"
className={` block px-3 ${baseButtonClasses} ${normalButtonClass}`}
onClick={handleClose}
>
View in Canvas
</a>
<button
onClick={handleUpdateCanvas}
disabled={updateInCanvasMutation.isPending}
className={`unstyled ${baseButtonClasses} ${normalButtonClass}`}
>
Update in Canvas
</button>
<button
onClick={handleDeleteFromCanvas}
disabled={deleteFromCanvasMutation.isPending}
className={`unstyled ${baseButtonClasses} ${dangerClasses}`}
>
Delete from Canvas
</button>
</>
)}
{!canvasUrl && (
<>
<button
onClick={handleAddToCanvas}
disabled={addToCanvasMutation.isPending}
className={`unstyled ${baseButtonClasses} ${normalButtonClass}`}
>
Add to Canvas
</button>
<button
onClick={() => setConfirmingDelete(true)}
className={`unstyled ${baseButtonClasses} ${dangerClasses}`}
>
Delete from Disk
</button>
</>
)}
<button
onClick={handleDuplicate}
className={`unstyled ${baseButtonClasses} ${normalButtonClass}`}
>
Duplicate
</button>
</>
)}
</div>
</div>
)}
</Modal>
);
};

View File

@@ -1,37 +0,0 @@
"use client";
import MarkdownDisplay from "@/components/MarkdownDisplay";
import { IModuleItem } from "@/features/local/modules/IModuleItem";
import { FC } from "react";
export const GetPreviewContent: FC<{
type: "assignment" | "page" | "quiz";
item: IModuleItem;
}> = ({ type, item }) => {
if (type === "assignment" && "description" in item) {
const assignment = item as {
description: string;
githubClassroomAssignmentShareLink?: string;
};
return (
<MarkdownDisplay
markdown={assignment.description}
replaceText={[
{
source: "insert_github_classroom_url",
destination: assignment.githubClassroomAssignmentShareLink || "",
},
]}
/>
);
} else if (type === "page" && "text" in item) {
return <MarkdownDisplay markdown={item.text as string} />;
} else if (type === "quiz" && "questions" in item) {
const quiz = item as { questions: { text: string }[] };
return quiz.questions.map((q, i: number) => (
<div key={i} className="">
<MarkdownDisplay markdown={q.text as string} />
</div>
));
}
return null;
};

View File

@@ -1,93 +0,0 @@
"use client";
import { IModuleItem } from "@/features/local/modules/IModuleItem";
import { getModuleItemUrl } from "@/services/urlUtils";
import Link from "next/link";
import { FC, ReactNode } from "react";
import { useCourseContext } from "../../../context/courseContext";
import { useTooltip } from "@/components/useTooltip";
import { DraggableItem } from "../../../context/drag/draggingContext";
import ClientOnly from "@/components/ClientOnly";
import { useDragStyleContext } from "../../../context/drag/dragStyleContext";
import { Tooltip } from "../../../../../../components/Tooltip";
import { AssignmentDayItemContextMenu } from "./DayItemContextMenu";
import { GetPreviewContent } from "./GetPreviewContent";
import { useModal } from "@/components/Modal";
export const ItemInDay: FC<{
type: "assignment" | "page" | "quiz";
status: "localOnly" | "incomplete" | "published";
moduleName: string;
item: IModuleItem;
message: ReactNode;
}> = ({ type, moduleName, status, item, message }) => {
const { courseName } = useCourseContext();
const { setIsDragging } = useDragStyleContext();
const { visible, targetRef, showTooltip, hideTooltip } = useTooltip(500);
const modalControl = useModal();
const handleContextMenu = (e: React.MouseEvent) => {
if (type !== "assignment") return;
e.preventDefault();
e.stopPropagation();
modalControl.openModal({ x: e.clientX, y: e.clientY });
};
return (
<div className={" relative group "}>
<Link
href={getModuleItemUrl(courseName, moduleName, type, item.name)}
shallow={true}
className={
" border rounded-sm sm:px-1 sm:mx-1 break-words mb-1 truncate sm:text-wrap text-nowrap " +
" bg-slate-800 " +
" block " +
(status === "localOnly" && " text-slate-500 border-slate-600 ") +
(status === "incomplete" && " border-rose-900 ") +
(status === "published" && " border-green-800 ")
}
role="button"
draggable="true"
onDragStart={(e) => {
const draggableItem: DraggableItem = {
type,
item,
sourceModuleName: moduleName,
};
e.dataTransfer.setData(
"draggableItem",
JSON.stringify(draggableItem),
);
setIsDragging(true);
}}
onMouseEnter={showTooltip}
onMouseLeave={hideTooltip}
onContextMenu={handleContextMenu}
ref={targetRef}
>
{item.name}
</Link>
<ClientOnly>
{status === "published" ? (
<Tooltip
message={
<div className="max-w-md">
<GetPreviewContent type={type} item={item} />
</div>
}
targetRef={targetRef}
visible={visible}
/>
) : (
<Tooltip message={message} targetRef={targetRef} visible={visible} />
)}
{type === "assignment" && (
<AssignmentDayItemContextMenu
modalControl={modalControl}
item={item}
moduleName={moduleName}
/>
)}
</ClientOnly>
</div>
);
};

View File

@@ -1,19 +1,18 @@
"use client"; "use client";
import { useCanvasAssignmentsQuery } from "@/hooks/canvas/canvasAssignmentHooks";
import { LocalAssignment } from "@/features/local/assignments/models/localAssignment"; import { useCanvasPagesQuery } from "@/hooks/canvas/canvasPageHooks";
import { useCanvasQuizzesQuery } from "@/hooks/canvas/canvasQuizHooks";
import { LocalAssignment } from "@/models/local/assignment/localAssignment";
import { LocalCoursePage } from "@/models/local/page/localCoursePage";
import { LocalQuiz } from "@/models/local/quiz/localQuiz";
import { import {
getDateFromStringOrThrow, getDateFromStringOrThrow,
getDateOnlyMarkdownString, getDateOnlyMarkdownString,
} from "@/features/local/utils/timeUtils"; } from "@/models/local/utils/timeUtils";
import { ReactNode } from "react"; import { ReactNode } from "react";
import { useCalendarItemsContext } from "../../context/calendarItemsContext"; import { useCalendarItemsContext } from "../../context/calendarItemsContext";
import { getStatus } from "./getStatus"; import { getStatus } from "./getStatus";
import { useLocalCourseSettingsQuery } from "@/features/local/course/localCoursesHooks"; import { useLocalCourseSettingsQuery } from "@/hooks/localCourse/localCoursesHooks";
import { LocalCoursePage } from "@/features/local/pages/localCoursePageModels";
import { LocalQuiz } from "@/features/local/quizzes/models/localQuiz";
import { useCanvasAssignmentsQuery } from "@/features/canvas/hooks/canvasAssignmentHooks";
import { useCanvasPagesQuery } from "@/features/canvas/hooks/canvasPageHooks";
import { useCanvasQuizzesQuery } from "@/features/canvas/hooks/canvasQuizHooks";
export function useTodaysItems(day: string) { export function useTodaysItems(day: string) {
const { data: settings } = useLocalCourseSettingsQuery(); const { data: settings } = useLocalCourseSettingsQuery();

View File

@@ -1,24 +0,0 @@
import Link from "next/link";
export default function ItemNavigationButtons({
previousUrl,
nextUrl,
}: {
previousUrl: string | null;
nextUrl: string | null;
}) {
return (
<>
{previousUrl && (
<Link className="btn" href={previousUrl} shallow={true}>
Previous
</Link>
)}
{nextUrl && (
<Link className="btn" href={nextUrl} shallow={true}>
Next
</Link>
)}
</>
);
}

View File

@@ -1,4 +1,4 @@
"use client"; "use client"
import { ReactNode } from "react"; import { ReactNode } from "react";
import { import {
CalendarItemsContext, CalendarItemsContext,
@@ -8,7 +8,7 @@ import {
useCourseQuizzesByModuleByDateQuery, useCourseQuizzesByModuleByDateQuery,
useCourseAssignmentsByModuleByDateQuery, useCourseAssignmentsByModuleByDateQuery,
useCoursePagesByModuleByDateQuery, useCoursePagesByModuleByDateQuery,
} from "@/features/local/modules/localCourseModuleHooks"; } from "@/hooks/localCourse/localCourseModuleHooks";
export default function CalendarItemsContextProvider({ export default function CalendarItemsContextProvider({
children, children,

View File

@@ -1,6 +1,6 @@
import { LocalAssignment } from "@/features/local/assignments/models/localAssignment"; import { LocalAssignment } from "@/models/local/assignment/localAssignment";
import { LocalCoursePage } from "@/features/local/pages/localCoursePageModels"; import { LocalCoursePage } from "@/models/local/page/localCoursePage";
import { LocalQuiz } from "@/features/local/quizzes/models/localQuiz"; import { LocalQuiz } from "@/models/local/quiz/localQuiz";
import { createContext, useContext } from "react"; import { createContext, useContext } from "react";
export interface CalendarItemsInterface { export interface CalendarItemsInterface {

View File

@@ -1,5 +1,5 @@
"use client"; "use client";
import { IModuleItem } from "@/features/local/modules/IModuleItem"; import { IModuleItem } from "@/models/local/IModuleItem";
import { createContext, useContext, DragEvent } from "react"; import { createContext, useContext, DragEvent } from "react";
export interface DraggableItem { export interface DraggableItem {

View File

@@ -1,8 +1,6 @@
"use client"; "use client";
import { import { getDateFromStringOrThrow, dateToMarkdownString } from "@/models/local/utils/timeUtils";
getDateFromStringOrThrow,
dateToMarkdownString,
} from "@/features/local/utils/timeUtils";
export function getNewLockDate( export function getNewLockDate(
originalDueDate: string, originalDueDate: string,
@@ -11,18 +9,15 @@ export function getNewLockDate(
): string | undefined { ): string | undefined {
// todo: preserve previous due date / lock date offset // todo: preserve previous due date / lock date offset
const dueDate = getDateFromStringOrThrow(originalDueDate, "dueAt date"); const dueDate = getDateFromStringOrThrow(originalDueDate, "dueAt date");
const lockDate = const lockDate = originalLockDate === undefined
originalLockDate === undefined ? undefined
? undefined : getDateFromStringOrThrow(originalLockDate, "lockAt date");
: getDateFromStringOrThrow(originalLockDate, "lockAt date");
const originalOffset = const originalOffset = lockDate === undefined ? undefined : lockDate.getTime() - dueDate.getTime();
lockDate === undefined ? undefined : lockDate.getTime() - dueDate.getTime();
const newLockDate = const newLockDate = originalOffset === undefined
originalOffset === undefined ? undefined
? undefined : new Date(dayAsDate.getTime() + originalOffset);
: new Date(dayAsDate.getTime() + originalOffset);
return newLockDate === undefined return newLockDate === undefined
? undefined ? undefined

View File

@@ -1,26 +1,26 @@
"use client"; "use client";
import { useUpdateAssignmentMutation } from "@/hooks/localCourse/assignmentHooks";
import { import {
useLecturesSuspenseQuery, useLecturesSuspenseQuery,
useLectureUpdateMutation, useLectureUpdateMutation,
} from "@/features/local/lectures/lectureHooks"; } from "@/hooks/localCourse/lectureHooks";
import { useLocalCourseSettingsQuery } from "@/features/local/course/localCoursesHooks"; import { useLocalCourseSettingsQuery } from "@/hooks/localCourse/localCoursesHooks";
import { useUpdatePageMutation } from "@/features/local/pages/pageHooks"; import { useUpdatePageMutation } from "@/hooks/localCourse/pageHooks";
import { LocalAssignment } from "@/features/local/assignments/models/localAssignment"; import { LocalAssignment } from "@/models/local/assignment/localAssignment";
import { Lecture } from "@/features/local/lectures/lectureModel"; import { Lecture } from "@/models/local/lecture";
import { getLectureForDay } from "@/features/local/utils/lectureUtils"; import { getLectureForDay } from "@/models/local/utils/lectureUtils";
import { LocalCoursePage } from "@/models/local/page/localCoursePage";
import { LocalQuiz } from "@/models/local/quiz/localQuiz";
import { import {
getDateFromStringOrThrow, getDateFromStringOrThrow,
getDateOnlyMarkdownString, getDateOnlyMarkdownString,
dateToMarkdownString, dateToMarkdownString,
} from "@/features/local/utils/timeUtils"; } from "@/models/local/utils/timeUtils";
import { Dispatch, SetStateAction, useCallback, DragEvent } from "react"; import { Dispatch, SetStateAction, useCallback, DragEvent } from "react";
import { DraggableItem } from "./draggingContext"; import { DraggableItem } from "./draggingContext";
import { getNewLockDate } from "./getNewLockDate"; import { getNewLockDate } from "./getNewLockDate";
import { useUpdateQuizMutation } from "@/features/local/quizzes/quizHooks"; import { useUpdateQuizMutation } from "@/hooks/localCourse/quizHooks";
import { useCourseContext } from "../courseContext"; import { useCourseContext } from "../courseContext";
import { useUpdateAssignmentMutation } from "@/features/local/assignments/assignmentHooks";
import { LocalCoursePage } from "@/features/local/pages/localCoursePageModels";
import { LocalQuiz } from "@/features/local/quizzes/models/localQuiz";
export function useItemDropOnDay({ export function useItemDropOnDay({
setIsDragging, setIsDragging,

View File

@@ -1,13 +1,13 @@
"use client"; "use client";
import { useUpdatePageMutation } from "@/features/local/pages/pageHooks"; import { useUpdateAssignmentMutation } from "@/hooks/localCourse/assignmentHooks";
import { LocalAssignment } from "@/features/local/assignments/models/localAssignment"; import { useUpdatePageMutation } from "@/hooks/localCourse/pageHooks";
import { LocalAssignment } from "@/models/local/assignment/localAssignment";
import { LocalCoursePage } from "@/models/local/page/localCoursePage";
import { LocalQuiz } from "@/models/local/quiz/localQuiz";
import { Dispatch, SetStateAction, useCallback, DragEvent } from "react"; import { Dispatch, SetStateAction, useCallback, DragEvent } from "react";
import { DraggableItem } from "./draggingContext"; import { DraggableItem } from "./draggingContext";
import { useCourseContext } from "../courseContext"; import { useCourseContext } from "../courseContext";
import { useUpdateQuizMutation } from "@/features/local/quizzes/quizHooks"; import { useUpdateQuizMutation } from "@/hooks/localCourse/quizHooks";
import { useUpdateAssignmentMutation } from "@/features/local/assignments/assignmentHooks";
import { LocalCoursePage } from "@/features/local/pages/localCoursePageModels";
import { LocalQuiz } from "@/features/local/quizzes/models/localQuiz";
export function useItemDropOnModule({ export function useItemDropOnModule({
setIsDragging, setIsDragging,

View File

@@ -1,70 +0,0 @@
import { describe, it, expect } from "vitest";
import {
getOrderedItems,
getOrderedLectures,
getNavigationLinks,
OrderedCourseItem,
} from "./navigationLogic";
import { CalendarItemsInterface } from "../context/calendarItemsContext";
describe("navigationLogic", () => {
const courseName = "testCourse";
it("getOrderedItems should order items by date, then alphabetically by name", () => {
const createMock = (
date: string,
name: string,
key: "assignments" | "quizzes" | "pages"
) =>
({
[date]: { "Module 1": { [key]: [{ name }] } },
} as unknown as CalendarItemsInterface);
const orderedItems = getOrderedItems(
courseName,
createMock("2023-01-01", "Z Assignment", "assignments"),
createMock("2023-01-01", "A Quiz", "quizzes"),
createMock("2023-01-02", "B Assignment", "assignments"),
createMock("2023-01-02", "A Page", "pages")
);
expect(orderedItems.map((i) => `${i.date} ${i.name}`)).toEqual([
"2023-01-01 A Quiz",
"2023-01-01 Z Assignment",
"2023-01-02 A Page",
"2023-01-02 B Assignment",
]);
});
it("getNavigationLinks should handle wrapping and normal navigation", () => {
const items: OrderedCourseItem[] = [
{ type: "assignment", name: "1", moduleName: "M", date: "D", url: "u1" },
{ type: "quiz", name: "2", moduleName: "M", date: "D", url: "u2" },
{ type: "page", name: "3", moduleName: "M", date: "D", url: "u3" },
];
// Forward wrap (last -> first)
expect(getNavigationLinks(items, "page", "3", "M").nextUrl).toBe("u1");
// Backward wrap (first -> last)
expect(getNavigationLinks(items, "assignment", "1", "M").previousUrl).toBe(
"u3"
);
// Normal navigation (middle)
const middle = getNavigationLinks(items, "quiz", "2", "M");
expect(middle.previousUrl).toBe("u1");
expect(middle.nextUrl).toBe("u3");
});
it("getOrderedLectures should flatten weeks and generate correct URLs", () => {
const weeks = [
{ lectures: [{ date: "01/01/2023" }] },
{ lectures: [{ date: "01/02/2023" }, { date: "01/03/2023" }] },
];
const lectures = getOrderedLectures(weeks, courseName);
expect(lectures).toHaveLength(3);
expect(lectures[0].url).toContain(encodeURIComponent("01/01/2023"));
expect(lectures[0].type).toBe("lecture");
});
});

View File

@@ -1,83 +0,0 @@
import { CalendarItemsInterface } from "../context/calendarItemsContext";
import { getLectureUrl, getModuleItemUrl } from "@/services/urlUtils";
export type CourseItemType = "assignment" | "quiz" | "page" | "lecture";
export interface OrderedCourseItem {
type: CourseItemType;
name: string;
moduleName?: string;
date: string;
url: string;
}
export function getOrderedItems(
courseName: string,
...calendars: CalendarItemsInterface[]
): OrderedCourseItem[] {
const itemTypes = [
{ key: "assignments" as const, type: "assignment" as const },
{ key: "quizzes" as const, type: "quiz" as const },
{ key: "pages" as const, type: "page" as const },
];
return calendars
.flatMap((calendar) =>
Object.entries(calendar).flatMap(([date, modules]) =>
Object.entries(modules).flatMap(([moduleName, moduleData]) =>
itemTypes.flatMap(({ key, type }) =>
(moduleData[key] || []).map((item) => ({
type,
name: item.name,
moduleName,
date,
url: getModuleItemUrl(courseName, moduleName, type, item.name),
}))
)
)
)
)
.sort((a, b) => {
const dateCompare = a.date.localeCompare(b.date);
if (dateCompare !== 0) return dateCompare;
return a.name.localeCompare(b.name);
});
}
export function getOrderedLectures(
weeks: { lectures: { date: string }[] }[],
courseName: string
): OrderedCourseItem[] {
return weeks
.flatMap((week) => week.lectures)
.map((lecture) => ({
type: "lecture",
name: lecture.date,
date: lecture.date,
url: getLectureUrl(courseName, lecture.date),
}));
}
export function getNavigationLinks(
list: OrderedCourseItem[],
type: CourseItemType,
name: string,
moduleName?: string
) {
const index = list.findIndex((item) => {
if (type === "lecture") return item.date === name;
return (
item.name === name && item.type === type && item.moduleName === moduleName
);
});
if (index === -1) return { previousUrl: null, nextUrl: null };
const previousIndex = (index - 1 + list.length) % list.length;
const nextIndex = (index + 1) % list.length;
return {
previousUrl: list[previousIndex].url,
nextUrl: list[nextIndex].url,
};
}

View File

@@ -1,14 +0,0 @@
import { useOrderedCourseItems } from "./useOrderedCourseItems";
import { getNavigationLinks, CourseItemType } from "./navigationLogic";
export function useItemNavigation(
type: CourseItemType,
name: string,
moduleName?: string
) {
const { orderedItems, orderedLectures } = useOrderedCourseItems();
const list = type === "lecture" ? orderedLectures : orderedItems;
return getNavigationLinks(list, type, name, moduleName);
}

View File

@@ -1,24 +0,0 @@
import {
useCourseAssignmentsByModuleByDateQuery,
useCoursePagesByModuleByDateQuery,
useCourseQuizzesByModuleByDateQuery,
} from "@/features/local/modules/localCourseModuleHooks";
import { useLecturesSuspenseQuery } from "@/features/local/lectures/lectureHooks";
import { useCourseContext } from "../context/courseContext";
import { getOrderedItems, getOrderedLectures } from "./navigationLogic";
export function useOrderedCourseItems() {
const { courseName } = useCourseContext();
const { data: weeks } = useLecturesSuspenseQuery();
const orderedItems = getOrderedItems(
courseName,
useCourseAssignmentsByModuleByDateQuery(),
useCourseQuizzesByModuleByDateQuery(),
useCoursePagesByModuleByDateQuery()
);
const orderedLectures = getOrderedLectures(weeks, courseName);
return { orderedItems, orderedLectures };
}

View File

@@ -9,9 +9,8 @@ export async function generateMetadata({
params: Promise<{ courseName: string }>; params: Promise<{ courseName: string }>;
}): Promise<Metadata> { }): Promise<Metadata> {
const { courseName } = await params; const { courseName } = await params;
const decodedCourseName = decodeURIComponent(getTitle(courseName));
return { return {
title: decodedCourseName, title: getTitle(courseName),
}; };
} }

View File

@@ -3,18 +3,18 @@ import { MonacoEditor } from "@/components/editor/MonacoEditor";
import { import {
useLecturesSuspenseQuery, useLecturesSuspenseQuery,
useLectureUpdateMutation, useLectureUpdateMutation,
} from "@/features/local/lectures/lectureHooks"; } from "@/hooks/localCourse/lectureHooks";
import { import {
lectureToString, lectureToString,
parseLecture, parseLecture,
} from "@/features/local/lectures/lectureUtils"; } from "@/services/fileStorage/utils/lectureUtils";
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import LecturePreview from "./LecturePreview"; import LecturePreview from "./LecturePreview";
import EditLectureTitle from "./EditLectureTitle"; import EditLectureTitle from "./EditLectureTitle";
import LectureButtons from "./LectureButtons"; import LectureButtons from "./LectureButtons";
import { useCourseContext } from "../../context/courseContext"; import { useCourseContext } from "../../context/courseContext";
import { useLocalCourseSettingsQuery } from "@/features/local/course/localCoursesHooks"; import { useLocalCourseSettingsQuery } from "@/hooks/localCourse/localCoursesHooks";
import { Lecture } from "@/features/local/lectures/lectureModel"; import { Lecture } from "@/models/local/lecture";
import { useAuthoritativeUpdates } from "../../utils/useAuthoritativeUpdates"; import { useAuthoritativeUpdates } from "../../utils/useAuthoritativeUpdates";
import { EditLayout } from "@/components/EditLayout"; import { EditLayout } from "@/components/EditLayout";

View File

@@ -1,11 +1,10 @@
import { useLocalCourseSettingsQuery } from "@/features/local/course/localCoursesHooks"; import { useLocalCourseSettingsQuery } from "@/hooks/localCourse/localCoursesHooks";
import { getDateFromString } from "@/features/local/utils/timeUtils"; import { getDayOfWeek } from "@/models/local/localCourseSettings";
import { getLectureWeekName } from "@/features/local/lectures/lectureUtils"; import { getDateFromString } from "@/models/local/utils/timeUtils";
import { getLecturePreviewUrl } from "@/services/urlUtils"; import { getLectureWeekName } from "@/services/fileStorage/utils/lectureUtils";
import { getCourseUrl, getLecturePreviewUrl } from "@/services/urlUtils";
import { useCourseContext } from "../../context/courseContext"; import { useCourseContext } from "../../context/courseContext";
import Link from "next/link"; import Link from "next/link";
import { getDayOfWeek } from "@/features/local/course/localCourseSettings";
import { BreadCrumbs } from "@/components/BreadCrumbs";
export default function EditLectureTitle({ export default function EditLectureTitle({
lectureDay, lectureDay,
@@ -18,7 +17,15 @@ export default function EditLectureTitle({
const lectureWeekName = getLectureWeekName(settings.startDate, lectureDay); const lectureWeekName = getLectureWeekName(settings.startDate, lectureDay);
return ( return (
<div className="flex justify-between sm:flex-row flex-col"> <div className="flex justify-between sm:flex-row flex-col">
<BreadCrumbs /> <div className="my-auto">
<Link
className="btn hidden sm:inline"
href={getCourseUrl(courseName)}
shallow={true}
>
{courseName}
</Link>
</div>
<div className="flex justify-center "> <div className="flex justify-center ">
<h3 className="mt-auto me-3 text-slate-500 ">Lecture</h3> <h3 className="mt-auto me-3 text-slate-500 ">Lecture</h3>
<h1 className=""> <h1 className="">

View File

@@ -6,11 +6,9 @@ import { getCourseUrl } from "@/services/urlUtils";
import { useRouter } from "next/navigation"; import { useRouter } from "next/navigation";
import { useState } from "react"; import { useState } from "react";
import { useCourseContext } from "../../context/courseContext"; import { useCourseContext } from "../../context/courseContext";
import { useLocalCourseSettingsQuery } from "@/features/local/course/localCoursesHooks"; import { useLocalCourseSettingsQuery } from "@/hooks/localCourse/localCoursesHooks";
import { useDeleteLectureMutation } from "@/features/local/lectures/lectureHooks"; import { useDeleteLectureMutation } from "@/hooks/localCourse/lectureHooks";
import Link from "next/link"; import Link from "next/link";
import { useItemNavigation } from "../../hooks/useItemNavigation";
import ItemNavigationButtons from "../../components/ItemNavigationButtons";
export default function LectureButtons({ lectureDay }: { lectureDay: string }) { export default function LectureButtons({ lectureDay }: { lectureDay: string }) {
const { courseName } = useCourseContext(); const { courseName } = useCourseContext();
@@ -19,7 +17,6 @@ export default function LectureButtons({ lectureDay }: { lectureDay: string }) {
const [isLoading, setIsLoading] = useState(false); const [isLoading, setIsLoading] = useState(false);
const modal = useModal(); const modal = useModal();
const deleteLecture = useDeleteLectureMutation(); const deleteLecture = useDeleteLectureMutation();
const { previousUrl, nextUrl } = useItemNavigation("lecture", lectureDay);
return ( return (
<div className="p-5 flex flex-row justify-end gap-3"> <div className="p-5 flex flex-row justify-end gap-3">
@@ -64,7 +61,6 @@ export default function LectureButtons({ lectureDay }: { lectureDay: string }) {
<Link className="btn" href={getCourseUrl(courseName)} shallow={true}> <Link className="btn" href={getCourseUrl(courseName)} shallow={true}>
Go Back Go Back
</Link> </Link>
<ItemNavigationButtons previousUrl={previousUrl} nextUrl={nextUrl} />
{isLoading && <Spinner />} {isLoading && <Spinner />}
</div> </div>
); );

View File

@@ -1,5 +1,5 @@
import MarkdownDisplay from "@/components/MarkdownDisplay"; import MarkdownDisplay from "@/components/MarkdownDisplay";
import { Lecture } from "@/features/local/lectures/lectureModel"; import { Lecture } from "@/models/local/lecture";
export default function LecturePreview({ lecture }: { lecture: Lecture }) { export default function LecturePreview({ lecture }: { lecture: Lecture }) {
return ( return (
@@ -11,7 +11,7 @@ export default function LecturePreview({ lecture }: { lecture: Lecture }) {
</div> </div>
</section> </section>
<section> <section>
<MarkdownDisplay markdown={lecture.content} convertImages={false} /> <MarkdownDisplay markdown={lecture.content} />
</section> </section>
</> </>
); );

View File

@@ -11,9 +11,8 @@ export async function generateMetadata({
const { courseName, lectureDay } = await params; const { courseName, lectureDay } = await params;
const decodedDay = decodeURIComponent(lectureDay); const decodedDay = decodeURIComponent(lectureDay);
const dayOnly = decodedDay.split(" ")[0]; const dayOnly = decodedDay.split(" ")[0];
const decodedCourseName = decodeURIComponent(getTitle(courseName));
return { return {
title: getTitle(`${decodedCourseName} lecture ${dayOnly}`), title: getTitle(`${courseName} lecture ${dayOnly}`),
}; };
} }

View File

@@ -2,7 +2,7 @@ import EditLecture from "./EditLecture";
import { import {
getDateFromStringOrThrow, getDateFromStringOrThrow,
getDateOnlyMarkdownString, getDateOnlyMarkdownString,
} from "@/features/local/utils/timeUtils"; } from "@/models/local/utils/timeUtils";
export const dynamic = "force-dynamic"; export const dynamic = "force-dynamic";
export default async function page({ export default async function page({

View File

@@ -1,14 +1,17 @@
"use client"; "use client";
import LecturePreview from "../LecturePreview"; import LecturePreview from "../LecturePreview";
import { useLecturesSuspenseQuery } from "@/features/local/lectures/lectureHooks"; import { getCourseUrl, getLectureUrl } from "@/services/urlUtils";
import { BreadCrumbs } from "@/components/BreadCrumbs"; import { useCourseContext } from "../../../context/courseContext";
import Link from "next/link";
import { useLecturesSuspenseQuery } from "@/hooks/localCourse/lectureHooks";
export default function LecturePreviewPage({ export default function LecturePreviewPage({
lectureDay, lectureDay,
}: { }: {
lectureDay: string; lectureDay: string;
}) { }) {
const { courseName } = useCourseContext();
const { data: weeks } = useLecturesSuspenseQuery(); const { data: weeks } = useLecturesSuspenseQuery();
const lecture = weeks const lecture = weeks
.flatMap(({ lectures }) => lectures.map((lecture) => lecture)) .flatMap(({ lectures }) => lectures.map((lecture) => lecture))
@@ -20,7 +23,20 @@ export default function LecturePreviewPage({
return ( return (
<div className="flex h-full xl:flex-row flex-col "> <div className="flex h-full xl:flex-row flex-col ">
<div className="flex-shrink flex-1 pb-1 ms-3 xl:ms-0 flex flex-row flex-wrap gap-3 content-start "> <div className="flex-shrink flex-1 pb-1 ms-3 xl:ms-0 flex flex-row flex-wrap gap-3 content-start ">
<BreadCrumbs /> <div className="">
<Link
className="btn"
href={getLectureUrl(courseName, lectureDay)}
shallow={true}
>
Edit Lecture
</Link>
</div>
<div className="">
<Link className="btn" href={getCourseUrl(courseName)} shallow={true}>
Course Calendar
</Link>
</div>
</div> </div>
<div className="flex justify-center min-h-0 px-2"> <div className="flex justify-center min-h-0 px-2">
<div <div

View File

@@ -1,7 +1,7 @@
import { import {
getDateFromStringOrThrow, getDateFromStringOrThrow,
getDateOnlyMarkdownString, getDateOnlyMarkdownString,
} from "@/features/local/utils/timeUtils"; } from "@/models/local/utils/timeUtils";
import LecturePreviewPage from "./LecturePreviewPage"; import LecturePreviewPage from "./LecturePreviewPage";
export const dynamic = "force-dynamic"; export const dynamic = "force-dynamic";

View File

@@ -1,6 +1,6 @@
import { Expandable } from "@/components/Expandable"; import { Expandable } from "@/components/Expandable";
import TextInput from "@/components/form/TextInput"; import TextInput from "@/components/form/TextInput";
import { useCreateModuleMutation } from "@/features/local/modules/localCourseModuleHooks"; import { useCreateModuleMutation } from "@/hooks/localCourse/localCourseModuleHooks";
import React, { useState } from "react"; import React, { useState } from "react";
import { useCourseContext } from "../context/courseContext"; import { useCourseContext } from "../context/courseContext";

View File

@@ -1,11 +1,11 @@
"use client"; "use client";
import { usePagesQueries } from "@/features/local/pages/pageHooks"; import { usePagesQueries } from "@/hooks/localCourse/pageHooks";
import { IModuleItem } from "@/features/local/modules/IModuleItem"; import { IModuleItem } from "@/models/local/IModuleItem";
import { import {
getDateFromString, getDateFromString,
getDateFromStringOrThrow, getDateFromStringOrThrow,
getDateOnlyMarkdownString, getDateOnlyMarkdownString,
} from "@/features/local/utils/timeUtils"; } from "@/models/local/utils/timeUtils";
import { Fragment } from "react"; import { Fragment } from "react";
import Modal, { useModal } from "../../../../components/Modal"; import Modal, { useModal } from "../../../../components/Modal";
import NewItemForm from "./NewItemForm"; import NewItemForm from "./NewItemForm";
@@ -21,13 +21,10 @@ import { getModuleItemUrl } from "@/services/urlUtils";
import { useCourseContext } from "../context/courseContext"; import { useCourseContext } from "../context/courseContext";
import { Expandable } from "../../../../components/Expandable"; import { Expandable } from "../../../../components/Expandable";
import { useDragStyleContext } from "../context/drag/dragStyleContext"; import { useDragStyleContext } from "../context/drag/dragStyleContext";
import { useQuizzesQueries } from "@/features/local/quizzes/quizHooks"; import { useQuizzesQueries } from "@/hooks/localCourse/quizHooks";
import { useAssignmentNamesQuery } from "@/hooks/localCourse/assignmentHooks";
import { useTRPC } from "@/services/serverFunctions/trpcClient"; import { useTRPC } from "@/services/serverFunctions/trpcClient";
import { useSuspenseQueries } from "@tanstack/react-query"; import { useSuspenseQueries } from "@tanstack/react-query";
import { useAssignmentNamesQuery } from "@/features/local/assignments/assignmentHooks";
import { useReorderCanvasModuleItemsMutation } from "@/features/canvas/hooks/canvasModuleHooks";
import { useCanvasModulesQuery } from "@/features/canvas/hooks/canvasModuleHooks";
import { Spinner } from "@/components/Spinner";
export default function ExpandableModule({ export default function ExpandableModule({
moduleName, moduleName,
@@ -53,8 +50,6 @@ export default function ExpandableModule({
const { data: quizzes } = useQuizzesQueries(moduleName); const { data: quizzes } = useQuizzesQueries(moduleName);
const { data: pages } = usePagesQueries(moduleName); const { data: pages } = usePagesQueries(moduleName);
const modal = useModal(); const modal = useModal();
const reorderMutation = useReorderCanvasModuleItemsMutation();
const { data: canvasModules } = useCanvasModulesQuery();
const moduleItems: { const moduleItems: {
type: "assignment" | "quiz" | "page"; type: "assignment" | "quiz" | "page";
@@ -107,7 +102,7 @@ export default function ExpandableModule({
</ClientOnly> </ClientOnly>
<ExpandIcon <ExpandIcon
style={{ style={{
...(isExpanded ? { rotate: "90deg" } : {rotate: "180deg"}), ...(isExpanded ? { rotate: "-90deg" } : {}),
}} }}
/> />
</div> </div>
@@ -115,30 +110,6 @@ export default function ExpandableModule({
)} )}
> >
<> <>
{!reorderMutation.isPending && (
<button
className=" me-3"
onClick={() => {
const canvasModuleId = canvasModules?.find(
(m) => m.name === moduleName
)?.id;
if (!canvasModuleId) {
console.error(
"Canvas module ID not found for",
moduleName
);
return;
}
reorderMutation.mutate({
moduleId: canvasModuleId,
items: moduleItems.map((item) => item.item),
});
}}
>
Sort by Due Date
</button>
)}
{reorderMutation.isPending && <Spinner />}
<Modal <Modal
modalControl={modal} modalControl={modal}
buttonText="New Item" buttonText="New Item"

View File

@@ -2,9 +2,9 @@
import CheckIcon from "@/components/icons/CheckIcon"; import CheckIcon from "@/components/icons/CheckIcon";
import { Spinner } from "@/components/Spinner"; import { Spinner } from "@/components/Spinner";
import { import {
useCanvasModulesQuery,
useAddCanvasModuleMutation, useAddCanvasModuleMutation,
} from "@/features/canvas/hooks/canvasModuleHooks"; useCanvasModulesQuery,
} from "@/hooks/canvas/canvasModuleHooks";
export function ModuleCanvasStatus({ moduleName }: { moduleName: string }) { export function ModuleCanvasStatus({ moduleName }: { moduleName: string }) {
const { data: canvasModules } = useCanvasModulesQuery(); const { data: canvasModules } = useCanvasModulesQuery();

View File

@@ -1,5 +1,5 @@
"use client"; "use client";
import { useModuleNamesQuery } from "@/features/local/modules/localCourseModuleHooks"; import { useModuleNamesQuery } from "@/hooks/localCourse/localCourseModuleHooks";
import ExpandableModule from "./ExpandableModule"; import ExpandableModule from "./ExpandableModule";
import CreateModule from "./CreateModule"; import CreateModule from "./CreateModule";

View File

@@ -3,21 +3,20 @@ import ButtonSelect from "@/components/ButtonSelect";
import SelectInput from "@/components/form/SelectInput"; import SelectInput from "@/components/form/SelectInput";
import TextInput from "@/components/form/TextInput"; import TextInput from "@/components/form/TextInput";
import { Spinner } from "@/components/Spinner"; import { Spinner } from "@/components/Spinner";
import { useModuleNamesQuery } from "@/features/local/modules/localCourseModuleHooks"; import { useCreateAssignmentMutation } from "@/hooks/localCourse/assignmentHooks";
import { useLocalCourseSettingsQuery } from "@/features/local/course/localCoursesHooks"; import { useModuleNamesQuery } from "@/hooks/localCourse/localCourseModuleHooks";
import { useCreatePageMutation } from "@/features/local/pages/pageHooks"; import { useLocalCourseSettingsQuery } from "@/hooks/localCourse/localCoursesHooks";
import { LocalAssignmentGroup } from "@/features/local/assignments/models/localAssignmentGroup"; import { useCreatePageMutation } from "@/hooks/localCourse/pageHooks";
import { LocalAssignmentGroup } from "@/models/local/assignment/localAssignmentGroup";
import React, { useState } from "react"; import React, { useState } from "react";
import { useCourseContext } from "../context/courseContext"; import { useCourseContext } from "../context/courseContext";
import { useCreateQuizMutation } from "@/features/local/quizzes/quizHooks"; import { useCreateQuizMutation } from "@/hooks/localCourse/quizHooks";
import { import {
getDateFromString, getDateFromString,
dateToMarkdownString, dateToMarkdownString,
getDateFromStringOrThrow, getDateFromStringOrThrow,
} from "@/features/local/utils/timeUtils"; } from "@/models/local/utils/timeUtils";
import { useCreateAssignmentMutation } from "@/features/local/assignments/assignmentHooks";
import { validateFileName } from "@/services/fileNameValidation";
export default function NewItemForm({ export default function NewItemForm({
moduleName: defaultModuleName, moduleName: defaultModuleName,
@@ -40,13 +39,6 @@ export default function NewItemForm({
); );
const [name, setName] = useState(""); const [name, setName] = useState("");
const [nameError, setNameError] = useState("");
const handleNameChange = (newName: string) => {
setName(newName);
const error = validateFileName(newName);
setNameError(error);
};
const defaultDate = getDateFromString( const defaultDate = getDateFromString(
creationDate ? creationDate : dateToMarkdownString(new Date()) creationDate ? creationDate : dateToMarkdownString(new Date())
@@ -73,12 +65,6 @@ export default function NewItemForm({
className="flex flex-col gap-3" className="flex flex-col gap-3"
onSubmit={(e) => { onSubmit={(e) => {
e.preventDefault(); e.preventDefault();
// Validate name before submission
if (nameError) {
return;
}
const dueAt = const dueAt =
dueDate === "" dueDate === ""
? dueDate ? dueDate
@@ -167,31 +153,22 @@ export default function NewItemForm({
<div> <div>
<ButtonSelect<"Assignment" | "Quiz" | "Page"> <ButtonSelect<"Assignment" | "Quiz" | "Page">
options={["Assignment", "Quiz", "Page"]} options={["Assignment", "Quiz", "Page"]}
getOptionName={(o) => o?.toString() ?? ""} getName={(o) => o?.toString() ?? ""}
setValue={(t) => setType(t ?? "Assignment")} setSelectedOption={(t) => setType(t ?? "Assignment")}
value={type} selectedOption={type}
label="Type" label="Type"
/> />
</div> </div>
<div> <div>
<TextInput <TextInput label={type + " Name"} value={name} setValue={setName} />
label={type + " Name"}
value={name}
setValue={handleNameChange}
/>
{nameError && (
<div className="text-red-300 bg-red-950/50 border p-1 rounded border-red-900/50 text-sm mt-1">
{nameError}
</div>
)}
</div> </div>
<div> <div>
{type !== "Page" && ( {type !== "Page" && (
<ButtonSelect <ButtonSelect
options={settings.assignmentGroups} options={settings.assignmentGroups}
getOptionName={(g) => g?.name ?? ""} getName={(g) => g?.name ?? ""}
setValue={setAssignmentGroup} setSelectedOption={setAssignmentGroup}
value={assignmentGroup} selectedOption={assignmentGroup}
label="Assignment Group" label="Assignment Group"
/> />
)} )}
@@ -201,9 +178,7 @@ export default function NewItemForm({
No assignment groups created, create them in the course settings page No assignment groups created, create them in the course settings page
</div> </div>
)} )}
<button disabled={!!nameError} type="submit"> <button type="submit">Create</button>
Create
</button>
{isPending && <Spinner />} {isPending && <Spinner />}
</form> </form>
); );

View File

@@ -6,19 +6,17 @@ import {
useAddAssignmentToCanvasMutation, useAddAssignmentToCanvasMutation,
useDeleteAssignmentFromCanvasMutation, useDeleteAssignmentFromCanvasMutation,
useUpdateAssignmentInCanvasMutation, useUpdateAssignmentInCanvasMutation,
} from "@/features/canvas/hooks/canvasAssignmentHooks"; } from "@/hooks/canvas/canvasAssignmentHooks";
import { baseCanvasUrl } from "@/features/canvas/services/canvasServiceUtils";
import { import {
useAssignmentQuery, useAssignmentQuery,
useDeleteAssignmentMutation, useDeleteAssignmentMutation,
} from "@/features/local/assignments/assignmentHooks"; } from "@/hooks/localCourse/assignmentHooks";
import { useLocalCourseSettingsQuery } from "@/features/local/course/localCoursesHooks"; import { useLocalCourseSettingsQuery } from "@/hooks/localCourse/localCoursesHooks";
import { baseCanvasUrl } from "@/services/canvas/canvasServiceUtils";
import { getCourseUrl } from "@/services/urlUtils"; import { getCourseUrl } from "@/services/urlUtils";
import Link from "next/link"; import Link from "next/link";
import { useRouter } from "next/navigation"; import { useRouter } from "next/navigation";
import { useState } from "react"; import { useState } from "react";
import { useItemNavigation } from "../../../../hooks/useItemNavigation";
import ItemNavigationButtons from "../../../../components/ItemNavigationButtons";
export function AssignmentFooterButtons({ export function AssignmentFooterButtons({
moduleName, moduleName,
@@ -44,11 +42,6 @@ export function AssignmentFooterButtons({
const deleteLocal = useDeleteAssignmentMutation(); const deleteLocal = useDeleteAssignmentMutation();
const [isLoading, setIsLoading] = useState(false); const [isLoading, setIsLoading] = useState(false);
const modal = useModal(); const modal = useModal();
const { previousUrl, nextUrl } = useItemNavigation(
"assignment",
assignmentName,
moduleName
);
const assignmentInCanvas = canvasAssignments?.find( const assignmentInCanvas = canvasAssignments?.find(
(a) => a.name === assignmentName (a) => a.name === assignmentName
@@ -162,7 +155,6 @@ export function AssignmentFooterButtons({
<Link className="btn" href={getCourseUrl(courseName)} shallow={true}> <Link className="btn" href={getCourseUrl(courseName)} shallow={true}>
Go Back Go Back
</Link> </Link>
<ItemNavigationButtons previousUrl={previousUrl} nextUrl={nextUrl} />
</div> </div>
</div> </div>
); );

View File

@@ -1,7 +1,7 @@
import MarkdownDisplay from "@/components/MarkdownDisplay"; import MarkdownDisplay from "@/components/MarkdownDisplay";
import { LocalAssignment } from "@/features/local/assignments/models/localAssignment"; import { LocalAssignment } from "@/models/local/assignment/localAssignment";
import { rubricItemIsExtraCredit } from "@/features/local/assignments/models/rubricItem"; import { rubricItemIsExtraCredit } from "@/models/local/assignment/rubricItem";
import { assignmentPoints } from "@/features/local/assignments/models/utils/assignmentPointsUtils"; import { assignmentPoints } from "@/models/local/assignment/utils/assignmentPointsUtils";
import { formatHumanReadableDate } from "@/services/utils/dateFormat"; import { formatHumanReadableDate } from "@/services/utils/dateFormat";
import React, { Fragment } from "react"; import React, { Fragment } from "react";
@@ -59,15 +59,13 @@ export default function AssignmentPreview({
<hr /> <hr />
<br /> <br />
<section> <section>
<MarkdownDisplay <MarkdownDisplay markdown={assignment.description} />
markdown={assignment.description} {/* <div
replaceText={[ className="markdownPreview"
{ dangerouslySetInnerHTML={{
source: "insert_github_classroom_url", __html: htmlPreview,
destination: assignment.githubClassroomAssignmentShareLink || "", }}
}, ></div> */}
]}
/>
</section> </section>
<hr /> <hr />
<section> <section>
@@ -75,14 +73,15 @@ export default function AssignmentPreview({
{extraPoints !== 0 && ( {extraPoints !== 0 && (
<h5 className="text-center">{extraPoints} Extra Credit Points</h5> <h5 className="text-center">{extraPoints} Extra Credit Points</h5>
)} )}
<div className="grid grid-cols-[auto_auto_1fr]"> <div className="grid grid-cols-3">
{assignment.rubric.map((rubricItem, i) => ( {assignment.rubric.map((rubricItem, i) => (
<Fragment key={rubricItem.label + i}> <Fragment key={rubricItem.label + i}>
<div className="text-end pe-1"> <div className="text-end pe-3 col-span-2">{rubricItem.label}</div>
{rubricItemIsExtraCredit(rubricItem) ? "Extra Credit" : ""} <div>
{rubricItem.points}
{rubricItemIsExtraCredit(rubricItem) ? " - Extra Credit" : ""}
</div> </div>
<div className="text-end pe-3">{rubricItem.points}</div>
<div>{rubricItem.label}</div>
</Fragment> </Fragment>
))} ))}
</div> </div>

View File

@@ -1,13 +1,18 @@
"use client"; "use client";
import { MonacoEditor } from "@/components/editor/MonacoEditor"; import { MonacoEditor } from "@/components/editor/MonacoEditor";
import {
useAssignmentQuery,
useUpdateAssignmentMutation,
useUpdateImageSettingsForAssignment,
} from "@/hooks/localCourse/assignmentHooks";
import { import {
LocalAssignment, LocalAssignment,
localAssignmentMarkdown, localAssignmentMarkdown,
} from "@/features/local/assignments/models/localAssignment"; } from "@/models/local/assignment/localAssignment";
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import AssignmentPreview from "./AssignmentPreview"; import AssignmentPreview from "./AssignmentPreview";
import { useCourseContext } from "@/app/course/[courseName]/context/courseContext"; import { useCourseContext } from "@/app/course/[courseName]/context/courseContext";
import { useLocalCourseSettingsQuery } from "@/features/local/course/localCoursesHooks"; import { useLocalCourseSettingsQuery } from "@/hooks/localCourse/localCoursesHooks";
import ClientOnly from "@/components/ClientOnly"; import ClientOnly from "@/components/ClientOnly";
import { SuspenseAndErrorHandling } from "@/components/SuspenseAndErrorHandling"; import { SuspenseAndErrorHandling } from "@/components/SuspenseAndErrorHandling";
import { useRouter } from "next/navigation"; import { useRouter } from "next/navigation";
@@ -17,11 +22,6 @@ import EditAssignmentHeader from "./EditAssignmentHeader";
import { Spinner } from "@/components/Spinner"; import { Spinner } from "@/components/Spinner";
import { getAssignmentHelpString } from "./getAssignmentHelpString"; import { getAssignmentHelpString } from "./getAssignmentHelpString";
import { EditLayout } from "@/components/EditLayout"; import { EditLayout } from "@/components/EditLayout";
import {
useAssignmentQuery,
useUpdateAssignmentMutation,
useUpdateImageSettingsForAssignment,
} from "@/features/local/assignments/assignmentHooks";
export default function EditAssignment({ export default function EditAssignment({
moduleName, moduleName,
@@ -131,7 +131,7 @@ export default function EditAssignment({
Body={ Body={
<> <>
{showHelp && ( {showHelp && (
<div className=" max-w-96 flex-1 h-full overflow-y-auto"> <div className=" max-w-96">
<pre> <pre>
<code>{getAssignmentHelpString(settings)}</code> <code>{getAssignmentHelpString(settings)}</code>
</pre> </pre>

View File

@@ -1,6 +1,7 @@
import { BreadCrumbs } from "@/components/BreadCrumbs"; import { useCourseContext } from "@/app/course/[courseName]/context/courseContext";
import { UpdateAssignmentName } from "./UpdateAssignmentName"; import { UpdateAssignmentName } from "./UpdateAssignmentName";
import { RightSingleChevron } from "@/components/icons/RightSingleChevron"; import { getCourseUrl } from "@/services/urlUtils";
import Link from "next/link";
export default function EditAssignmentHeader({ export default function EditAssignmentHeader({
moduleName, moduleName,
@@ -9,21 +10,17 @@ export default function EditAssignmentHeader({
assignmentName: string; assignmentName: string;
moduleName: string; moduleName: string;
}) { }) {
const { courseName } = useCourseContext();
return ( return (
<div className="py-1 flex flex-row justify-between"> <div className="py-1 flex flex-row justify-start gap-3">
<div className="flex flex-row"> <Link className="btn" href={getCourseUrl(courseName)} shallow={true}>
<BreadCrumbs /> {courseName}
<span className="text-slate-500 cursor-default select-none my-auto"> </Link>
<RightSingleChevron /> <UpdateAssignmentName
</span> assignmentName={assignmentName}
<div className="my-auto px-3">{assignmentName}</div> moduleName={moduleName}
</div> />
<div className="px-1"> <div className="my-auto">{assignmentName}</div>
<UpdateAssignmentName
assignmentName={assignmentName}
moduleName={moduleName}
/>
</div>
</div> </div>
); );
} }

View File

@@ -5,7 +5,7 @@ import { Spinner } from "@/components/Spinner";
import { import {
useAssignmentQuery, useAssignmentQuery,
useUpdateAssignmentMutation, useUpdateAssignmentMutation,
} from "@/features/local/assignments/assignmentHooks"; } from "@/hooks/localCourse/assignmentHooks";
import { getModuleItemUrl } from "@/services/urlUtils"; import { getModuleItemUrl } from "@/services/urlUtils";
import { useRouter } from "next/navigation"; import { useRouter } from "next/navigation";
import { useState } from "react"; import { useState } from "react";
@@ -40,37 +40,22 @@ export function UpdateAssignmentName({
if (name === assignmentName) closeModal(); if (name === assignmentName) closeModal();
setIsLoading(true); // page refresh resets flag setIsLoading(true); // page refresh resets flag
try { await updateAssignment.mutateAsync({
await updateAssignment.mutateAsync({ assignment: assignment,
assignment: assignment, moduleName,
moduleName, assignmentName: name,
assignmentName: name, previousModuleName: moduleName,
previousModuleName: moduleName, previousAssignmentName: assignmentName,
previousAssignmentName: assignmentName, courseName,
courseName, });
});
// update url (will trigger reload...) // update url (will trigger reload...)
router.replace( router.replace(
getModuleItemUrl(courseName, moduleName, "assignment", name), getModuleItemUrl(courseName, moduleName, "assignment", name),
{} {}
); );
} finally {
setIsLoading(false);
}
}} }}
> >
<div
className="
text-yellow-300
bg-yellow-950/30
border-2
rounded-lg
border-yellow-800
p-1 text-sm mb-2"
>
Warning: does not rename in Canvas
</div>
<TextInput <TextInput
value={name} value={name}
setValue={setName} setValue={setName}

View File

@@ -1,6 +1,6 @@
"use client"; "use client";
import { AssignmentSubmissionType } from "@/features/local/assignments/models/assignmentSubmissionType"; import { AssignmentSubmissionType } from "@/models/local/assignment/assignmentSubmissionType";
import { LocalCourseSettings } from "@/features/local/course/localCourseSettings"; import { LocalCourseSettings } from "@/models/local/localCourseSettings";
export function getAssignmentHelpString(settings: LocalCourseSettings) { export function getAssignmentHelpString(settings: LocalCourseSettings) {
const groupNames = settings.assignmentGroups.map((g) => g.name).join("\n- "); const groupNames = settings.assignmentGroups.map((g) => g.name).join("\n- ");
@@ -33,7 +33,6 @@ You can use markdown to format your assignment description. For example, you can
[Link to Canvas](https://canvas.instructure.com) [Link to Canvas](https://canvas.instructure.com)
\`Inline code\` \`Inline code\`
> Blockquote > Blockquote
@@ -55,32 +54,6 @@ flowchart TD
C -->|Three| F[fa:fa-car Car] C -->|Three| F[fa:fa-car Car]
\`\`\` \`\`\`
## LaTeX Math
**Inline math:** The Fibonacci sequence is defined as: \$F(n) = F(n-1) + F(n-2)\$ where \$F(0) = 0\$ and \$F(1) = 1\$.
**Block math:**
\$\$F(n) = F(n-1) + F(n-2)\$\$
**Complex equations:**
\$\$
F(n) = \\begin{cases}
0 & \\text{if } n = 0 \\\\
1 & \\text{if } n = 1 \\\\
F(n-1) + F(n-2) & \\text{if } n > 1
\\end{cases}
\$\$
## github classroom links will be replaced by the GithubClassroomAssignmentShareLink setting
[Github Classroom](insert_github_classroom_url)
## Files
If you have mounted a folder in the /app/public/images directory, you can link to files like this:
![formulas](/images/facultyFiles/1405/lab-04-simple-math-formulas.png)
## Rubric ## Rubric
- 1pt: singular point - 1pt: singular point

View File

@@ -14,9 +14,8 @@ export async function generateMetadata({
}): Promise<Metadata> { }): Promise<Metadata> {
const { courseName, assignmentName } = await params; const { courseName, assignmentName } = await params;
const decodedAssignmentName = decodeURIComponent(assignmentName); const decodedAssignmentName = decodeURIComponent(assignmentName);
const decodedCourseName = decodeURIComponent(courseName);
return { return {
title: getTitle(`${decodedAssignmentName}, ${decodedCourseName}`), title: getTitle(`${decodedAssignmentName}, ${courseName}`),
}; };
} }

View File

@@ -1,8 +1,13 @@
"use client"; "use client";
import { MonacoEditor } from "@/components/editor/MonacoEditor"; import { MonacoEditor } from "@/components/editor/MonacoEditor";
import {
usePageQuery,
useUpdatePageMutation,
} from "@/hooks/localCourse/pageHooks";
import { localPageMarkdownUtils } from "@/models/local/page/localCoursePage";
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import PagePreview from "./PagePreview"; import PagePreview from "./PagePreview";
import { useLocalCourseSettingsQuery } from "@/features/local/course/localCoursesHooks"; import { useLocalCourseSettingsQuery } from "@/hooks/localCourse/localCoursesHooks";
import EditPageButtons from "./EditPageButtons"; import EditPageButtons from "./EditPageButtons";
import ClientOnly from "@/components/ClientOnly"; import ClientOnly from "@/components/ClientOnly";
import { useRouter } from "next/navigation"; import { useRouter } from "next/navigation";
@@ -10,11 +15,6 @@ import { useCourseContext } from "@/app/course/[courseName]/context/courseContex
import { useAuthoritativeUpdates } from "@/app/course/[courseName]/utils/useAuthoritativeUpdates"; import { useAuthoritativeUpdates } from "@/app/course/[courseName]/utils/useAuthoritativeUpdates";
import EditPageHeader from "./EditPageHeader"; import EditPageHeader from "./EditPageHeader";
import { EditLayout } from "@/components/EditLayout"; import { EditLayout } from "@/components/EditLayout";
import { localPageMarkdownUtils } from "@/features/local/pages/localCoursePageModels";
import {
usePageQuery,
useUpdatePageMutation,
} from "@/features/local/pages/pageHooks";
export default function EditPage({ export default function EditPage({
moduleName, moduleName,
@@ -102,13 +102,13 @@ export default function EditPage({
<EditLayout <EditLayout
Header={<EditPageHeader pageName={pageName} moduleName={moduleName} />} Header={<EditPageHeader pageName={pageName} moduleName={moduleName} />}
Body={ Body={
<div className="flex min-h-0 flex-1 gap-4 overflow-hidden"> <div className="columns-2 min-h-0 flex-1">
<div className="flex-1 h-full min-w-0 overflow-hidden"> <div className="flex-1 h-full">
<MonacoEditor key={monacoKey} value={text} onChange={textUpdate} /> <MonacoEditor key={monacoKey} value={text} onChange={textUpdate} />
</div> </div>
<div className="flex-1 h-full min-w-0 flex flex-col overflow-hidden"> <div className="h-full">
<div className="text-red-300">{error && error}</div> <div className="text-red-300">{error && error}</div>
<div className="flex-1 overflow-y-auto"> <div className="h-full overflow-y-auto">
<br /> <br />
<PagePreview page={page} /> <PagePreview page={page} />
</div> </div>
@@ -125,5 +125,5 @@ export default function EditPage({
</> </>
} }
/> />
); )
} }

View File

@@ -4,21 +4,19 @@ import { Spinner } from "@/components/Spinner";
import { import {
useCanvasPagesQuery, useCanvasPagesQuery,
useCreateCanvasPageMutation, useCreateCanvasPageMutation,
useUpdateCanvasPageMutation,
useDeleteCanvasPageMutation, useDeleteCanvasPageMutation,
} from "@/features/canvas/hooks/canvasPageHooks"; useUpdateCanvasPageMutation,
import { baseCanvasUrl } from "@/features/canvas/services/canvasServiceUtils"; } from "@/hooks/canvas/canvasPageHooks";
import { useLocalCourseSettingsQuery } from "@/features/local/course/localCoursesHooks"; import { useLocalCourseSettingsQuery } from "@/hooks/localCourse/localCoursesHooks";
import { import {
useDeletePageMutation, useDeletePageMutation,
usePageQuery, usePageQuery,
} from "@/features/local/pages/pageHooks"; } from "@/hooks/localCourse/pageHooks";
import { baseCanvasUrl } from "@/services/canvas/canvasServiceUtils";
import { getCourseUrl } from "@/services/urlUtils"; import { getCourseUrl } from "@/services/urlUtils";
import Link from "next/link"; import Link from "next/link";
import { useRouter } from "next/navigation"; import { useRouter } from "next/navigation";
import React, { useState } from "react"; import React, { useState } from "react";
import { useItemNavigation } from "../../../../hooks/useItemNavigation";
import ItemNavigationButtons from "../../../../components/ItemNavigationButtons";
export default function EditPageButtons({ export default function EditPageButtons({
moduleName, moduleName,
@@ -38,11 +36,6 @@ export default function EditPageButtons({
const deletePageLocal = useDeletePageMutation(); const deletePageLocal = useDeletePageMutation();
const modal = useModal(); const modal = useModal();
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const { previousUrl, nextUrl } = useItemNavigation(
"page",
pageName,
moduleName
);
const pageInCanvas = canvasPages?.find((p) => p.title === pageName); const pageInCanvas = canvasPages?.find((p) => p.title === pageName);
@@ -132,7 +125,6 @@ export default function EditPageButtons({
<Link className="btn" href={getCourseUrl(courseName)} shallow={true}> <Link className="btn" href={getCourseUrl(courseName)} shallow={true}>
Go Back Go Back
</Link> </Link>
<ItemNavigationButtons previousUrl={previousUrl} nextUrl={nextUrl} />
</div> </div>
); );
} }

View File

@@ -1,6 +1,7 @@
import { useCourseContext } from "@/app/course/[courseName]/context/courseContext";
import { getCourseUrl } from "@/services/urlUtils";
import Link from "next/link";
import { UpdatePageName } from "./UpdatePageName"; import { UpdatePageName } from "./UpdatePageName";
import { BreadCrumbs } from "@/components/BreadCrumbs";
import { RightSingleChevron } from "@/components/icons/RightSingleChevron";
export default function EditPageHeader({ export default function EditPageHeader({
moduleName, moduleName,
@@ -9,18 +10,18 @@ export default function EditPageHeader({
pageName: string; pageName: string;
moduleName: string; moduleName: string;
}) { }) {
const { courseName } = useCourseContext();
return ( return (
<div className="py-1 flex flex-row justify-between"> <div className="py-1 flex flex-row justify-start gap-3">
<div className="flex flex-row"> <Link
<BreadCrumbs /> className="btn"
<span className="text-slate-500 cursor-default select-none my-auto"> href={getCourseUrl(courseName)}
<RightSingleChevron /> shallow={true}
</span> >
<div className="my-auto px-3">{pageName}</div> {courseName}
</div> </Link>
<div className="px-1"> <UpdatePageName pageName={pageName} moduleName={moduleName} />
<UpdatePageName pageName={pageName} moduleName={moduleName} /> <div className="my-auto">{pageName}</div>
</div>
</div> </div>
); );
} }

View File

@@ -1,7 +1,9 @@
import MarkdownDisplay from "@/components/MarkdownDisplay"; import MarkdownDisplay from "@/components/MarkdownDisplay";
import { LocalCoursePage } from "@/features/local/pages/localCoursePageModels"; import { LocalCoursePage } from "@/models/local/page/localCoursePage";
import React from "react"; import React from "react";
export default function PagePreview({ page }: { page: LocalCoursePage }) { export default function PagePreview({ page }: { page: LocalCoursePage }) {
return <MarkdownDisplay markdown={page.text} />; return (
<MarkdownDisplay markdown={page.text} />
);
} }

View File

@@ -5,7 +5,7 @@ import { Spinner } from "@/components/Spinner";
import { import {
usePageQuery, usePageQuery,
useUpdatePageMutation, useUpdatePageMutation,
} from "@/features/local/pages/pageHooks"; } from "@/hooks/localCourse/pageHooks";
import { getModuleItemUrl } from "@/services/urlUtils"; import { getModuleItemUrl } from "@/services/urlUtils";
import { useRouter } from "next/navigation"; import { useRouter } from "next/navigation";
import { useState } from "react"; import { useState } from "react";
@@ -56,17 +56,6 @@ export function UpdatePageName({
); );
}} }}
> >
<div
className="
text-yellow-300
bg-yellow-950/30
border-2
rounded-lg
border-yellow-800
p-1 text-sm mb-2"
>
Warning: does not rename in Canvas
</div>
<TextInput value={name} setValue={setName} label={"Rename Page"} /> <TextInput value={name} setValue={setName} label={"Rename Page"} />
<button className="w-full my-3">Save New Name</button> <button className="w-full my-3">Save New Name</button>
{isLoading && <Spinner />} {isLoading && <Spinner />}

View File

@@ -14,9 +14,8 @@ export async function generateMetadata({
}): Promise<Metadata> { }): Promise<Metadata> {
const { courseName, pageName } = await params; const { courseName, pageName } = await params;
const decodedPageName = decodeURIComponent(pageName); const decodedPageName = decodeURIComponent(pageName);
const decodedCourseName = decodeURIComponent(courseName);
return { return {
title: getTitle(`${decodedPageName}, ${decodedCourseName}`), title: getTitle(`${decodedPageName}, ${courseName}`),
}; };
} }

View File

@@ -1,5 +1,6 @@
"use client"; "use client";
import { MonacoEditor } from "@/components/editor/MonacoEditor"; import { MonacoEditor } from "@/components/editor/MonacoEditor";
import { quizMarkdownUtils } from "@/models/local/quiz/utils/quizMarkdownUtils";
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import QuizPreview from "./QuizPreview"; import QuizPreview from "./QuizPreview";
import { QuizButtons } from "./QuizButton"; import { QuizButtons } from "./QuizButton";
@@ -10,17 +11,13 @@ import { useCourseContext } from "@/app/course/[courseName]/context/courseContex
import { import {
useQuizQuery, useQuizQuery,
useUpdateQuizMutation, useUpdateQuizMutation,
} from "@/features/local/quizzes/quizHooks"; } from "@/hooks/localCourse/quizHooks";
import { useAuthoritativeUpdates } from "../../../../utils/useAuthoritativeUpdates"; import { useAuthoritativeUpdates } from "../../../../utils/useAuthoritativeUpdates";
import { extractLabelValue } from "@/features/local/assignments/models/utils/markdownUtils"; import { extractLabelValue } from "@/models/local/assignment/utils/markdownUtils";
import EditQuizHeader from "./EditQuizHeader"; import EditQuizHeader from "./EditQuizHeader";
import { useLocalCourseSettingsQuery } from "@/features/local/course/localCoursesHooks"; import { LocalCourseSettings } from "@/models/local/localCourseSettings";
import { useGlobalSettingsQuery } from "@/features/local/globalSettings/globalSettingsHooks"; import { useLocalCourseSettingsQuery } from "@/hooks/localCourse/localCoursesHooks";
import { getFeedbackDelimitersFromSettings } from "@/features/local/globalSettings/globalSettingsUtils";
import type { GlobalSettings } from "@/features/local/globalSettings/globalSettingsModels";
import { EditLayout } from "@/components/EditLayout"; import { EditLayout } from "@/components/EditLayout";
import { quizMarkdownUtils } from "@/features/local/quizzes/models/utils/quizMarkdownUtils";
import { LocalCourseSettings } from "@/features/local/course/localCourseSettings";
const helpString = (settings: LocalCourseSettings) => { const helpString = (settings: LocalCourseSettings) => {
const groupNames = settings.assignmentGroups.map((g) => g.name).join("\n- "); const groupNames = settings.assignmentGroups.map((g) => g.name).join("\n- ");
@@ -64,38 +61,11 @@ points: 4
the underscore is optional the underscore is optional
short answer short answer
--- ---
short answer with auto-graded responses
*a) answer 1
*b) other valid answer
short_answer=
---
this is a matching question this is a matching question
^ left answer - right dropdown ^ left answer - right dropdown
^ other thing - another option ^ other thing - another option
^ - distractor ^ - distractor
^ - other distractor ^ - other distractor`;
---
Points: 3
FEEDBACK EXAMPLE
What is 2+3?
+ Correct! Good job
- Incorrect, try again
... This is general feedback shown regardless
*a) 4
*b) 5
c) 6
---
Points: 2
FEEDBACK EXAMPLE
Multiline feedback example
+
Great work!
You understand the concept.
-
Not quite right.
Review the material and try again.
*a) correct answer
b) wrong answer`;
}; };
export default function EditQuiz({ export default function EditQuiz({
@@ -114,15 +84,10 @@ export default function EditQuiz({
isFetching, isFetching,
} = useQuizQuery(moduleName, quizName); } = useQuizQuery(moduleName, quizName);
const updateQuizMutation = useUpdateQuizMutation(); const updateQuizMutation = useUpdateQuizMutation();
const { data: globalSettings } = useGlobalSettingsQuery();
const feedbackDelimiters = getFeedbackDelimitersFromSettings(
(globalSettings ?? ({} as GlobalSettings)) as GlobalSettings,
);
const { clientIsAuthoritative, text, textUpdate, monacoKey } = const { clientIsAuthoritative, text, textUpdate, monacoKey } =
useAuthoritativeUpdates({ useAuthoritativeUpdates({
serverUpdatedAt: serverDataUpdatedAt, serverUpdatedAt: serverDataUpdatedAt,
startingText: quizMarkdownUtils.toMarkdown(quiz, feedbackDelimiters), startingText: quizMarkdownUtils.toMarkdown(quiz),
}); });
const [error, setError] = useState(""); const [error, setError] = useState("");
@@ -138,18 +103,13 @@ export default function EditQuiz({
try { try {
const name = extractLabelValue(text, "Name"); const name = extractLabelValue(text, "Name");
if ( if (
quizMarkdownUtils.toMarkdown(quiz, feedbackDelimiters) !== quizMarkdownUtils.toMarkdown(quiz) !==
quizMarkdownUtils.toMarkdown( quizMarkdownUtils.toMarkdown(
quizMarkdownUtils.parseMarkdown(text, name, feedbackDelimiters), quizMarkdownUtils.parseMarkdown(text, name)
feedbackDelimiters,
) )
) { ) {
if (clientIsAuthoritative) { if (clientIsAuthoritative) {
const updatedQuiz = quizMarkdownUtils.parseMarkdown( const updatedQuiz = quizMarkdownUtils.parseMarkdown(text, quizName);
text,
quizName,
feedbackDelimiters,
);
await updateQuizMutation.mutateAsync({ await updateQuizMutation.mutateAsync({
quiz: updatedQuiz, quiz: updatedQuiz,
moduleName, moduleName,
@@ -160,7 +120,7 @@ export default function EditQuiz({
}); });
} else { } else {
console.log( console.log(
"client not authoritative, updating client with server quiz", "client not authoritative, updating client with server quiz"
); );
textUpdate(quizMarkdownUtils.toMarkdown(quiz), true); textUpdate(quizMarkdownUtils.toMarkdown(quiz), true);
} }
@@ -178,7 +138,6 @@ export default function EditQuiz({
}, [ }, [
clientIsAuthoritative, clientIsAuthoritative,
courseName, courseName,
feedbackDelimiters,
isFetching, isFetching,
moduleName, moduleName,
quiz, quiz,
@@ -195,7 +154,7 @@ export default function EditQuiz({
Body={ Body={
<> <>
{showHelp && ( {showHelp && (
<pre className=" max-w-96 h-full overflow-y-auto"> <pre className=" max-w-96">
<code>{helpString(settings)}</code> <code>{helpString(settings)}</code>
</pre> </pre>
)} )}

View File

@@ -1,6 +1,7 @@
import { RightSingleChevron } from "@/components/icons/RightSingleChevron"; import { useCourseContext } from "@/app/course/[courseName]/context/courseContext";
import { getCourseUrl } from "@/services/urlUtils";
import Link from "next/link";
import { UpdateQuizName } from "./UpdateQuizName"; import { UpdateQuizName } from "./UpdateQuizName";
import { BreadCrumbs } from "@/components/BreadCrumbs";
export default function EditQuizHeader({ export default function EditQuizHeader({
moduleName, moduleName,
@@ -9,18 +10,18 @@ export default function EditQuizHeader({
quizName: string; quizName: string;
moduleName: string; moduleName: string;
}) { }) {
const { courseName } = useCourseContext();
return ( return (
<div className="py-1 flex flex-row justify-between"> <div className="py-1 flex flex-row justify-start gap-3">
<div className="flex flex-row"> <Link
<BreadCrumbs /> className="btn"
<span className="text-slate-500 cursor-default select-none my-auto"> href={getCourseUrl(courseName)}
<RightSingleChevron /> shallow={true}
</span> >
<div className="my-auto px-3">{quizName}</div> {courseName}
</div> </Link>
<div className="px-1"> <UpdateQuizName quizName={quizName} moduleName={moduleName} />
<UpdateQuizName quizName={quizName} moduleName={moduleName} /> <div>{quizName}</div>
</div>
</div> </div>
); );
} }

View File

@@ -5,18 +5,16 @@ import {
useCanvasQuizzesQuery, useCanvasQuizzesQuery,
useAddQuizToCanvasMutation, useAddQuizToCanvasMutation,
useDeleteQuizFromCanvasMutation, useDeleteQuizFromCanvasMutation,
} from "@/features/canvas/hooks/canvasQuizHooks"; } from "@/hooks/canvas/canvasQuizHooks";
import { baseCanvasUrl } from "@/features/canvas/services/canvasServiceUtils"; import { useLocalCourseSettingsQuery } from "@/hooks/localCourse/localCoursesHooks";
import { useLocalCourseSettingsQuery } from "@/features/local/course/localCoursesHooks";
import { import {
useDeleteQuizMutation, useDeleteQuizMutation,
useQuizQuery, useQuizQuery,
} from "@/features/local/quizzes/quizHooks"; } from "@/hooks/localCourse/quizHooks";
import { baseCanvasUrl } from "@/services/canvas/canvasServiceUtils";
import { getCourseUrl } from "@/services/urlUtils"; import { getCourseUrl } from "@/services/urlUtils";
import Link from "next/link"; import Link from "next/link";
import { useRouter } from "next/navigation"; import { useRouter } from "next/navigation";
import { useItemNavigation } from "../../../../hooks/useItemNavigation";
import ItemNavigationButtons from "../../../../components/ItemNavigationButtons";
export function QuizButtons({ export function QuizButtons({
moduleName, moduleName,
@@ -37,11 +35,6 @@ export function QuizButtons({
const deleteFromCanvas = useDeleteQuizFromCanvasMutation(); const deleteFromCanvas = useDeleteQuizFromCanvasMutation();
const deleteLocal = useDeleteQuizMutation(); const deleteLocal = useDeleteQuizMutation();
const modal = useModal(); const modal = useModal();
const { previousUrl, nextUrl } = useItemNavigation(
"quiz",
quizName,
moduleName
);
const quizInCanvas = canvasQuizzes?.find((c) => c.title === quizName); const quizInCanvas = canvasQuizzes?.find((c) => c.title === quizName);
@@ -118,7 +111,6 @@ export function QuizButtons({
<Link className="btn" href={getCourseUrl(courseName)} shallow={true}> <Link className="btn" href={getCourseUrl(courseName)} shallow={true}>
Go Back Go Back
</Link> </Link>
<ItemNavigationButtons previousUrl={previousUrl} nextUrl={nextUrl} />
</div> </div>
</div> </div>
); );

View File

@@ -1,10 +1,10 @@
import CheckIcon from "@/components/icons/CheckIcon"; import CheckIcon from "@/components/icons/CheckIcon";
import MarkdownDisplay from "@/components/MarkdownDisplay"; import MarkdownDisplay from "@/components/MarkdownDisplay";
import { useQuizQuery } from "@/hooks/localCourse/quizHooks";
import { import {
LocalQuizQuestion, LocalQuizQuestion,
QuestionType, QuestionType,
} from "@/features/local/quizzes/models/localQuizQuestion"; } from "@/models/local/quiz/localQuizQuestion";
import { useQuizQuery } from "@/features/local/quizzes/quizHooks";
import { escapeMatchingText } from "@/services/utils/questionHtmlUtils"; import { escapeMatchingText } from "@/services/utils/questionHtmlUtils";
export default function QuizPreview({ export default function QuizPreview({
@@ -80,45 +80,6 @@ function QuizQuestionPreview({ question }: { question: LocalQuizQuestion }) {
</div> </div>
</div> </div>
<MarkdownDisplay markdown={question.text} className="ms-4 mb-2" /> <MarkdownDisplay markdown={question.text} className="ms-4 mb-2" />
{/* Feedback Section */}
{(question.correctComments ||
question.incorrectComments ||
question.neutralComments) && (
<div className=" m-2 ps-2 py-1 rounded flex bg-slate-950/50">
<div>Feedback</div>
<div className="mx-4 space-y-1">
{question.correctComments && (
<div className="border-l-2 border-green-700 pl-2 py-1 flex">
<span className="text-green-500">+ </span>
<MarkdownDisplay
markdown={question.correctComments}
className="ms-4 mb-2"
/>
</div>
)}
{question.incorrectComments && (
<div className="border-l-2 border-red-700 pl-2 py-1 flex">
<span className="text-red-500">- </span>
<MarkdownDisplay
markdown={question.incorrectComments}
className="ms-4 mb-2"
/>
</div>
)}
{question.neutralComments && (
<div className="border-l-2 border-blue-800 pl-2 py-1 flex">
<span className="text-blue-500">... </span>
<MarkdownDisplay
markdown={question.neutralComments}
className="ms-4 mb-2"
/>
</div>
)}
</div>
</div>
)}
{question.questionType === QuestionType.MATCHING && ( {question.questionType === QuestionType.MATCHING && (
<div> <div>
{question.answers.map((answer) => ( {question.answers.map((answer) => (

View File

@@ -5,7 +5,7 @@ import { Spinner } from "@/components/Spinner";
import { import {
useQuizQuery, useQuizQuery,
useUpdateQuizMutation, useUpdateQuizMutation,
} from "@/features/local/quizzes/quizHooks"; } from "@/hooks/localCourse/quizHooks";
import { getModuleItemUrl } from "@/services/urlUtils"; import { getModuleItemUrl } from "@/services/urlUtils";
import { useRouter } from "next/navigation"; import { useRouter } from "next/navigation";
import { useState } from "react"; import { useState } from "react";
@@ -56,17 +56,6 @@ export function UpdateQuizName({
); );
}} }}
> >
<div
className="
text-yellow-300
bg-yellow-950/30
border-2
rounded-lg
border-yellow-800
p-1 text-sm mb-2"
>
Warning: does not rename in Canvas
</div>
<TextInput value={name} setValue={setName} label={"Rename Quiz"} /> <TextInput value={name} setValue={setName} label={"Rename Quiz"} />
<button className="w-full my-3">Save New Name</button> <button className="w-full my-3">Save New Name</button>
{isLoading && <Spinner />} {isLoading && <Spinner />}

View File

@@ -14,9 +14,8 @@ export async function generateMetadata({
}): Promise<Metadata> { }): Promise<Metadata> {
const { courseName, quizName } = await params; const { courseName, quizName } = await params;
const decodedQuizName = decodeURIComponent(quizName); const decodedQuizName = decodeURIComponent(quizName);
const decodedCourseName = decodeURIComponent(courseName);
return { return {
title: getTitle(`${decodedQuizName}, ${decodedCourseName}`), title: getTitle(`${decodedQuizName}, ${courseName}`),
}; };
} }

View File

@@ -4,13 +4,14 @@ import { CourseNavigation } from "./CourseNavigation";
import { DragStyleContextProvider } from "./context/drag/dragStyleContext"; import { DragStyleContextProvider } from "./context/drag/dragStyleContext";
import CollapsableSidebar from "./CollapsableSidebar"; import CollapsableSidebar from "./CollapsableSidebar";
export default async function CoursePage() { export default async function CoursePage() {
return ( return (
<> <>
<div className="h-full flex flex-col"> <div className="h-full flex flex-col">
<DragStyleContextProvider> <DragStyleContextProvider>
<DraggingContextProvider> <DraggingContextProvider>
<div className="flex sm:flex-row h-full flex-col max-w-[2400px] w-full mx-auto"> <div className="flex sm:flex-row h-full flex-col max-w-[2400px] mx-auto">
<div className="flex-1 h-full flex flex-col"> <div className="flex-1 h-full flex flex-col">
<CourseNavigation /> <CourseNavigation />
<CourseCalendar /> <CourseCalendar />

View File

@@ -3,22 +3,20 @@
import { import {
useLocalCourseSettingsQuery, useLocalCourseSettingsQuery,
useUpdateLocalCourseSettingsMutation, useUpdateLocalCourseSettingsMutation,
} from "@/features/local/course/localCoursesHooks"; } from "@/hooks/localCourse/localCoursesHooks";
import { LocalAssignmentGroup } from "@/features/local/assignments/models/localAssignmentGroup"; import { LocalAssignmentGroup } from "@/models/local/assignment/localAssignmentGroup";
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import TextInput from "../../../../components/form/TextInput"; import TextInput from "../../../../components/form/TextInput";
import { useSetAssignmentGroupsMutation } from "@/hooks/canvas/canvasCourseHooks";
import { settingsBox } from "./sharedSettings"; import { settingsBox } from "./sharedSettings";
import { Spinner } from "@/components/Spinner"; import { Spinner } from "@/components/Spinner";
import { baseCanvasUrl } from "@/services/canvas/canvasServiceUtils";
import MeatballIcon from "./MeatballIcon"; import MeatballIcon from "./MeatballIcon";
import { useSetAssignmentGroupsMutation } from "@/features/canvas/hooks/canvasCourseHooks";
import { baseCanvasUrl } from "@/features/canvas/services/canvasServiceUtils";
import Modal, { useModal } from "@/components/Modal";
export default function AssignmentGroupManagement() { export default function AssignmentGroupManagement() {
const { data: settings, isPending } = useLocalCourseSettingsQuery(); const { data: settings, isPending } = useLocalCourseSettingsQuery();
const updateSettings = useUpdateLocalCourseSettingsMutation(); const updateSettings = useUpdateLocalCourseSettingsMutation();
const applyInCanvas = useSetAssignmentGroupsMutation(settings.canvasId); const applyInCanvas = useSetAssignmentGroupsMutation(settings.canvasId);
const modal = useModal();
const [assignmentGroups, setAssignmentGroups] = useState< const [assignmentGroups, setAssignmentGroups] = useState<
LocalAssignmentGroup[] LocalAssignmentGroup[]
@@ -106,46 +104,17 @@ export default function AssignmentGroupManagement() {
</div> </div>
<br /> <br />
<div className="flex justify-end"> <div className="flex justify-end">
<Modal <button
modalControl={modal} onClick={async () => {
buttonText="Update Assignment Groups In Canvas" const newSettings = await applyInCanvas.mutateAsync(settings);
buttonClass="btn-"
modalWidth="w-1/5" // prevent debounce from resetting
if (newSettings) setAssignmentGroups(newSettings.assignmentGroups);
}}
disabled={applyInCanvas.isPending}
> >
{({ closeModal }) => ( Update Assignment Groups In Canvas
<div> </button>
<div className="text-center font-bold">
DANGER: updating assignment groups can delete assignments and grades from canvas.
</div>
<div className="text-center">
This is only recommended to do at the beginning of a semester. Are you sure you want to continue?
</div>
<br />
<div className="flex justify-around gap-3">
<button
onClick={async () => {
const newSettings = await applyInCanvas.mutateAsync(
settings
);
// prevent debounce from resetting
if (newSettings)
setAssignmentGroups(newSettings.assignmentGroups);
}}
disabled={applyInCanvas.isPending}
className="btn-danger"
>
Yes
</button>
<button onClick={closeModal} disabled={applyInCanvas.isPending}>
No
</button>
</div>
{applyInCanvas.isPending && <Spinner />}
</div>
)}
</Modal>
</div> </div>
{applyInCanvas.isPending && <Spinner />} {applyInCanvas.isPending && <Spinner />}
{applyInCanvas.isSuccess && ( {applyInCanvas.isSuccess && (

View File

@@ -4,7 +4,7 @@ import { Spinner } from "@/components/Spinner";
import { import {
useLocalCourseSettingsQuery, useLocalCourseSettingsQuery,
useUpdateLocalCourseSettingsMutation, useUpdateLocalCourseSettingsMutation,
} from "@/features/local/course/localCoursesHooks"; } from "@/hooks/localCourse/localCoursesHooks";
import React from "react"; import React from "react";
export default function DaysOfWeekSettings() { export default function DaysOfWeekSettings() {

View File

@@ -3,7 +3,7 @@
import { import {
useLocalCourseSettingsQuery, useLocalCourseSettingsQuery,
useUpdateLocalCourseSettingsMutation, useUpdateLocalCourseSettingsMutation,
} from "@/features/local/course/localCoursesHooks"; } from "@/hooks/localCourse/localCoursesHooks";
import { TimePicker } from "../../../../components/TimePicker"; import { TimePicker } from "../../../../components/TimePicker";
import { useState } from "react"; import { useState } from "react";
import DefaultLockOffset from "./DefaultLockOffset"; import DefaultLockOffset from "./DefaultLockOffset";

View File

@@ -3,7 +3,7 @@ import TextInput from "@/components/form/TextInput";
import { import {
useLocalCourseSettingsQuery, useLocalCourseSettingsQuery,
useUpdateLocalCourseSettingsMutation, useUpdateLocalCourseSettingsMutation,
} from "@/features/local/course/localCoursesHooks"; } from "@/hooks/localCourse/localCoursesHooks";
import { useState, useEffect } from "react"; import { useState, useEffect } from "react";
import { settingsBox } from "./sharedSettings"; import { settingsBox } from "./sharedSettings";

View File

@@ -4,7 +4,7 @@ import TextInput from "@/components/form/TextInput";
import { import {
useLocalCourseSettingsQuery, useLocalCourseSettingsQuery,
useUpdateLocalCourseSettingsMutation, useUpdateLocalCourseSettingsMutation,
} from "@/features/local/course/localCoursesHooks"; } from "@/hooks/localCourse/localCoursesHooks";
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
export default function DefaultLockOffset() { export default function DefaultLockOffset() {

View File

@@ -1,8 +1,8 @@
"use client"; "use client";
import { useLocalCourseSettingsQuery } from "@/features/local/course/localCoursesHooks"; import { useLocalCourseSettingsQuery } from "@/hooks/localCourse/localCoursesHooks";
import { settingsBox } from "./sharedSettings"; import { settingsBox } from "./sharedSettings";
import { useCourseStudentsQuery } from "@/hooks/canvas/canvasCourseHooks";
import { Spinner } from "@/components/Spinner"; import { Spinner } from "@/components/Spinner";
import { useCourseStudentsQuery } from "@/features/canvas/hooks/canvasCourseHooks";
export default function GithubClassroomList() { export default function GithubClassroomList() {
const { data: settings } = useLocalCourseSettingsQuery(); const { data: settings } = useLocalCourseSettingsQuery();

View File

@@ -5,13 +5,13 @@ import { SuspenseAndErrorHandling } from "@/components/SuspenseAndErrorHandling"
import { import {
useLocalCourseSettingsQuery, useLocalCourseSettingsQuery,
useUpdateLocalCourseSettingsMutation, useUpdateLocalCourseSettingsMutation,
} from "@/features/local/course/localCoursesHooks"; } from "@/hooks/localCourse/localCoursesHooks";
import { getDateFromString } from "@/features/local/utils/timeUtils"; import { getDateFromString } from "@/models/local/utils/timeUtils";
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import { import {
holidaysToString, holidaysToString,
parseHolidays, parseHolidays,
} from "../../../../features/local/utils/settingsUtils"; } from "../../../../models/local/utils/settingsUtils";
import { settingsBox } from "./sharedSettings"; import { settingsBox } from "./sharedSettings";
const exampleString = `springBreak: const exampleString = `springBreak:

View File

@@ -1,5 +1,5 @@
"use client"; "use client";
import { useLocalCourseSettingsQuery } from "@/features/local/course/localCoursesHooks"; import { useLocalCourseSettingsQuery } from "@/hooks/localCourse/localCoursesHooks";
import { getCourseUrl } from "@/services/urlUtils"; import { getCourseUrl } from "@/services/urlUtils";
import Link from "next/link"; import Link from "next/link";
import React from "react"; import React from "react";

View File

@@ -1,6 +1,6 @@
"use client"; "use client";
import { useLocalCourseSettingsQuery } from "@/features/local/course/localCoursesHooks"; import { useLocalCourseSettingsQuery } from "@/hooks/localCourse/localCoursesHooks";
import { getDateOnlyMarkdownString } from "@/features/local/utils/timeUtils"; import { getDateOnlyMarkdownString } from "@/models/local/utils/timeUtils";
import React from "react"; import React from "react";
import { settingsBox } from "./sharedSettings"; import { settingsBox } from "./sharedSettings";

View File

@@ -3,11 +3,11 @@ import SelectInput from "@/components/form/SelectInput";
import { import {
useLocalCourseSettingsQuery, useLocalCourseSettingsQuery,
useUpdateLocalCourseSettingsMutation, useUpdateLocalCourseSettingsMutation,
} from "@/features/local/course/localCoursesHooks"; } from "@/hooks/localCourse/localCoursesHooks";
import { import {
AssignmentSubmissionType, AssignmentSubmissionType,
AssignmentSubmissionTypeList, AssignmentSubmissionTypeList,
} from "@/features/local/assignments/models/assignmentSubmissionType"; } from "@/models/local/assignment/assignmentSubmissionType";
import React, { useEffect, useState } from "react"; import React, { useEffect, useState } from "react";
import { settingsBox } from "./sharedSettings"; import { settingsBox } from "./sharedSettings";

View File

@@ -1,10 +1,8 @@
import React, { useState } from "react"; import React, { useState } from "react";
import { useCanvasTabsQuery } from "@/hooks/canvas/canvasNavigationHooks";
import { useUpdateCanvasTabMutation } from "@/hooks/canvas/canvasNavigationHooks";
import { Spinner } from "@/components/Spinner"; import { Spinner } from "@/components/Spinner";
import { NavTabListItem } from "./NavTabListItem"; import { NavTabListItem } from "./NavTabListItem";
import {
useCanvasTabsQuery,
useUpdateCanvasTabMutation,
} from "@/features/canvas/hooks/canvasNavigationHooks";
export const CanvasNavigationManagement = () => { export const CanvasNavigationManagement = () => {
const { data: tabs, isLoading, isError } = useCanvasTabsQuery(); const { data: tabs, isLoading, isError } = useCanvasTabsQuery();

View File

@@ -1,6 +1,6 @@
import { Spinner } from "@/components/Spinner"; import { Spinner } from "@/components/Spinner";
import { useUpdateCanvasTabMutation } from "@/features/canvas/hooks/canvasNavigationHooks"; import { useUpdateCanvasTabMutation } from "@/hooks/canvas/canvasNavigationHooks";
import { CanvasCourseTab } from "@/features/canvas/services/canvasNavigationService"; import { CanvasCourseTab } from "@/services/canvas/canvasNavigationService";
import React, { FC } from "react"; import React, { FC } from "react";
export const NavTabListItem: FC<{ export const NavTabListItem: FC<{

View File

@@ -8,9 +8,8 @@ export async function generateMetadata({
params: Promise<{ courseName: string }>; params: Promise<{ courseName: string }>;
}): Promise<Metadata> { }): Promise<Metadata> {
const { courseName } = await params; const { courseName } = await params;
const decodedCourseName = decodeURIComponent(courseName);
return { return {
title: getTitle(decodedCourseName) + " Settings", title: getTitle(courseName) + " Settings",
}; };
} }

View File

@@ -76,6 +76,83 @@ code {
@apply text-wrap; @apply text-wrap;
} }
/* Syntax highlighting styles for code blocks */
pre code {
@apply block bg-transparent p-0 m-0 rounded-none;
}
pre {
@apply bg-gray-900 p-4 rounded-lg overflow-x-auto font-mono text-sm leading-relaxed;
}
/* Prism.js syntax highlighting styles */
.token.comment,
.token.prolog,
.token.doctype,
.token.cdata {
@apply text-gray-500;
}
.token.punctuation {
@apply text-gray-400;
}
.token.property,
.token.tag,
.token.constant,
.token.symbol,
.token.deleted {
@apply text-red-400;
}
.token.boolean,
.token.number {
@apply text-orange-400;
}
.token.selector,
.token.attr-name,
.token.string,
.token.char,
.token.builtin,
.token.inserted {
@apply text-green-400;
}
.token.operator,
.token.entity,
.token.url,
.language-css .token.string,
.style .token.string,
.token.variable {
@apply text-yellow-400;
}
.token.atrule,
.token.attr-value,
.token.function,
.token.class-name {
@apply text-blue-400;
}
.token.keyword {
@apply text-purple-400;
}
.token.regex,
.token.important {
@apply text-orange-400;
}
.token.important,
.token.bold {
@apply font-bold;
}
.token.italic {
@apply italic;
}
p { p {
@apply mb-3; @apply mb-3;
} }

View File

@@ -2,10 +2,15 @@ import type { Metadata } from "next";
import "./globals.css"; import "./globals.css";
import Providers from "./providers"; import Providers from "./providers";
import { Suspense } from "react"; import { Suspense } from "react";
import { dehydrate, HydrationBoundary } from "@tanstack/react-query";
import { MyToaster } from "./MyToaster"; import { MyToaster } from "./MyToaster";
import { createServerSideHelpers } from "@trpc/react-query/server";
import { trpcAppRouter } from "@/services/serverFunctions/router/app";
import { createTrpcContext } from "@/services/serverFunctions/context";
import superjson from "superjson";
import { fileStorageService } from "@/services/fileStorage/fileStorageService";
import { ClientCacheInvalidation } from "../components/realtime/ClientCacheInvalidation"; import { ClientCacheInvalidation } from "../components/realtime/ClientCacheInvalidation";
import { getTitle } from "@/services/titleUtils"; import { getTitle } from "@/services/titleUtils";
import DataHydration from "./DataHydration";
export const dynamic = "force-dynamic"; export const dynamic = "force-dynamic";
export const metadata: Metadata = { export const metadata: Metadata = {
@@ -20,7 +25,7 @@ export default async function RootLayout({
return ( return (
<html lang="en"> <html lang="en">
<head></head> <head></head>
<body className="flex justify-center h-screen" suppressHydrationWarning> <body className="flex justify-center">
<div className="bg-gray-950 h-screen text-slate-300 w-screen sm:p-1"> <div className="bg-gray-950 h-screen text-slate-300 w-screen sm:p-1">
<MyToaster /> <MyToaster />
<Suspense> <Suspense>
@@ -36,3 +41,77 @@ export default async function RootLayout({
</html> </html>
); );
} }
async function DataHydration({
children,
}: Readonly<{
children: React.ReactNode;
}>) {
console.log("starting hydration");
const trpcHelper = createServerSideHelpers({
router: trpcAppRouter,
ctx: createTrpcContext(),
transformer: superjson,
queryClientConfig: {
defaultOptions: {
queries: {
staleTime: Infinity,
},
},
},
});
const allSettings = await fileStorageService.settings.getAllCoursesSettings();
await Promise.all(
allSettings.map(async (settings) => {
const courseName = settings.name;
const moduleNames = await trpcHelper.module.getModuleNames.fetch({
courseName,
});
await Promise.all([
// assignments
...moduleNames.map(
async (moduleName) =>
await trpcHelper.assignment.getAllAssignments.prefetch({
courseName,
moduleName,
})
),
// quizzes
...moduleNames.map(
async (moduleName) =>
await trpcHelper.quiz.getAllQuizzes.prefetch({
courseName,
moduleName,
})
),
// pages
...moduleNames.map(
async (moduleName) =>
await trpcHelper.page.getAllPages.prefetch({
courseName,
moduleName,
})
),
]);
})
);
// lectures
await Promise.all(
allSettings.map(
async (settings) =>
await trpcHelper.lectures.getLectures.fetch({
courseName: settings.name,
})
)
);
const dehydratedState = dehydrate(trpcHelper.queryClient);
console.log("ran hydration");
return (
<HydrationBoundary state={dehydratedState}>{children}</HydrationBoundary>
);
}

View File

@@ -1,27 +1,24 @@
"use client"; "use client";
import React, { useState } from "react"; import React, { useState } from "react";
import { SuspenseAndErrorHandling } from "@/components/SuspenseAndErrorHandling"; import { SuspenseAndErrorHandling } from "@/components/SuspenseAndErrorHandling";
import AddNewCourseToGlobalSettingsForm from "./AddCourseToGlobalSettingsForm"; import NewCourseForm from "./NewCourseForm";
import ClientOnly from "@/components/ClientOnly"; import ClientOnly from "@/components/ClientOnly";
export default function AddCourseToGlobalSettings() { export default function AddNewCourse() {
const [showForm, setShowForm] = useState(false); const [showForm, setShowForm] = useState(false);
return ( return (
<div> <div>
<div className="flex justify-center"> <div className="flex justify-center">
<button className="" onClick={() => setShowForm((i) => !i)}> <button className="" onClick={() => setShowForm(true)}>
{showForm ? "Hide Form" : "Add New Course"} Add New Course
</button> </button>
</div> </div>
<div className={" collapsible " + (showForm && "expand")}> <div className={" collapsible " + (showForm && "expand")}>
<div className="border rounded-md p-3 m-3"> <div className="border rounded-md p-3 m-3">
<SuspenseAndErrorHandling> <SuspenseAndErrorHandling>
<ClientOnly> <ClientOnly>{showForm && <NewCourseForm />}</ClientOnly>
{showForm && <AddNewCourseToGlobalSettingsForm />}
</ClientOnly>
</SuspenseAndErrorHandling> </SuspenseAndErrorHandling>
</div> </div>
</div> </div>

View File

@@ -1,30 +1,26 @@
"use client"; "use client";
import ButtonSelect from "@/components/ButtonSelect";
import { DayOfWeekInput } from "@/components/form/DayOfWeekInput"; import { DayOfWeekInput } from "@/components/form/DayOfWeekInput";
import SelectInput from "@/components/form/SelectInput"; import SelectInput from "@/components/form/SelectInput";
import { StoragePathSelector } from "@/components/form/StoragePathSelector";
import TextInput from "@/components/form/TextInput";
import { Spinner } from "@/components/Spinner"; import { Spinner } from "@/components/Spinner";
import { SuspenseAndErrorHandling } from "@/components/SuspenseAndErrorHandling"; import { SuspenseAndErrorHandling } from "@/components/SuspenseAndErrorHandling";
import { useCourseListInTermQuery } from "@/hooks/canvas/canvasCourseHooks";
import { useCanvasTermsQuery } from "@/hooks/canvas/canvasHooks";
import { import {
useCreateLocalCourseMutation, useCreateLocalCourseMutation,
useLocalCoursesSettingsQuery, useLocalCoursesSettingsQuery,
} from "@/features/local/course/localCoursesHooks"; } from "@/hooks/localCourse/localCoursesHooks";
import { CanvasCourseModel } from "@/features/canvas/models/courses/canvasCourseModel"; import { useEmptyDirectoriesQuery } from "@/hooks/localCourse/storageDirectoryHooks";
import { CanvasEnrollmentTermModel } from "@/features/canvas/models/enrollmentTerms/canvasEnrollmentTermModel"; import { CanvasCourseModel } from "@/models/canvas/courses/canvasCourseModel";
import { AssignmentSubmissionType } from "@/features/local/assignments/models/assignmentSubmissionType"; import { CanvasEnrollmentTermModel } from "@/models/canvas/enrollmentTerms/canvasEnrollmentTermModel";
import { getCourseUrl } from "@/services/urlUtils"; import { AssignmentSubmissionType } from "@/models/local/assignment/assignmentSubmissionType";
import { useRouter } from "next/navigation";
import React, { Dispatch, SetStateAction, useMemo, useState } from "react";
import { import {
DayOfWeek, DayOfWeek,
LocalCourseSettings, LocalCourseSettings,
} from "@/features/local/course/localCourseSettings"; } from "@/models/local/localCourseSettings";
import { useCourseListInTermQuery } from "@/features/canvas/hooks/canvasCourseHooks"; import { getCourseUrl } from "@/services/urlUtils";
import { useCanvasTermsQuery } from "@/features/canvas/hooks/canvasHooks"; import { useRouter } from "next/navigation";
import { useDirectoryExistsQuery } from "@/features/local/utils/storageDirectoryHooks"; import React, { useMemo, useState } from "react";
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const sampleCompose = `services: const sampleCompose = `services:
canvas_manager: canvas_manager:
image: alexmickelson/canvas_management:2 # pull this image regularly image: alexmickelson/canvas_management:2 # pull this image regularly
@@ -41,7 +37,7 @@ const sampleCompose = `services:
- ~/projects/faculty/4850_AdvancedFE/2024-fall-alex/modules:/app/storage/advanced_frontend - ~/projects/faculty/4850_AdvancedFE/2024-fall-alex/modules:/app/storage/advanced_frontend
`; `;
export default function AddNewCourseToGlobalSettingsForm() { export default function NewCourseForm() {
const router = useRouter(); const router = useRouter();
const today = useMemo(() => new Date(), []); const today = useMemo(() => new Date(), []);
const { data: canvasTerms } = useCanvasTermsQuery(today); const { data: canvasTerms } = useCanvasTermsQuery(today);
@@ -58,7 +54,6 @@ export default function AddNewCourseToGlobalSettingsForm() {
const [courseToImport, setCourseToImport] = useState< const [courseToImport, setCourseToImport] = useState<
LocalCourseSettings | undefined LocalCourseSettings | undefined
>(); >();
const [name, setName] = useState("");
const createCourse = useCreateLocalCourseMutation(); const createCourse = useCreateLocalCourseMutation();
const formIsComplete = const formIsComplete =
@@ -66,13 +61,12 @@ export default function AddNewCourseToGlobalSettingsForm() {
return ( return (
<div> <div>
<ButtonSelect <SelectInput
options={canvasTerms}
getOptionName={(t) => t?.name ?? ""}
setValue={setSelectedTerm}
value={selectedTerm} value={selectedTerm}
setValue={setSelectedTerm}
label={"Canvas Term"} label={"Canvas Term"}
center={true} options={canvasTerms}
getOptionName={(t) => t.name}
/> />
<SuspenseAndErrorHandling> <SuspenseAndErrorHandling>
{selectedTerm && ( {selectedTerm && (
@@ -86,8 +80,6 @@ export default function AddNewCourseToGlobalSettingsForm() {
setSelectedDaysOfWeek={setSelectedDaysOfWeek} setSelectedDaysOfWeek={setSelectedDaysOfWeek}
courseToImport={courseToImport} courseToImport={courseToImport}
setCourseToImport={setCourseToImport} setCourseToImport={setCourseToImport}
name={name}
setName={setName}
/> />
)} )}
</SuspenseAndErrorHandling> </SuspenseAndErrorHandling>
@@ -96,16 +88,10 @@ export default function AddNewCourseToGlobalSettingsForm() {
disabled={!formIsComplete || createCourse.isPending} disabled={!formIsComplete || createCourse.isPending}
onClick={async () => { onClick={async () => {
if (formIsComplete) { if (formIsComplete) {
console.log(
"Creating course with settings:",
selectedDirectory,
"old course",
courseToImport
);
const newSettings: LocalCourseSettings = courseToImport const newSettings: LocalCourseSettings = courseToImport
? { ? {
...courseToImport, ...courseToImport,
name: name, name: selectedDirectory,
daysOfWeek: selectedDaysOfWeek, daysOfWeek: selectedDaysOfWeek,
canvasId: selectedCanvasCourse.id, canvasId: selectedCanvasCourse.id,
startDate: selectedTerm.start_at ?? "", startDate: selectedTerm.start_at ?? "",
@@ -121,7 +107,7 @@ export default function AddNewCourseToGlobalSettingsForm() {
assets: [], assets: [],
} }
: { : {
name: name, name: selectedDirectory,
assignmentGroups: [], assignmentGroups: [],
daysOfWeek: selectedDaysOfWeek, daysOfWeek: selectedDaysOfWeek,
canvasId: selectedCanvasCourse.id, canvasId: selectedCanvasCourse.id,
@@ -140,10 +126,8 @@ export default function AddNewCourseToGlobalSettingsForm() {
await createCourse.mutateAsync({ await createCourse.mutateAsync({
settings: newSettings, settings: newSettings,
settingsFromCourseToImport: courseToImport, settingsFromCourseToImport: courseToImport,
name,
directory: selectedDirectory,
}); });
router.push(getCourseUrl(name)); router.push(getCourseUrl(selectedDirectory));
} }
}} }}
> >
@@ -151,6 +135,11 @@ export default function AddNewCourseToGlobalSettingsForm() {
</button> </button>
</div> </div>
{createCourse.isPending && <Spinner />} {createCourse.isPending && <Spinner />}
<pre>
<div>Example docker compose</div>
<code className="language-yml">{sampleCompose}</code>
</pre>
</div> </div>
); );
} }
@@ -159,35 +148,32 @@ function OtherSettings({
selectedTerm, selectedTerm,
selectedCanvasCourse, selectedCanvasCourse,
setSelectedCanvasCourse, setSelectedCanvasCourse,
selectedDirectory: _, selectedDirectory,
setSelectedDirectory, setSelectedDirectory,
selectedDaysOfWeek, selectedDaysOfWeek,
setSelectedDaysOfWeek, setSelectedDaysOfWeek,
courseToImport, courseToImport,
setCourseToImport, setCourseToImport,
name,
setName,
}: { }: {
selectedTerm: CanvasEnrollmentTermModel; selectedTerm: CanvasEnrollmentTermModel;
selectedCanvasCourse: CanvasCourseModel | undefined; selectedCanvasCourse: CanvasCourseModel | undefined;
setSelectedCanvasCourse: Dispatch< setSelectedCanvasCourse: React.Dispatch<
SetStateAction<CanvasCourseModel | undefined> React.SetStateAction<CanvasCourseModel | undefined>
>; >;
selectedDirectory: string | undefined; selectedDirectory: string | undefined;
setSelectedDirectory: Dispatch<SetStateAction<string | undefined>>; setSelectedDirectory: React.Dispatch<
React.SetStateAction<string | undefined>
>;
selectedDaysOfWeek: DayOfWeek[]; selectedDaysOfWeek: DayOfWeek[];
setSelectedDaysOfWeek: Dispatch<SetStateAction<DayOfWeek[]>>; setSelectedDaysOfWeek: React.Dispatch<React.SetStateAction<DayOfWeek[]>>;
courseToImport: LocalCourseSettings | undefined; courseToImport: LocalCourseSettings | undefined;
setCourseToImport: Dispatch<SetStateAction<LocalCourseSettings | undefined>>; setCourseToImport: React.Dispatch<
name: string; React.SetStateAction<LocalCourseSettings | undefined>
setName: Dispatch<SetStateAction<string>>; >;
}) { }) {
const { data: canvasCourses, isLoading: canvasCoursesLoading } = const { data: canvasCourses } = useCourseListInTermQuery(selectedTerm.id);
useCourseListInTermQuery(selectedTerm.id);
const { data: allSettings } = useLocalCoursesSettingsQuery(); const { data: allSettings } = useLocalCoursesSettingsQuery();
const [directory, setDirectory] = useState("./"); const { data: emptyDirectories } = useEmptyDirectoriesQuery();
const { data: directoryExists, isLoading: directoryExistsLoading } =
useDirectoryExistsQuery(directory);
const populatedCanvasCourseIds = allSettings?.map((s) => s.canvasId) ?? []; const populatedCanvasCourseIds = allSettings?.map((s) => s.canvasId) ?? [];
const availableCourses = const availableCourses =
@@ -198,43 +184,24 @@ function OtherSettings({
return ( return (
<> <>
<ButtonSelect <SelectInput
value={selectedCanvasCourse} value={selectedCanvasCourse}
setValue={setSelectedCanvasCourse} setValue={setSelectedCanvasCourse}
label={"Course"} label={"Course"}
options={availableCourses} options={availableCourses}
getOptionName={(c) => c?.name ?? ""} getOptionName={(c) => c.name}
center={true}
/> />
{canvasCoursesLoading && <Spinner />} <SelectInput
{!canvasCoursesLoading && availableCourses.length === 0 && ( value={selectedDirectory}
<div className="text-center text-red-300"> setValue={setSelectedDirectory}
<div className="flex justify-center ">
<div className="text-left">
No available courses in this term to add. Either
<ol>
<li>all courses have already been added, or</li>
<li>there are no courses in this term</li>
</ol>
</div>
</div>
</div>
)}
<StoragePathSelector
value={directory}
setValue={setDirectory}
setLastTypedValue={setSelectedDirectory}
label={"Storage Folder"} label={"Storage Folder"}
options={emptyDirectories ?? []}
getOptionName={(d) => d}
emptyOptionText="--- add a new folder to your docker compose to add more folders ---"
/> />
<div className="text-center mt-2 min-h-6"> <div className="px-5">
{directoryExistsLoading && <Spinner />} New folders will not be created automatically, you are expected to mount
{!directoryExistsLoading && directoryExists && ( a docker volume for each courses.
<div className="text-red-300">Directory must be a new folder</div>
)}
{!directoryExistsLoading && directoryExists === false && (
<div className="text-green-300"> New folder</div>
)}
</div> </div>
<br /> <br />
<div className="flex justify-center"> <div className="flex justify-center">
@@ -258,7 +225,6 @@ function OtherSettings({
options={allSettings} options={allSettings}
getOptionName={(c) => c.name} getOptionName={(c) => c.name}
/> />
<TextInput value={name} setValue={setName} label={"Display Name"} />
<div className="px-5"> <div className="px-5">
Assignments, Quizzes, Pages, and Lectures will have their due dates Assignments, Quizzes, Pages, and Lectures will have their due dates
moved based on how far they are from the start of the semester. moved based on how far they are from the start of the semester.

View File

@@ -1,6 +1,5 @@
import CourseList from "./CourseList"; import CourseList from "./CourseList";
import { AddExistingCourseToGlobalSettings } from "./addCourse/AddExistingCourseToGlobalSettings"; import AddNewCourse from "./newCourse/AddNewCourse";
import AddCourseToGlobalSettings from "./addCourse/AddNewCourse";
import TodaysLectures from "./todaysLectures/TodaysLectures"; import TodaysLectures from "./todaysLectures/TodaysLectures";
export default async function Home() { export default async function Home() {
@@ -19,31 +18,7 @@ export default async function Home() {
<TodaysLectures /> <TodaysLectures />
<br /> <br />
<br /> <br />
<AddCourseToGlobalSettings /> <AddNewCourse />
<br />
<div className="mb-96">
<AddExistingCourseToGlobalSettings />
<br />
<br />
<br />
<br />
<br />
<br />
<br />
<br />
<br />
<br />
<br />
<br />
<br />
<br />
<br />
<br />
<br />
<br />
<br />
<br />
</div>
</div> </div>
</main> </main>
); );

View File

@@ -17,7 +17,6 @@ export function makeQueryClient() {
// refetchOnMount: false, // refetchOnMount: false,
}, },
mutations: { mutations: {
retry: 0,
onError: (error) => { onError: (error) => {
const message = getAxiosErrorMessage(error as AxiosError); const message = getAxiosErrorMessage(error as AxiosError);
console.error("Mutation error:", message); console.error("Mutation error:", message);

View File

@@ -3,13 +3,13 @@
import { getLecturePreviewUrl } from "@/services/urlUtils"; import { getLecturePreviewUrl } from "@/services/urlUtils";
import Link from "next/link"; import Link from "next/link";
import { useCourseContext } from "../course/[courseName]/context/courseContext"; import { useCourseContext } from "../course/[courseName]/context/courseContext";
import { useLecturesSuspenseQuery as useLecturesQuery } from "@/features/local/lectures/lectureHooks"; import { useLecturesSuspenseQuery as useLecturesQuery } from "@/hooks/localCourse/lectureHooks";
import { getLectureForDay } from "@/features/local/utils/lectureUtils"; import { getLectureForDay } from "@/models/local/utils/lectureUtils";
import { getDateOnlyMarkdownString } from "@/features/local/utils/timeUtils"; import { getDateOnlyMarkdownString } from "@/models/local/utils/timeUtils";
export default function OneCourseLectures() { export default function OneCourseLectures() {
const { courseName } = useCourseContext(); const { courseName } = useCourseContext();
const { data: weeks } = useLecturesQuery(); const {data: weeks} = useLecturesQuery();
const dayAsDate = new Date(); const dayAsDate = new Date();
const dayAsString = getDateOnlyMarkdownString(dayAsDate); const dayAsString = getDateOnlyMarkdownString(dayAsDate);

Some files were not shown because too many files have changed in this diff Show More