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
240 changed files with 12219 additions and 3630 deletions

View File

@@ -1,31 +0,0 @@
name: Deploy to Docker Hub
on:
push:
branches: [ main, development, staging ]
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: Extract branch name
shell: bash
run: echo "BRANCH_NAME=${GITHUB_REF#refs/heads/}" >> $GITHUB_ENV
- name: Build and push Docker image
run: |
chmod +x ./build.sh
./build.sh -t -p -b "$BRANCH_NAME"

View File

@@ -4,40 +4,12 @@
<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:
```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: []
```
<!-- draft -->
## 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.

121
build.sh
View File

@@ -1,14 +1,13 @@
#!/bin/bash
MAJOR_VERSION="3"
MINOR_VERSION="0"
MAJOR_VERSION="2"
MINOR_VERSION="8"
VERSION="$MAJOR_VERSION.$MINOR_VERSION"
BRANCH=""
TAG_FLAG=false
PUSH_FLAG=false
while getopts ":tpb:" opt; do
while getopts ":tp" opt; do
case ${opt} in
t)
TAG_FLAG=true
@@ -16,12 +15,9 @@ while getopts ":tpb:" opt; do
p)
PUSH_FLAG=true
;;
b)
BRANCH="$OPTARG"
;;
\?)
echo "Invalid option: -$OPTARG" >&2
echo "Usage: $0 [-t] [-p] [-b branch]"
echo "Usage: $0 [-t] [-p]"
exit 1
;;
esac
@@ -33,72 +29,24 @@ docker build -t canvas_management:$VERSION .
if [ "$TAG_FLAG" = true ]; then
echo "Tagging images..."
if [ -n "$BRANCH" ]; then
# Branch-specific tags
echo "alexmickelson/canvas_management:$VERSION-$BRANCH"
echo "alexmickelson/canvas_management:$MAJOR_VERSION-$BRANCH"
echo "alexmickelson/canvas_management:latest-$BRANCH"
docker image tag canvas_management:"$VERSION" alexmickelson/canvas_management:"$VERSION-$BRANCH"
docker image tag canvas_management:"$VERSION" alexmickelson/canvas_management:"$MAJOR_VERSION-$BRANCH"
docker image tag canvas_management:"$VERSION" alexmickelson/canvas_management:latest-$BRANCH
# Only create non-branch tags if branch is "main"
if [ "$BRANCH" = "main" ]; then
echo "alexmickelson/canvas_management:$VERSION"
echo "alexmickelson/canvas_management:$MAJOR_VERSION"
echo "alexmickelson/canvas_management:latest"
docker image tag canvas_management:"$VERSION" alexmickelson/canvas_management:"$VERSION"
docker image tag canvas_management:"$VERSION" alexmickelson/canvas_management:"$MAJOR_VERSION"
docker image tag canvas_management:"$VERSION" alexmickelson/canvas_management:latest
fi
else
# No branch specified - create standard tags (for local development)
echo "alexmickelson/canvas_management:$VERSION"
echo "alexmickelson/canvas_management:$MAJOR_VERSION"
echo "alexmickelson/canvas_management:latest"
docker image tag canvas_management:"$VERSION" alexmickelson/canvas_management:"$VERSION"
docker image tag canvas_management:"$VERSION" alexmickelson/canvas_management:"$MAJOR_VERSION"
docker image tag canvas_management:"$VERSION" alexmickelson/canvas_management:latest
fi
echo "alexmickelson/canvas_management:$VERSION"
echo "alexmickelson/canvas_management:$MAJOR_VERSION"
echo "alexmickelson/canvas_management:latest"
docker image tag canvas_management:"$VERSION" alexmickelson/canvas_management:"$VERSION"
docker image tag canvas_management:"$VERSION" alexmickelson/canvas_management:"$MAJOR_VERSION"
docker image tag canvas_management:latest alexmickelson/canvas_management:latest
fi
if [ "$PUSH_FLAG" = true ]; then
echo "Pushing images..."
if [ -n "$BRANCH" ]; then
# Push branch-specific tags
echo "alexmickelson/canvas_management:$VERSION-$BRANCH"
echo "alexmickelson/canvas_management:$MAJOR_VERSION-$BRANCH"
echo "alexmickelson/canvas_management:latest-$BRANCH"
docker push alexmickelson/canvas_management:"$VERSION-$BRANCH"
docker push alexmickelson/canvas_management:"$MAJOR_VERSION-$BRANCH"
docker push alexmickelson/canvas_management:latest-$BRANCH
# Only push non-branch tags if branch is "main"
if [ "$BRANCH" = "main" ]; then
echo "alexmickelson/canvas_management:$VERSION"
echo "alexmickelson/canvas_management:$MAJOR_VERSION"
echo "alexmickelson/canvas_management:latest"
docker push alexmickelson/canvas_management:"$VERSION"
docker push alexmickelson/canvas_management:"$MAJOR_VERSION"
docker push alexmickelson/canvas_management:latest
fi
else
# No branch specified - push standard tags (for local development)
echo "alexmickelson/canvas_management:$VERSION"
echo "alexmickelson/canvas_management:$MAJOR_VERSION"
echo "alexmickelson/canvas_management:latest"
docker push alexmickelson/canvas_management:"$VERSION"
docker push alexmickelson/canvas_management:"$MAJOR_VERSION"
docker push alexmickelson/canvas_management:latest
fi
echo "alexmickelson/canvas_management:$VERSION"
echo "alexmickelson/canvas_management:$MAJOR_VERSION"
echo "alexmickelson/canvas_management:latest"
docker push alexmickelson/canvas_management:"$VERSION"
docker push alexmickelson/canvas_management:"$MAJOR_VERSION"
docker push alexmickelson/canvas_management:latest
fi
if [ "$TAG_FLAG" = false ] && [ "$PUSH_FLAG" = false ]; then
@@ -106,33 +54,12 @@ if [ "$TAG_FLAG" = false ] && [ "$PUSH_FLAG" = false ]; then
echo "Build complete."
echo "To tag, run with -t flag."
echo "To push, run with -p flag."
echo "To build for a specific branch, use -b branch_name flag."
echo "Or manually run:"
echo ""
if [ -n "$BRANCH" ]; then
echo "# Branch-specific tags:"
echo "docker image tag canvas_management:$VERSION alexmickelson/canvas_management:$VERSION-$BRANCH"
echo "docker image tag canvas_management:$VERSION alexmickelson/canvas_management:$MAJOR_VERSION-$BRANCH"
echo "docker image tag canvas_management:$VERSION alexmickelson/canvas_management:latest-$BRANCH"
echo "docker push alexmickelson/canvas_management:$VERSION-$BRANCH"
echo "docker push alexmickelson/canvas_management:$MAJOR_VERSION-$BRANCH"
echo "docker push alexmickelson/canvas_management:latest-$BRANCH"
if [ "$BRANCH" = "main" ]; then
echo ""
echo "# Main branch also gets standard tags:"
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:latest"
echo "docker push alexmickelson/canvas_management:$VERSION"
echo "docker push alexmickelson/canvas_management:$MAJOR_VERSION"
echo "docker push alexmickelson/canvas_management:latest"
fi
else
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:latest"
echo "docker push alexmickelson/canvas_management:$VERSION"
echo "docker push alexmickelson/canvas_management:$MAJOR_VERSION"
echo "docker push alexmickelson/canvas_management:latest"
fi
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:latest alexmickelson/canvas_management:latest"
echo "docker push alexmickelson/canvas_management:$VERSION"
echo "docker push alexmickelson/canvas_management:$MAJOR_VERSION"
echo "docker push alexmickelson/canvas_management:latest"
fi

View File

@@ -15,20 +15,17 @@ services:
- NEXT_PUBLIC_ENABLE_FILE_SYNC=true
- REDIS_URL=redis://redis:6379
volumes:
# - ./globalSettings.dev.yml:/app/globalSettings.yml
- ./globalSettings.yml:/app/globalSettings.yml
- .:/app
- ~/projects/faculty:/app/storage
# - ~/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/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
redis:
@@ -50,7 +47,7 @@ services:
--api-key "$MCP_TOKEN" \
--server-type "streamable_http" \
--cors-allow-origins "*" \
-- http://canvas-dev:3000/api/mcp/mcp/
-- http://canvas-dev:3000/api/mcp
'
working_dir: /app
ports:

View File

@@ -1,8 +1,8 @@
services:
canvas_manager:
image: alexmickelson/canvas_management:3
image: alexmickelson/canvas_management:2.7
user: "1000:1000"
container_name: canvas-manager
container_name: canvas-manager-2
ports:
- 3000:3000
env_file:
@@ -14,8 +14,30 @@ services:
- REDIS_URL=redis://redis:6379
# - FILE_POLLING=true
volumes:
- ./globalSettings.yml:/app/globalSettings.yml
- ~/projects/faculty:/app/storage
# - ~/projects/faculty/3840_Telemetry/2024Spring_alex/modules:/app/storage/spring_2024_telemetry
# - ~/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

View File

@@ -10,11 +10,13 @@ const compat = new FlatCompat({
});
const eslintConfig = [
{
ignores: ["**/node_modules/**", "**/.next/**", "storage/**"],
},
...compat.config({
extends: ["next/core-web-vitals", "next/typescript", "prettier"],
ignores: [
"**/node_modules/**",
"**/.next/**",
"storage/**"
],
rules: {
"react-refresh/only-export-components": "off", // Disabled the rule
"@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: ./4850_AdvancedFE/2025-fall-alex/modules/
name: Adv Frontend
- path: ./1420/2025-fall-alex/modules/
name: "1420"
- path: ./1810/2025-fall-alex/modules/
name: Web Intro
- path: ./1430/2025-fall-alex/modules/
name: UX
- path: ./1425/2025-fall-alex/modules/
name: "1425"
- path: ./1405/2025_spring_alex/
name: 1405_old
- path: ./3840_Telemetry/2025_spring_alex/modules/
name: Telem and Ops
- path: ./4850_AdvancedFE/2024-fall-alex/modules/
name: Old Adv Frontend
- path: ./1430/2025-spring-jonathan/Modules/
name: Jonathan UX
- path: ./1400/2025_spring_alex/modules/
name: 1400-spring
- path: ./1420/2024-fall/Modules/
name: 1420_old

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

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

@@ -1,10 +1,6 @@
"use client";
import { useLocalCoursesSettingsQuery } from "@/features/local/course/localCoursesHooks";
import {
getDateKey,
getTermName,
groupByStartDate,
} from "@/features/local/utils/timeUtils";
import { useLocalCoursesSettingsQuery } from "@/hooks/localCourse/localCoursesHooks";
import { getDateKey, getTermName, groupByStartDate } from "@/models/local/utils/timeUtils";
import { getCourseUrl } from "@/services/urlUtils";
import Link from "next/link";
@@ -15,8 +11,6 @@ export default function CourseList() {
const sortedDates = Object.keys(coursesByStartDate).sort();
console.log(allSettings, coursesByStartDate);
return (
<div className="flex flex-row ">
{sortedDates.map((startDate) => (
@@ -30,12 +24,11 @@ export default function CourseList() {
<Link
href={getCourseUrl(settings.name)}
shallow={true}
prefetch={true}
className="
font-bold text-xl block
transition-all hover:scale-105 hover:underline hover:text-slate-200
mb-3
"
font-bold text-xl block
transition-all hover:scale-105 hover:underline hover:text-slate-200
mb-3
"
>
{settings.name}
</Link>
@@ -45,4 +38,4 @@ export default function CourseList() {
))}
</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)}>
Add 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 { groupByStartDate } from "@/features/local/utils/timeUtils";
import { assignmentMarkdownSerializer } from "@/models/local/assignment/utils/assignmentMarkdownSerializer";
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 { 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(
(server) => {
@@ -43,17 +42,17 @@ const handler = createMcpHandler(
courseName: z.string(),
},
async ({ courseName }) => {
const modules = await getModuleNamesFromFiles(
const modules = await fileStorageService.modules.getModuleNames(
courseName
);
const assignments = (
await Promise.all(
modules.map(async (moduleName) => {
const assignments = await courseItemFileStorageService.getItems({
courseName,
moduleName,
type: "Assignment",
});
const assignments =
await fileStorageService.assignments.getAssignments(
courseName,
moduleName
);
return assignments.map((assignment) => ({
assignmentName: assignment.name,
moduleName,
@@ -102,12 +101,11 @@ const handler = createMcpHandler(
"courseName, moduleName, and assignmentName must be strings"
);
}
const assignment = await courseItemFileStorageService.getItem({
const assignment = await fileStorageService.assignments.getAssignment(
courseName,
moduleName,
name: assignmentName,
type: "Assignment",
});
assignmentName
);
console.log("mcp assignment", assignment);
return {
@@ -120,58 +118,42 @@ const handler = createMcpHandler(
};
}
);
server.tool(
"get_github_classroom_url_instructions",
"gets instructions for creating a GitHub Classroom assignment, call this to get a prompt showing how to create a GitHub Classroom assignment",
{},
async () => {
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 {
content: [
contents: [
{
type: "text",
text: githubClassroomUrlPrompt,
uri: uri.href,
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: {

View File

@@ -1,8 +1,10 @@
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";
const handler = async (request: Request) => {
// await new Promise(r => setTimeout(r, 1000)); // delay for testing
return fetchRequestHandler({
endpoint: "/api/trpc",
req: request,

View File

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

View File

@@ -1,23 +1,23 @@
"use client";
import { Spinner } from "@/components/Spinner";
import {
useCanvasAssignmentsQuery,
canvasAssignmentKeys,
} from "@/features/canvas/hooks/canvasAssignmentHooks";
import { canvasCourseKeys } from "@/features/canvas/hooks/canvasCourseHooks";
useCanvasAssignmentsQuery,
} from "@/hooks/canvas/canvasAssignmentHooks";
import { canvasCourseKeys } from "@/hooks/canvas/canvasCourseHooks";
import {
useCanvasModulesQuery,
canvasCourseModuleKeys,
} from "@/features/canvas/hooks/canvasModuleHooks";
useCanvasModulesQuery,
} from "@/hooks/canvas/canvasModuleHooks";
import {
useCanvasPagesQuery,
canvasPageKeys,
} from "@/features/canvas/hooks/canvasPageHooks";
useCanvasPagesQuery,
} from "@/hooks/canvas/canvasPageHooks";
import {
useCanvasQuizzesQuery,
canvasQuizKeys,
} from "@/features/canvas/hooks/canvasQuizHooks";
import { useLocalCourseSettingsQuery } from "@/features/local/course/localCoursesHooks";
useCanvasQuizzesQuery,
} from "@/hooks/canvas/canvasQuizHooks";
import { useLocalCourseSettingsQuery } from "@/hooks/localCourse/localCoursesHooks";
import { useQueryClient } from "@tanstack/react-query";
import Link from "next/link";
@@ -96,4 +96,4 @@ function getSemesterName(startDate: string) {
} else {
return `Fall ${year}`;
}
}
}

View File

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

View File

@@ -1,12 +1,12 @@
"use client";
import { CalendarMonthModel, getWeekNumber } from "./calendarMonthUtils";
import { DayOfWeek } from "@/models/local/localCourseSettings";
import { Expandable } from "@/components/Expandable";
import { CalendarWeek } from "./CalendarWeek";
import { useLocalCourseSettingsQuery } from "@/features/local/course/localCoursesHooks";
import { getDateFromStringOrThrow } from "@/features/local/utils/timeUtils";
import { useLocalCourseSettingsQuery } from "@/hooks/localCourse/localCoursesHooks";
import { getDateFromStringOrThrow } from "@/models/local/utils/timeUtils";
import UpChevron from "@/components/icons/UpChevron";
import DownChevron from "@/components/icons/DownChevron";
import { DayOfWeek } from "@/features/local/course/localCourseSettings";
export const CalendarMonth = ({ month }: { month: CalendarMonthModel }) => {
// const weekInMilliseconds = 604_800_000;
@@ -18,7 +18,7 @@ export const CalendarMonth = ({ month }: { month: CalendarMonthModel }) => {
);
const isPastSemester = Date.now() > new Date(settings.endDate).getTime();
const pastWeekNumber = getWeekNumber(
startDate,
new Date(Date.now() - four_days_in_milliseconds)
@@ -29,8 +29,7 @@ export const CalendarMonth = ({ month }: { month: CalendarMonthModel }) => {
new Date(month.year, month.month, 1)
);
const shouldCollapse =
pastWeekNumber >= startOfMonthWeekNumber && !isPastSemester;
const shouldCollapse = (pastWeekNumber >= startOfMonthWeekNumber) && !isPastSemester;
const monthName = new Date(month.year, month.month - 1, 1).toLocaleString(
"default",

View File

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

View File

@@ -1,8 +1,8 @@
"use client";
import { getDateFromStringOrThrow } from "@/features/local/utils/timeUtils";
import { getDateFromStringOrThrow } from "@/models/local/utils/timeUtils";
import { getMonthsBetweenDates } from "./calendarMonthUtils";
import { CalendarMonth } from "./CalendarMonth";
import { useLocalCourseSettingsQuery } from "@/features/local/course/localCoursesHooks";
import { useLocalCourseSettingsQuery } from "@/hooks/localCourse/localCoursesHooks";
import { useEffect, useMemo, useRef } from "react";
import CalendarItemsContextProvider from "../context/CalendarItemsContextProvider";
@@ -43,15 +43,15 @@ export default function CourseCalendar() {
return (
<div
className="
min-h-0
flex-grow
border-2
border-gray-900
rounded-lg
bg-linear-to-br
from-blue-950/30
to-fuchsia-950/10 to-60%
sm:p-1
min-h-0
flex-grow
border-2
border-gray-900
rounded-lg
bg-linear-to-br
from-blue-950/30
to-fuchsia-950/10 to-60%
sm:p-1
"
>
<div

View File

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

View File

@@ -2,13 +2,13 @@
import {
getDateFromStringOrThrow,
getDateOnlyMarkdownString,
} from "@/features/local/utils/timeUtils";
} from "@/models/local/utils/timeUtils";
import { useDraggingContext } from "../../context/drag/draggingContext";
import { useLocalCourseSettingsQuery } from "@/features/local/course/localCoursesHooks";
import { useLocalCourseSettingsQuery } from "@/hooks/localCourse/localCoursesHooks";
import { getDayOfWeek } from "@/models/local/localCourseSettings";
import { ItemInDay } from "./ItemInDay";
import { useTodaysItems } from "./useTodaysItems";
import { DayTitle } from "./DayTitle";
import { getDayOfWeek } from "@/features/local/course/localCourseSettings";
export default function Day({ day, month }: { day: string; month: number }) {
const dayAsDate = getDateFromStringOrThrow(

View File

@@ -5,8 +5,8 @@ import { useCourseContext } from "../../context/courseContext";
import NewItemForm from "../../modules/NewItemForm";
import { DraggableItem } from "../../context/drag/draggingContext";
import { useDragStyleContext } from "../../context/drag/dragStyleContext";
import { getLectureForDay } from "@/features/local/utils/lectureUtils";
import { useLecturesSuspenseQuery } from "@/features/local/lectures/lectureHooks";
import { getLectureForDay } from "@/models/local/utils/lectureUtils";
import { useLecturesSuspenseQuery } from "@/hooks/localCourse/lectureHooks";
import ClientOnly from "@/components/ClientOnly";
import { Tooltip } from "@/components/Tooltip";
import { useRef, useState } from "react";

View File

@@ -1,4 +1,4 @@
import { IModuleItem } from "@/features/local/modules/IModuleItem";
import { IModuleItem } from "@/models/local/IModuleItem";
import { getModuleItemUrl } from "@/services/urlUtils";
import Link from "next/link";
import { ReactNode, useRef, useState } from "react";

View File

@@ -1,18 +1,18 @@
"use client";
import { CanvasAssignment } from "@/features/canvas/models/assignments/canvasAssignment";
import { CanvasPage } from "@/features/canvas/models/pages/canvasPageModel";
import { CanvasQuiz } from "@/features/canvas/models/quizzes/canvasQuizModel";
import { LocalAssignment } from "@/features/local/assignments/models/localAssignment";
import { CanvasAssignment } from "@/models/canvas/assignments/canvasAssignment";
import { CanvasPage } from "@/models/canvas/pages/canvasPageModel";
import { CanvasQuiz } from "@/models/canvas/quizzes/canvasQuizModel";
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 {
dateToMarkdownString,
getDateFromStringOrThrow,
} from "@/features/local/utils/timeUtils";
} from "@/models/local/utils/timeUtils";
import { markdownToHTMLSafe } from "@/services/htmlMarkdownUtils";
import { htmlIsCloseEnough } from "@/services/utils/htmlIsCloseEnough";
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 = ({
item,
@@ -105,16 +105,7 @@ export const getStatus = ({
try {
const htmlIsSame = htmlIsCloseEnough(
markdownToHTMLSafe({
markdownString: assignment.description,
settings,
replaceText: [
{
source: "insert_github_classroom_url",
destination: assignment.githubClassroomAssignmentShareLink || "",
},
],
}),
markdownToHTMLSafe(assignment.description, settings),
canvasAssignment.description
);
if (!htmlIsSame)

View File

@@ -1,19 +1,18 @@
"use client";
import { LocalAssignment } from "@/features/local/assignments/models/localAssignment";
import { useCanvasAssignmentsQuery } from "@/hooks/canvas/canvasAssignmentHooks";
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 {
getDateFromStringOrThrow,
getDateOnlyMarkdownString,
} from "@/features/local/utils/timeUtils";
} from "@/models/local/utils/timeUtils";
import { ReactNode } from "react";
import { useCalendarItemsContext } from "../../context/calendarItemsContext";
import { getStatus } from "./getStatus";
import { useLocalCourseSettingsQuery } from "@/features/local/course/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";
import { useLocalCourseSettingsQuery } from "@/hooks/localCourse/localCoursesHooks";
export function useTodaysItems(day: string) {
const { data: settings } = useLocalCourseSettingsQuery();

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,13 +1,13 @@
"use client";
import { useUpdatePageMutation } from "@/features/local/pages/pageHooks";
import { LocalAssignment } from "@/features/local/assignments/models/localAssignment";
import { useUpdateAssignmentMutation } from "@/hooks/localCourse/assignmentHooks";
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 { DraggableItem } from "./draggingContext";
import { useCourseContext } from "../courseContext";
import { useUpdateQuizMutation } from "@/features/local/quizzes/quizHooks";
import { useUpdateAssignmentMutation } from "@/features/local/assignments/assignmentHooks";
import { LocalCoursePage } from "@/features/local/pages/localCoursePageModels";
import { LocalQuiz } from "@/features/local/quizzes/models/localQuiz";
import { useUpdateQuizMutation } from "@/hooks/localCourse/quizHooks";
export function useItemDropOnModule({
setIsDragging,

View File

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

View File

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

View File

@@ -1,10 +1,10 @@
import { useLocalCourseSettingsQuery } from "@/features/local/course/localCoursesHooks";
import { getDateFromString } from "@/features/local/utils/timeUtils";
import { getLectureWeekName } from "@/features/local/lectures/lectureUtils";
import { useLocalCourseSettingsQuery } from "@/hooks/localCourse/localCoursesHooks";
import { getDayOfWeek } from "@/models/local/localCourseSettings";
import { getDateFromString } from "@/models/local/utils/timeUtils";
import { getLectureWeekName } from "@/services/fileStorage/utils/lectureUtils";
import { getCourseUrl, getLecturePreviewUrl } from "@/services/urlUtils";
import { useCourseContext } from "../../context/courseContext";
import Link from "next/link";
import { getDayOfWeek } from "@/features/local/course/localCourseSettings";
export default function EditLectureTitle({
lectureDay,
@@ -22,7 +22,6 @@ export default function EditLectureTitle({
className="btn hidden sm:inline"
href={getCourseUrl(courseName)}
shallow={true}
prefetch={true}
>
{courseName}
</Link>

View File

@@ -6,8 +6,8 @@ import { getCourseUrl } from "@/services/urlUtils";
import { useRouter } from "next/navigation";
import { useState } from "react";
import { useCourseContext } from "../../context/courseContext";
import { useLocalCourseSettingsQuery } from "@/features/local/course/localCoursesHooks";
import { useDeleteLectureMutation } from "@/features/local/lectures/lectureHooks";
import { useLocalCourseSettingsQuery } from "@/hooks/localCourse/localCoursesHooks";
import { useDeleteLectureMutation } from "@/hooks/localCourse/lectureHooks";
import Link from "next/link";
export default function LectureButtons({ lectureDay }: { lectureDay: string }) {

View File

@@ -1,5 +1,5 @@
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 }) {
return (

View File

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

View File

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

View File

@@ -4,7 +4,7 @@ import LecturePreview from "../LecturePreview";
import { getCourseUrl, getLectureUrl } from "@/services/urlUtils";
import { useCourseContext } from "../../../context/courseContext";
import Link from "next/link";
import { useLecturesSuspenseQuery } from "@/features/local/lectures/lectureHooks";
import { useLecturesSuspenseQuery } from "@/hooks/localCourse/lectureHooks";
export default function LecturePreviewPage({
lectureDay,

View File

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

View File

@@ -1,6 +1,6 @@
import { Expandable } from "@/components/Expandable";
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 { useCourseContext } from "../context/courseContext";

View File

@@ -1,11 +1,11 @@
"use client";
import { usePagesQueries } from "@/features/local/pages/pageHooks";
import { IModuleItem } from "@/features/local/modules/IModuleItem";
import { usePagesQueries } from "@/hooks/localCourse/pageHooks";
import { IModuleItem } from "@/models/local/IModuleItem";
import {
getDateFromString,
getDateFromStringOrThrow,
getDateOnlyMarkdownString,
} from "@/features/local/utils/timeUtils";
} from "@/models/local/utils/timeUtils";
import { Fragment } from "react";
import Modal, { useModal } from "../../../../components/Modal";
import NewItemForm from "./NewItemForm";
@@ -21,13 +21,10 @@ import { getModuleItemUrl } from "@/services/urlUtils";
import { useCourseContext } from "../context/courseContext";
import { Expandable } from "../../../../components/Expandable";
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 { 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({
moduleName,
@@ -53,8 +50,6 @@ export default function ExpandableModule({
const { data: quizzes } = useQuizzesQueries(moduleName);
const { data: pages } = usePagesQueries(moduleName);
const modal = useModal();
const reorderMutation = useReorderCanvasModuleItemsMutation();
const { data: canvasModules } = useCanvasModulesQuery();
const moduleItems: {
type: "assignment" | "quiz" | "page";
@@ -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
modalControl={modal}
buttonText="New Item"

View File

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

View File

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

View File

@@ -3,20 +3,20 @@ import ButtonSelect from "@/components/ButtonSelect";
import SelectInput from "@/components/form/SelectInput";
import TextInput from "@/components/form/TextInput";
import { Spinner } from "@/components/Spinner";
import { useModuleNamesQuery } from "@/features/local/modules/localCourseModuleHooks";
import { useLocalCourseSettingsQuery } from "@/features/local/course/localCoursesHooks";
import { useCreatePageMutation } from "@/features/local/pages/pageHooks";
import { LocalAssignmentGroup } from "@/features/local/assignments/models/localAssignmentGroup";
import { useCreateAssignmentMutation } from "@/hooks/localCourse/assignmentHooks";
import { useModuleNamesQuery } from "@/hooks/localCourse/localCourseModuleHooks";
import { useLocalCourseSettingsQuery } from "@/hooks/localCourse/localCoursesHooks";
import { useCreatePageMutation } from "@/hooks/localCourse/pageHooks";
import { LocalAssignmentGroup } from "@/models/local/assignment/localAssignmentGroup";
import React, { useState } from "react";
import { useCourseContext } from "../context/courseContext";
import { useCreateQuizMutation } from "@/features/local/quizzes/quizHooks";
import { useCreateQuizMutation } from "@/hooks/localCourse/quizHooks";
import {
getDateFromString,
dateToMarkdownString,
getDateFromStringOrThrow,
} from "@/features/local/utils/timeUtils";
import { useCreateAssignmentMutation } from "@/features/local/assignments/assignmentHooks";
} from "@/models/local/utils/timeUtils";
export default function NewItemForm({
moduleName: defaultModuleName,
@@ -153,9 +153,9 @@ export default function NewItemForm({
<div>
<ButtonSelect<"Assignment" | "Quiz" | "Page">
options={["Assignment", "Quiz", "Page"]}
getOptionName={(o) => o?.toString() ?? ""}
setValue={(t) => setType(t ?? "Assignment")}
value={type}
getName={(o) => o?.toString() ?? ""}
setSelectedOption={(t) => setType(t ?? "Assignment")}
selectedOption={type}
label="Type"
/>
</div>
@@ -166,9 +166,9 @@ export default function NewItemForm({
{type !== "Page" && (
<ButtonSelect
options={settings.assignmentGroups}
getOptionName={(g) => g?.name ?? ""}
setValue={setAssignmentGroup}
value={assignmentGroup}
getName={(g) => g?.name ?? ""}
setSelectedOption={setAssignmentGroup}
selectedOption={assignmentGroup}
label="Assignment Group"
/>
)}

View File

@@ -6,13 +6,13 @@ import {
useAddAssignmentToCanvasMutation,
useDeleteAssignmentFromCanvasMutation,
useUpdateAssignmentInCanvasMutation,
} from "@/features/canvas/hooks/canvasAssignmentHooks";
import { baseCanvasUrl } from "@/features/canvas/services/canvasServiceUtils";
} from "@/hooks/canvas/canvasAssignmentHooks";
import {
useAssignmentQuery,
useDeleteAssignmentMutation,
} from "@/features/local/assignments/assignmentHooks";
import { useLocalCourseSettingsQuery } from "@/features/local/course/localCoursesHooks";
} from "@/hooks/localCourse/assignmentHooks";
import { useLocalCourseSettingsQuery } from "@/hooks/localCourse/localCoursesHooks";
import { baseCanvasUrl } from "@/services/canvas/canvasServiceUtils";
import { getCourseUrl } from "@/services/urlUtils";
import Link from "next/link";
import { useRouter } from "next/navigation";

View File

@@ -1,7 +1,7 @@
import MarkdownDisplay from "@/components/MarkdownDisplay";
import { LocalAssignment } from "@/features/local/assignments/models/localAssignment";
import { rubricItemIsExtraCredit } from "@/features/local/assignments/models/rubricItem";
import { assignmentPoints } from "@/features/local/assignments/models/utils/assignmentPointsUtils";
import { LocalAssignment } from "@/models/local/assignment/localAssignment";
import { rubricItemIsExtraCredit } from "@/models/local/assignment/rubricItem";
import { assignmentPoints } from "@/models/local/assignment/utils/assignmentPointsUtils";
import { formatHumanReadableDate } from "@/services/utils/dateFormat";
import React, { Fragment } from "react";
@@ -59,15 +59,13 @@ export default function AssignmentPreview({
<hr />
<br />
<section>
<MarkdownDisplay
markdown={assignment.description}
replaceText={[
{
source: "insert_github_classroom_url",
destination: assignment.githubClassroomAssignmentShareLink || "",
},
]}
/>
<MarkdownDisplay markdown={assignment.description} />
{/* <div
className="markdownPreview"
dangerouslySetInnerHTML={{
__html: htmlPreview,
}}
></div> */}
</section>
<hr />
<section>

View File

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

View File

@@ -13,12 +13,7 @@ export default function EditAssignmentHeader({
const { courseName } = useCourseContext();
return (
<div className="py-1 flex flex-row justify-start gap-3">
<Link
className="btn"
href={getCourseUrl(courseName)}
shallow={true}
prefetch={true}
>
<Link className="btn" href={getCourseUrl(courseName)} shallow={true}>
{courseName}
</Link>
<UpdateAssignmentName

View File

@@ -5,7 +5,7 @@ import { Spinner } from "@/components/Spinner";
import {
useAssignmentQuery,
useUpdateAssignmentMutation,
} from "@/features/local/assignments/assignmentHooks";
} from "@/hooks/localCourse/assignmentHooks";
import { getModuleItemUrl } from "@/services/urlUtils";
import { useRouter } from "next/navigation";
import { useState } from "react";

View File

@@ -1,6 +1,6 @@
"use client";
import { AssignmentSubmissionType } from "@/features/local/assignments/models/assignmentSubmissionType";
import { LocalCourseSettings } from "@/features/local/course/localCourseSettings";
import { AssignmentSubmissionType } from "@/models/local/assignment/assignmentSubmissionType";
import { LocalCourseSettings } from "@/models/local/localCourseSettings";
export function getAssignmentHelpString(settings: LocalCourseSettings) {
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)
\`Inline code\`
> Blockquote
@@ -55,32 +54,6 @@ flowchart TD
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
- 1pt: singular point

View File

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

View File

@@ -1,8 +1,13 @@
"use client";
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 PagePreview from "./PagePreview";
import { useLocalCourseSettingsQuery } from "@/features/local/course/localCoursesHooks";
import { useLocalCourseSettingsQuery } from "@/hooks/localCourse/localCoursesHooks";
import EditPageButtons from "./EditPageButtons";
import ClientOnly from "@/components/ClientOnly";
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 EditPageHeader from "./EditPageHeader";
import { EditLayout } from "@/components/EditLayout";
import { localPageMarkdownUtils } from "@/features/local/pages/localCoursePageModels";
import {
usePageQuery,
useUpdatePageMutation,
} from "@/features/local/pages/pageHooks";
export default function EditPage({
moduleName,
@@ -125,5 +125,5 @@ export default function EditPage({
</>
}
/>
);
)
}

View File

@@ -4,15 +4,15 @@ import { Spinner } from "@/components/Spinner";
import {
useCanvasPagesQuery,
useCreateCanvasPageMutation,
useUpdateCanvasPageMutation,
useDeleteCanvasPageMutation,
} from "@/features/canvas/hooks/canvasPageHooks";
import { baseCanvasUrl } from "@/features/canvas/services/canvasServiceUtils";
import { useLocalCourseSettingsQuery } from "@/features/local/course/localCoursesHooks";
useUpdateCanvasPageMutation,
} from "@/hooks/canvas/canvasPageHooks";
import { useLocalCourseSettingsQuery } from "@/hooks/localCourse/localCoursesHooks";
import {
useDeletePageMutation,
usePageQuery,
} from "@/features/local/pages/pageHooks";
} from "@/hooks/localCourse/pageHooks";
import { baseCanvasUrl } from "@/services/canvas/canvasServiceUtils";
import { getCourseUrl } from "@/services/urlUtils";
import Link from "next/link";
import { useRouter } from "next/navigation";

View File

@@ -17,7 +17,6 @@ export default function EditPageHeader({
className="btn"
href={getCourseUrl(courseName)}
shallow={true}
prefetch={true}
>
{courseName}
</Link>

View File

@@ -1,7 +1,9 @@
import MarkdownDisplay from "@/components/MarkdownDisplay";
import { LocalCoursePage } from "@/features/local/pages/localCoursePageModels";
import { LocalCoursePage } from "@/models/local/page/localCoursePage";
import React from "react";
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 {
usePageQuery,
useUpdatePageMutation,
} from "@/features/local/pages/pageHooks";
} from "@/hooks/localCourse/pageHooks";
import { getModuleItemUrl } from "@/services/urlUtils";
import { useRouter } from "next/navigation";
import { useState } from "react";

View File

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

View File

@@ -1,5 +1,6 @@
"use client";
import { MonacoEditor } from "@/components/editor/MonacoEditor";
import { quizMarkdownUtils } from "@/models/local/quiz/utils/quizMarkdownUtils";
import { useEffect, useState } from "react";
import QuizPreview from "./QuizPreview";
import { QuizButtons } from "./QuizButton";
@@ -10,14 +11,13 @@ import { useCourseContext } from "@/app/course/[courseName]/context/courseContex
import {
useQuizQuery,
useUpdateQuizMutation,
} from "@/features/local/quizzes/quizHooks";
} from "@/hooks/localCourse/quizHooks";
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 { useLocalCourseSettingsQuery } from "@/features/local/course/localCoursesHooks";
import { LocalCourseSettings } from "@/models/local/localCourseSettings";
import { useLocalCourseSettingsQuery } from "@/hooks/localCourse/localCoursesHooks";
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 groupNames = settings.assignmentGroups.map((g) => g.name).join("\n- ");
@@ -61,11 +61,6 @@ points: 4
the underscore is optional
short answer
---
short answer with auto-graded responses
*a) answer 1
*b) other valid answer
short_answer=
---
this is a matching question
^ left answer - right dropdown
^ other thing - another option

View File

@@ -17,7 +17,6 @@ export default function EditQuizHeader({
className="btn"
href={getCourseUrl(courseName)}
shallow={true}
prefetch={true}
>
{courseName}
</Link>

View File

@@ -5,13 +5,13 @@ import {
useCanvasQuizzesQuery,
useAddQuizToCanvasMutation,
useDeleteQuizFromCanvasMutation,
} from "@/features/canvas/hooks/canvasQuizHooks";
import { baseCanvasUrl } from "@/features/canvas/services/canvasServiceUtils";
import { useLocalCourseSettingsQuery } from "@/features/local/course/localCoursesHooks";
} from "@/hooks/canvas/canvasQuizHooks";
import { useLocalCourseSettingsQuery } from "@/hooks/localCourse/localCoursesHooks";
import {
useDeleteQuizMutation,
useQuizQuery,
} from "@/features/local/quizzes/quizHooks";
} from "@/hooks/localCourse/quizHooks";
import { baseCanvasUrl } from "@/services/canvas/canvasServiceUtils";
import { getCourseUrl } from "@/services/urlUtils";
import Link from "next/link";
import { useRouter } from "next/navigation";

View File

@@ -1,10 +1,10 @@
import CheckIcon from "@/components/icons/CheckIcon";
import MarkdownDisplay from "@/components/MarkdownDisplay";
import { useQuizQuery } from "@/hooks/localCourse/quizHooks";
import {
LocalQuizQuestion,
QuestionType,
} from "@/features/local/quizzes/models/localQuizQuestion";
import { useQuizQuery } from "@/features/local/quizzes/quizHooks";
} from "@/models/local/quiz/localQuizQuestion";
import { escapeMatchingText } from "@/services/utils/questionHtmlUtils";
export default function QuizPreview({

View File

@@ -5,7 +5,7 @@ import { Spinner } from "@/components/Spinner";
import {
useQuizQuery,
useUpdateQuizMutation,
} from "@/features/local/quizzes/quizHooks";
} from "@/hooks/localCourse/quizHooks";
import { getModuleItemUrl } from "@/services/urlUtils";
import { useRouter } from "next/navigation";
import { useState } from "react";

View File

@@ -14,9 +14,8 @@ export async function generateMetadata({
}): Promise<Metadata> {
const { courseName, quizName } = await params;
const decodedQuizName = decodeURIComponent(quizName);
const decodedCourseName = decodeURIComponent(courseName);
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 CollapsableSidebar from "./CollapsableSidebar";
export default async function CoursePage() {
return (
<>
<div className="h-full flex flex-col">
<DragStyleContextProvider>
<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">
<CourseNavigation />
<CourseCalendar />

View File

@@ -3,22 +3,20 @@
import {
useLocalCourseSettingsQuery,
useUpdateLocalCourseSettingsMutation,
} from "@/features/local/course/localCoursesHooks";
import { LocalAssignmentGroup } from "@/features/local/assignments/models/localAssignmentGroup";
} from "@/hooks/localCourse/localCoursesHooks";
import { LocalAssignmentGroup } from "@/models/local/assignment/localAssignmentGroup";
import { useEffect, useState } from "react";
import TextInput from "../../../../components/form/TextInput";
import { useSetAssignmentGroupsMutation } from "@/hooks/canvas/canvasCourseHooks";
import { settingsBox } from "./sharedSettings";
import { Spinner } from "@/components/Spinner";
import { baseCanvasUrl } from "@/services/canvas/canvasServiceUtils";
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() {
const { data: settings, isPending } = useLocalCourseSettingsQuery();
const updateSettings = useUpdateLocalCourseSettingsMutation();
const applyInCanvas = useSetAssignmentGroupsMutation(settings.canvasId);
const modal = useModal();
const [assignmentGroups, setAssignmentGroups] = useState<
LocalAssignmentGroup[]
@@ -106,46 +104,17 @@ export default function AssignmentGroupManagement() {
</div>
<br />
<div className="flex justify-end">
<Modal
modalControl={modal}
buttonText="Update Assignment Groups In Canvas"
buttonClass="btn-"
modalWidth="w-1/5"
>
{({ closeModal }) => (
<div>
<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
);
<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>
// prevent debounce from resetting
if (newSettings) setAssignmentGroups(newSettings.assignmentGroups);
}}
disabled={applyInCanvas.isPending}
>
Update Assignment Groups In Canvas
</button>
</div>
{applyInCanvas.isPending && <Spinner />}
{applyInCanvas.isSuccess && (

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -76,6 +76,83 @@ code {
@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 {
@apply mb-3;
}

View File

@@ -2,10 +2,15 @@ import type { Metadata } from "next";
import "./globals.css";
import Providers from "./providers";
import { Suspense } from "react";
import { dehydrate, HydrationBoundary } from "@tanstack/react-query";
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 { getTitle } from "@/services/titleUtils";
import DataHydration from "./DataHydration";
export const dynamic = "force-dynamic";
export const metadata: Metadata = {
@@ -29,10 +34,84 @@ export default async function RootLayout({
<ClientCacheInvalidation></ClientCacheInvalidation>
{children}
</DataHydration>
</Providers>
</Providers>
</Suspense>
</div>
</body>
</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,16 +1,16 @@
"use client";
import React, { useState } from "react";
import { SuspenseAndErrorHandling } from "@/components/SuspenseAndErrorHandling";
import AddNewCourseToGlobalSettingsForm from "./AddCourseToGlobalSettingsForm";
import NewCourseForm from "./NewCourseForm";
import ClientOnly from "@/components/ClientOnly";
export default function AddCourseToGlobalSettings() {
export default function AddNewCourse() {
const [showForm, setShowForm] = useState(false);
return (
<div>
<div className="flex justify-center">
<button className="" onClick={() => setShowForm((i) => !i)}>
<button className="" onClick={() => setShowForm(true)}>
Add New Course
</button>
</div>
@@ -18,9 +18,7 @@ export default function AddCourseToGlobalSettings() {
<div className={" collapsible " + (showForm && "expand")}>
<div className="border rounded-md p-3 m-3">
<SuspenseAndErrorHandling>
<ClientOnly>
{showForm && <AddNewCourseToGlobalSettingsForm />}
</ClientOnly>
<ClientOnly>{showForm && <NewCourseForm />}</ClientOnly>
</SuspenseAndErrorHandling>
</div>
</div>

View File

@@ -1,27 +1,25 @@
"use client";
import ButtonSelect from "@/components/ButtonSelect";
import { DayOfWeekInput } from "@/components/form/DayOfWeekInput";
import SelectInput from "@/components/form/SelectInput";
import { StoragePathSelector } from "@/components/form/StoragePathSelector";
import TextInput from "@/components/form/TextInput";
import { Spinner } from "@/components/Spinner";
import { SuspenseAndErrorHandling } from "@/components/SuspenseAndErrorHandling";
import { useCourseListInTermQuery } from "@/hooks/canvas/canvasCourseHooks";
import { useCanvasTermsQuery } from "@/hooks/canvas/canvasHooks";
import {
useCreateLocalCourseMutation,
useLocalCoursesSettingsQuery,
} from "@/features/local/course/localCoursesHooks";
import { CanvasCourseModel } from "@/features/canvas/models/courses/canvasCourseModel";
import { CanvasEnrollmentTermModel } from "@/features/canvas/models/enrollmentTerms/canvasEnrollmentTermModel";
import { AssignmentSubmissionType } from "@/features/local/assignments/models/assignmentSubmissionType";
import { getCourseUrl } from "@/services/urlUtils";
import { useRouter } from "next/navigation";
import React, { Dispatch, SetStateAction, useMemo, useState } from "react";
} from "@/hooks/localCourse/localCoursesHooks";
import { useEmptyDirectoriesQuery } from "@/hooks/localCourse/storageDirectoryHooks";
import { CanvasCourseModel } from "@/models/canvas/courses/canvasCourseModel";
import { CanvasEnrollmentTermModel } from "@/models/canvas/enrollmentTerms/canvasEnrollmentTermModel";
import { AssignmentSubmissionType } from "@/models/local/assignment/assignmentSubmissionType";
import {
DayOfWeek,
LocalCourseSettings,
} from "@/features/local/course/localCourseSettings";
import { useCourseListInTermQuery } from "@/features/canvas/hooks/canvasCourseHooks";
import { useCanvasTermsQuery } from "@/features/canvas/hooks/canvasHooks";
} from "@/models/local/localCourseSettings";
import { getCourseUrl } from "@/services/urlUtils";
import { useRouter } from "next/navigation";
import React, { useMemo, useState } from "react";
const sampleCompose = `services:
canvas_manager:
@@ -39,7 +37,7 @@ const sampleCompose = `services:
- ~/projects/faculty/4850_AdvancedFE/2024-fall-alex/modules:/app/storage/advanced_frontend
`;
export default function AddNewCourseToGlobalSettingsForm() {
export default function NewCourseForm() {
const router = useRouter();
const today = useMemo(() => new Date(), []);
const { data: canvasTerms } = useCanvasTermsQuery(today);
@@ -56,7 +54,6 @@ export default function AddNewCourseToGlobalSettingsForm() {
const [courseToImport, setCourseToImport] = useState<
LocalCourseSettings | undefined
>();
const [name, setName] = useState("");
const createCourse = useCreateLocalCourseMutation();
const formIsComplete =
@@ -64,13 +61,12 @@ export default function AddNewCourseToGlobalSettingsForm() {
return (
<div>
<ButtonSelect
options={canvasTerms}
getOptionName={(t) => t?.name ?? ""}
setValue={setSelectedTerm}
<SelectInput
value={selectedTerm}
setValue={setSelectedTerm}
label={"Canvas Term"}
center={true}
options={canvasTerms}
getOptionName={(t) => t.name}
/>
<SuspenseAndErrorHandling>
{selectedTerm && (
@@ -84,8 +80,6 @@ export default function AddNewCourseToGlobalSettingsForm() {
setSelectedDaysOfWeek={setSelectedDaysOfWeek}
courseToImport={courseToImport}
setCourseToImport={setCourseToImport}
name={name}
setName={setName}
/>
)}
</SuspenseAndErrorHandling>
@@ -94,7 +88,6 @@ export default function AddNewCourseToGlobalSettingsForm() {
disabled={!formIsComplete || createCourse.isPending}
onClick={async () => {
if (formIsComplete) {
console.log("Creating course with settings:", selectedDirectory);
const newSettings: LocalCourseSettings = courseToImport
? {
...courseToImport,
@@ -133,10 +126,8 @@ export default function AddNewCourseToGlobalSettingsForm() {
await createCourse.mutateAsync({
settings: newSettings,
settingsFromCourseToImport: courseToImport,
name,
directory: selectedDirectory,
});
router.push(getCourseUrl(name));
router.push(getCourseUrl(selectedDirectory));
}
}}
>
@@ -157,35 +148,32 @@ function OtherSettings({
selectedTerm,
selectedCanvasCourse,
setSelectedCanvasCourse,
selectedDirectory: _,
selectedDirectory,
setSelectedDirectory,
selectedDaysOfWeek,
setSelectedDaysOfWeek,
courseToImport,
setCourseToImport,
name,
setName,
}: {
selectedTerm: CanvasEnrollmentTermModel;
selectedCanvasCourse: CanvasCourseModel | undefined;
setSelectedCanvasCourse: Dispatch<
SetStateAction<CanvasCourseModel | undefined>
setSelectedCanvasCourse: React.Dispatch<
React.SetStateAction<CanvasCourseModel | undefined>
>;
selectedDirectory: string | undefined;
setSelectedDirectory: Dispatch<SetStateAction<string | undefined>>;
setSelectedDirectory: React.Dispatch<
React.SetStateAction<string | undefined>
>;
selectedDaysOfWeek: DayOfWeek[];
setSelectedDaysOfWeek: Dispatch<SetStateAction<DayOfWeek[]>>;
setSelectedDaysOfWeek: React.Dispatch<React.SetStateAction<DayOfWeek[]>>;
courseToImport: LocalCourseSettings | undefined;
setCourseToImport: Dispatch<SetStateAction<LocalCourseSettings | undefined>>;
name: string;
setName: Dispatch<SetStateAction<string>>;
setCourseToImport: React.Dispatch<
React.SetStateAction<LocalCourseSettings | undefined>
>;
}) {
const { data: canvasCourses } = useCourseListInTermQuery(selectedTerm.id);
const { data: allSettings } = useLocalCoursesSettingsQuery();
const [directory, setDirectory] = useState("./");
// const directoryIsCourseQuery = useDirectoryIsCourseQuery(
// selectedDirectory ?? "./"
// );
const { data: emptyDirectories } = useEmptyDirectoriesQuery();
const populatedCanvasCourseIds = allSettings?.map((s) => s.canvasId) ?? [];
const availableCourses =
@@ -196,21 +184,25 @@ function OtherSettings({
return (
<>
<ButtonSelect
<SelectInput
value={selectedCanvasCourse}
setValue={setSelectedCanvasCourse}
label={"Course"}
options={availableCourses}
getOptionName={(c) => c?.name ?? ""}
center={true}
getOptionName={(c) => c.name}
/>
<StoragePathSelector
value={directory}
setValue={setDirectory}
setLastTypedValue={setSelectedDirectory}
<SelectInput
value={selectedDirectory}
setValue={setSelectedDirectory}
label={"Storage Folder"}
options={emptyDirectories ?? []}
getOptionName={(d) => d}
emptyOptionText="--- add a new folder to your docker compose to add more folders ---"
/>
<div className="px-5">
New folders will not be created automatically, you are expected to mount
a docker volume for each courses.
</div>
<br />
<div className="flex justify-center">
<DayOfWeekInput
@@ -233,7 +225,6 @@ function OtherSettings({
options={allSettings}
getOptionName={(c) => c.name}
/>
<TextInput value={name} setValue={setName} label={"Display Name"} />
<div className="px-5">
Assignments, Quizzes, Pages, and Lectures will have their due dates
moved based on how far they are from the start of the semester.

View File

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

View File

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

View File

@@ -1,6 +1,6 @@
"use client";
import { useLocalCoursesSettingsQuery } from "@/features/local/course/localCoursesHooks";
import { useLocalCoursesSettingsQuery } from "@/hooks/localCourse/localCoursesHooks";
import OneCourseLectures from "./OneCourseLectures";
import { SuspenseAndErrorHandling } from "@/components/SuspenseAndErrorHandling";
import CourseContextProvider from "../course/[courseName]/context/CourseContextProvider";

View File

@@ -2,41 +2,34 @@ import React from "react";
export default function ButtonSelect<T>({
options,
getOptionName,
setValue,
value,
label,
center = false,
getName,
setSelectedOption,
selectedOption,
label
}: {
options: T[];
getOptionName: (value: T | undefined) => string;
setValue: (value: T | undefined) => void;
value: T | undefined;
getName: (value: T | undefined) => string;
setSelectedOption: (value: T | undefined) => void;
selectedOption: T | undefined;
label: string;
center?: boolean;
}) {
return (
<div className={center ? "text-center" : ""}>
<div>
<label>{label}</label>
<div
<div className="flex flex-row gap-3 flex-wrap">
{options.map((o) => (
<button
type="button"
key={getName(o)}
className={
"flex flex-row gap-3 flex-wrap " + (center ? "justify-center" : "")
getName(o) === getName(selectedOption) ? " " : "unstyled btn-outline"
}
>
{options.map((o) => (
<button
type="button"
key={getOptionName(o)}
className={
getOptionName(o) === getOptionName(value)
? " "
: "unstyled btn-outline"
}
onClick={() => setValue(o)}
>
{getOptionName(o)}
</button>
))}
onClick={() => setSelectedOption(o)}
>
{getName(o)}
</button>
))}
</div>
</div>
);

View File

@@ -1,19 +1,14 @@
import { useLocalCourseSettingsQuery } from "@/features/local/course/localCoursesHooks";
import { useLocalCourseSettingsQuery } from "@/hooks/localCourse/localCoursesHooks";
import { SuspenseAndErrorHandling } from "./SuspenseAndErrorHandling";
import { markdownToHTMLSafe } from "@/services/htmlMarkdownUtils";
import { LocalCourseSettings } from "@/features/local/course/localCourseSettings";
import { LocalCourseSettings } from "@/models/local/localCourseSettings";
export default function MarkdownDisplay({
markdown,
className = "",
replaceText = [],
}: {
markdown: string;
className?: string;
replaceText?: {
source: string;
destination: string;
}[];
}) {
const { data: settings } = useLocalCourseSettingsQuery();
return (
@@ -22,7 +17,6 @@ export default function MarkdownDisplay({
markdown={markdown}
settings={settings}
className={className}
replaceText={replaceText}
/>
</SuspenseAndErrorHandling>
);
@@ -32,25 +26,16 @@ function DangerousInnerMarkdown({
markdown,
settings,
className,
replaceText,
}: {
markdown: string;
settings: LocalCourseSettings;
className: string;
replaceText: {
source: string;
destination: string;
}[];
}) {
return (
<div
className={"markdownPreview " + className}
dangerouslySetInnerHTML={{
__html: markdownToHTMLSafe({
markdownString: markdown,
settings,
replaceText,
}),
__html: markdownToHTMLSafe(markdown, settings),
}}
></div>
);

View File

@@ -1,4 +1,3 @@
"use client"
import { getErrorMessage } from "@/services/utils/queryClient";
import { QueryErrorResetBoundary } from "@tanstack/react-query";
import { FC, ReactNode, Suspense } from "react";

View File

@@ -1,5 +1,5 @@
"use client";
import { SimpleTimeOnly } from "@/features/local/course/localCourseSettings";
import { SimpleTimeOnly } from "@/models/local/localCourseSettings";
import { FC } from "react";
export const TimePicker: FC<{

View File

@@ -1,4 +1,4 @@
import { DayOfWeek } from "@/features/local/course/localCourseSettings";
import { DayOfWeek } from "@/models/local/localCourseSettings";
export function DayOfWeekInput({
selectedDays,

View File

@@ -1,169 +0,0 @@
import { useDirectoryContentsQuery } from "@/features/local/utils/storageDirectoryHooks";
import { useState, useRef, useEffect } from "react";
import { createPortal } from "react-dom";
export function StoragePathSelector({
value,
setValue,
label,
className,
setLastTypedValue,
}: {
value: string;
setValue: (newValue: string) => void;
label: string;
className?: string;
setLastTypedValue?: (value: string) => void;
}) {
const [path, setPath] = useState(value);
const { data: directoryContents } = useDirectoryContentsQuery(value);
const [isFocused, setIsFocused] = useState(false);
const [highlightedIndex, setHighlightedIndex] = useState<number>(-1);
const [arrowUsed, setArrowUsed] = useState(false);
const inputRef = useRef<HTMLInputElement>(null);
const dropdownRef = useRef<HTMLDivElement>(null);
useEffect(() => {
setPath(value);
}, [value]);
useEffect(() => {
if (setLastTypedValue) setLastTypedValue(path);
}, [path, setLastTypedValue]);
const handleKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
if (!isFocused || filteredFolders.length === 0) return;
if (e.key === "ArrowDown") {
setHighlightedIndex((prev) => (prev + 1) % filteredFolders.length);
setArrowUsed(true);
e.preventDefault();
} else if (e.key === "ArrowUp") {
setHighlightedIndex(
(prev) => (prev - 1 + filteredFolders.length) % filteredFolders.length
);
setArrowUsed(true);
e.preventDefault();
} else if (e.key === "Tab") {
if (highlightedIndex >= 0) {
handleSelectFolder(filteredFolders[highlightedIndex], arrowUsed);
e.preventDefault();
} else {
handleSelectFolder(filteredFolders[1], arrowUsed);
e.preventDefault();
}
} else if (e.key === "Enter") {
if (highlightedIndex >= 0) {
handleSelectFolder(filteredFolders[highlightedIndex], arrowUsed);
e.preventDefault();
} else {
setIsFocused(false);
inputRef.current?.blur();
e.preventDefault();
}
} else if (e.key === "Escape") {
setIsFocused(false);
inputRef.current?.blur();
e.preventDefault();
}
};
// Calculate dropdown position style
const dropdownPositionStyle = (() => {
if (inputRef.current) {
const rect = inputRef.current.getBoundingClientRect();
return {
top: rect.bottom + window.scrollY,
left: rect.left + window.scrollX,
width: rect.width,
};
}
return {};
})();
// Get last part of the path
const lastPart = path.split("/")[path.split("/").length - 1] || "";
// Filter options to those whose name matches the last part of the path
const filteredFolders = (directoryContents?.folders ?? []).filter((option) =>
option.toLowerCase().includes(lastPart.toLowerCase())
);
// Handle folder selection
const handleSelectFolder = (option: string, shouldFocus: boolean = false) => {
let newPath = path.endsWith("/")
? path + option
: path.replace(/[^/]*$/, option);
if (!newPath.endsWith("/")) {
newPath += "/";
}
setPath(newPath);
setValue(newPath);
setArrowUsed(false);
setHighlightedIndex(-1);
if (shouldFocus) {
setTimeout(() => inputRef.current?.focus(), 0);
}
};
// Scroll highlighted option into view when it changes
useEffect(() => {
if (dropdownRef.current && highlightedIndex >= 0) {
const optionElements =
dropdownRef.current.querySelectorAll(".dropdown-option");
if (optionElements[highlightedIndex]) {
(optionElements[highlightedIndex] as HTMLElement).scrollIntoView({
block: "nearest",
});
}
}
}, [highlightedIndex]);
return (
<label className={"flex flex-col relative " + className}>
{label}
<br />
<input
ref={inputRef}
className="bg-slate-800 w-full px-1"
value={path}
onChange={(e) => {
setPath(e.target.value);
if (e.target.value.endsWith("/")) {
setValue(e.target.value);
setTimeout(() => inputRef.current?.focus(), 0);
}
}}
onFocus={() => setIsFocused(true)}
onBlur={() => setTimeout(() => setIsFocused(false), 100)}
onKeyDown={handleKeyDown}
autoComplete="off"
/>
{isFocused &&
createPortal(
<div className=" ">
<div
ref={dropdownRef}
className={
" text-slate-300 border border-slate-500 " +
"absolute bg-slate-900 rounded-md mt-1 w-full max-h-96 overflow-y-auto pointer-events-auto"
}
style={dropdownPositionStyle}
>
{filteredFolders.map((option, idx) => (
<div
key={option}
className={`dropdown-option w-full px-2 py-1 cursor-pointer ${
highlightedIndex === idx ? "bg-blue-700 text-white" : ""
}`}
onMouseDown={() => handleSelectFolder(option)}
onMouseEnter={() => setHighlightedIndex(idx)}
>
{option}
</div>
))}
</div>
</div>,
document.body
)}
</label>
);
}

View File

@@ -6,14 +6,12 @@ export default function TextInput({
label,
className,
isTextArea = false,
inputRef = undefined,
}: {
value: string;
setValue: (newValue: string) => void;
label: string;
className?: string;
isTextArea?: boolean;
inputRef?: React.RefObject<HTMLInputElement | null>;
}) {
return (
<label className={"flex flex-col " + className}>
@@ -24,7 +22,6 @@ export default function TextInput({
className="bg-slate-800 border border-slate-500 rounded-md w-full px-1"
value={value}
onChange={(e) => setValue(e.target.value)}
ref={inputRef}
/>
)}
{isTextArea && (

View File

@@ -1,82 +0,0 @@
import { useLocalCourseSettingsQuery } from "@/features/local/course/localCoursesHooks";
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import { canvasModuleService } from "../services/canvasModuleService";
import { IModuleItem } from "@/features/local/modules/IModuleItem";
export const canvasCourseModuleKeys = {
modules: (canvasId: number) => ["canvas", canvasId, "module list"] as const,
};
export const useCanvasModulesQuery = () => {
const { data: settings } = useLocalCourseSettingsQuery();
return useQuery({
queryKey: canvasCourseModuleKeys.modules(settings.canvasId),
queryFn: async () =>
await canvasModuleService.getCourseModules(settings.canvasId),
});
};
export const useAddCanvasModuleMutation = () => {
const { data: settings } = useLocalCourseSettingsQuery();
const queryClient = useQueryClient();
return useMutation({
mutationFn: async (moduleName: string) =>
await canvasModuleService.createModule(settings.canvasId, moduleName),
onSuccess: () => {
queryClient.invalidateQueries({
queryKey: canvasCourseModuleKeys.modules(settings.canvasId),
});
},
});
};
export const useReorderCanvasModuleItemsMutation = () => {
const { data: settings } = useLocalCourseSettingsQuery();
const queryClient = useQueryClient();
return useMutation({
mutationFn: async ({
moduleId,
items,
}: {
moduleId: number;
items: IModuleItem[];
}) => {
if (!settings?.canvasId) throw new Error("No canvasId in settings");
const canvasModule = await canvasModuleService.getModuleWithItems(
settings.canvasId,
moduleId
);
if (!canvasModule.items) {
throw new Error(
"cannot sort canvas module items, no items found in module"
);
}
const canvasItems = canvasModule.items;
// Sort IModuleItems by dueAt
const sorted = [...items].sort((a, b) => {
const aDate = a.dueAt ? new Date(a.dueAt).getTime() : 0;
const bDate = b.dueAt ? new Date(b.dueAt).getTime() : 0;
return aDate - bDate;
});
// Map sorted IModuleItems to CanvasModuleItem ids by matching name/title
const orderedIds = sorted
.map((localItem) => canvasItems.find((canvasItem) => canvasItem.title === localItem.name)?.id)
.filter((id): id is number => typeof id === "number");
return await canvasModuleService.reorderModuleItems(
settings.canvasId,
moduleId,
orderedIds
);
},
onSuccess: (_data) => {
if (!settings?.canvasId) return;
queryClient.invalidateQueries({
queryKey: canvasCourseModuleKeys.modules(settings.canvasId),
});
},
});
};

View File

@@ -1,225 +0,0 @@
import { describe, it, expect, vi, beforeEach } from "vitest";
import { canvasQuizService } from "./canvasQuizService";
import { CanvasQuizQuestion } from "@/features/canvas/models/quizzes/canvasQuizQuestionModel";
import { LocalQuiz } from "@/features/local/quizzes/models/localQuiz";
import { QuestionType } from "@/features/local/quizzes/models/localQuizQuestion";
// Mock the dependencies
vi.mock("@/services/axiosUtils", () => ({
axiosClient: {
get: vi.fn(),
post: vi.fn(),
delete: vi.fn(),
},
}));
vi.mock("./canvasServiceUtils", () => ({
canvasApi: "https://test.instructure.com/api/v1",
paginatedRequest: vi.fn(),
}));
vi.mock("./canvasAssignmentService", () => ({
canvasAssignmentService: {
getAll: vi.fn(() => Promise.resolve([])),
delete: vi.fn(() => Promise.resolve()),
},
}));
vi.mock("@/services/htmlMarkdownUtils", () => ({
markdownToHTMLSafe: vi.fn(({ markdownString }) => `<p>${markdownString}</p>`),
}));
vi.mock("@/features/local/utils/timeUtils", () => ({
getDateFromStringOrThrow: vi.fn((dateString) => new Date(dateString)),
}));
vi.mock("@/services/utils/questionHtmlUtils", () => ({
escapeMatchingText: vi.fn((text) => text),
}));
describe("canvasQuizService", () => {
beforeEach(() => {
vi.clearAllMocks();
});
describe("getQuizQuestions", () => {
it("should fetch and sort quiz questions by position", async () => {
const mockQuestions: CanvasQuizQuestion[] = [
{
id: 3,
quiz_id: 1,
position: 3,
question_name: "Question 3",
question_type: "multiple_choice_question",
question_text: "What is 2+2?",
correct_comments: "",
incorrect_comments: "",
neutral_comments: "",
},
{
id: 1,
quiz_id: 1,
position: 1,
question_name: "Question 1",
question_type: "multiple_choice_question",
question_text: "What is your name?",
correct_comments: "",
incorrect_comments: "",
neutral_comments: "",
},
{
id: 2,
quiz_id: 1,
position: 2,
question_name: "Question 2",
question_type: "essay_question",
question_text: "Describe yourself",
correct_comments: "",
incorrect_comments: "",
neutral_comments: "",
},
];
const { paginatedRequest } = await import("./canvasServiceUtils");
vi.mocked(paginatedRequest).mockResolvedValue(mockQuestions);
const result = await canvasQuizService.getQuizQuestions(1, 1);
expect(result).toHaveLength(3);
expect(result[0].position).toBe(1);
expect(result[1].position).toBe(2);
expect(result[2].position).toBe(3);
expect(result[0].question_text).toBe("What is your name?");
expect(result[1].question_text).toBe("Describe yourself");
expect(result[2].question_text).toBe("What is 2+2?");
});
it("should handle questions without position", async () => {
const mockQuestions: CanvasQuizQuestion[] = [
{
id: 1,
quiz_id: 1,
question_name: "Question 1",
question_type: "multiple_choice_question",
question_text: "What is your name?",
correct_comments: "",
incorrect_comments: "",
neutral_comments: "",
},
{
id: 2,
quiz_id: 1,
question_name: "Question 2",
question_type: "essay_question",
question_text: "Describe yourself",
correct_comments: "",
incorrect_comments: "",
neutral_comments: "",
},
];
const { paginatedRequest } = await import("./canvasServiceUtils");
vi.mocked(paginatedRequest).mockResolvedValue(mockQuestions);
const result = await canvasQuizService.getQuizQuestions(1, 1);
expect(result).toHaveLength(2);
// Should maintain original order when no position is specified
});
});
describe("Question order verification (integration test concept)", () => {
it("should detect correct question order", async () => {
// This is a conceptual test showing what the verification should validate
const _localQuiz: LocalQuiz = {
name: "Test Quiz",
description: "A test quiz",
dueAt: "2023-12-01T23:59:00Z",
shuffleAnswers: false,
showCorrectAnswers: true,
oneQuestionAtATime: false,
allowedAttempts: 1,
questions: [
{
text: "What is your name?",
questionType: QuestionType.SHORT_ANSWER,
points: 5,
answers: [],
matchDistractors: [],
},
{
text: "Describe yourself",
questionType: QuestionType.ESSAY,
points: 10,
answers: [],
matchDistractors: [],
},
{
text: "What is 2+2?",
questionType: QuestionType.MULTIPLE_CHOICE,
points: 5,
answers: [
{ text: "3", correct: false },
{ text: "4", correct: true },
{ text: "5", correct: false },
],
matchDistractors: [],
},
],
};
const canvasQuestions: CanvasQuizQuestion[] = [
{
id: 1,
quiz_id: 1,
position: 1,
question_name: "Question 1",
question_type: "short_answer_question",
question_text: "<p>What is your name?</p>",
correct_comments: "",
incorrect_comments: "",
neutral_comments: "",
},
{
id: 2,
quiz_id: 1,
position: 2,
question_name: "Question 2",
question_type: "essay_question",
question_text: "<p>Describe yourself</p>",
correct_comments: "",
incorrect_comments: "",
neutral_comments: "",
},
{
id: 3,
quiz_id: 1,
position: 3,
question_name: "Question 3",
question_type: "multiple_choice_question",
question_text: "<p>What is 2+2?</p>",
correct_comments: "",
incorrect_comments: "",
neutral_comments: "",
},
];
// Mock the getQuizQuestions to return our test data
const { paginatedRequest } = await import("./canvasServiceUtils");
vi.mocked(paginatedRequest).mockResolvedValue(canvasQuestions);
const result = await canvasQuizService.getQuizQuestions(1, 1);
// Verify the questions are in the expected order
expect(result).toHaveLength(3);
expect(result[0].question_text).toContain("What is your name?");
expect(result[1].question_text).toContain("Describe yourself");
expect(result[2].question_text).toContain("What is 2+2?");
// Verify positions are sequential
expect(result[0].position).toBe(1);
expect(result[1].position).toBe(2);
expect(result[2].position).toBe(3);
});
});
});

View File

@@ -1,21 +0,0 @@
import { RubricItem } from "@/features/local/assignments/models/rubricItem";
export const getRubricCriterion = (rubric: RubricItem[]) => {
const criterion = rubric
.map((rubricItem) => ({
description: rubricItem.label,
points: rubricItem.points,
ratings: {
0: { description: "Full Marks", points: rubricItem.points },
1: { description: "No Marks", points: 0 },
},
}))
.reduce((acc, item, index) => {
return {
...acc,
[index]: item,
};
}, {} as { [key: number]: { description: string; points: number; ratings: { [key: number]: { description: string; points: number } } } });
return criterion;
};

View File

@@ -1,188 +0,0 @@
import { describe, it, expect, vi, beforeEach } from "vitest";
import { LocalQuiz } from "@/features/local/quizzes/models/localQuiz";
import { QuestionType } from "@/features/local/quizzes/models/localQuizQuestion";
import { DayOfWeek } from "@/features/local/course/localCourseSettings";
import { AssignmentSubmissionType } from "@/features/local/assignments/models/assignmentSubmissionType";
// Mock the dependencies
vi.mock("@/services/axiosUtils", () => ({
axiosClient: {
get: vi.fn(),
post: vi.fn(),
delete: vi.fn(),
},
}));
vi.mock("./canvasServiceUtils", () => ({
canvasApi: "https://test.instructure.com/api/v1",
paginatedRequest: vi.fn(),
}));
vi.mock("./canvasAssignmentService", () => ({
canvasAssignmentService: {
getAll: vi.fn(() => Promise.resolve([])),
delete: vi.fn(() => Promise.resolve()),
},
}));
vi.mock("@/services/htmlMarkdownUtils", () => ({
markdownToHTMLSafe: vi.fn(({ markdownString }) => `<p>${markdownString}</p>`),
}));
vi.mock("@/features/local/utils/timeUtils", () => ({
getDateFromStringOrThrow: vi.fn((dateString) => new Date(dateString)),
}));
vi.mock("@/services/utils/questionHtmlUtils", () => ({
escapeMatchingText: vi.fn((text) => text),
}));
describe("Quiz Order Verification Integration", () => {
beforeEach(() => {
vi.clearAllMocks();
});
it("demonstrates the question order verification workflow", async () => {
// This test demonstrates that the verification step is properly integrated
// into the quiz creation workflow
const testQuiz: LocalQuiz = {
name: "Test Quiz - Order Verification",
description: "Testing question order verification",
dueAt: "2023-12-01T23:59:00Z",
shuffleAnswers: false,
showCorrectAnswers: true,
oneQuestionAtATime: false,
allowedAttempts: 1,
questions: [
{
text: "First Question",
questionType: QuestionType.SHORT_ANSWER,
points: 5,
answers: [],
matchDistractors: [],
},
{
text: "Second Question",
questionType: QuestionType.ESSAY,
points: 10,
answers: [],
matchDistractors: [],
},
],
};
// Import the service after mocks are set up
const { canvasQuizService } = await import("./canvasQuizService");
const { axiosClient } = await import("@/services/axiosUtils");
const { paginatedRequest } = await import("./canvasServiceUtils");
// Mock successful quiz creation
vi.mocked(axiosClient.post).mockResolvedValueOnce({
data: { id: 123, title: "Test Quiz - Order Verification" },
});
// Mock question creation responses
vi.mocked(axiosClient.post)
.mockResolvedValueOnce({ data: { id: 1, position: 1 } })
.mockResolvedValueOnce({ data: { id: 2, position: 2 } });
// Mock reordering call
vi.mocked(axiosClient.post).mockResolvedValueOnce({ data: {} });
// Mock assignment cleanup (empty assignments)
vi.mocked(paginatedRequest).mockResolvedValueOnce([]);
// Mock the verification call - questions in correct order
vi.mocked(paginatedRequest).mockResolvedValueOnce([
{
id: 1,
quiz_id: 123,
position: 1,
question_name: "Question 1",
question_type: "short_answer_question",
question_text: "<p>First Question</p>",
correct_comments: "",
incorrect_comments: "",
neutral_comments: "",
},
{
id: 2,
quiz_id: 123,
position: 2,
question_name: "Question 2",
question_type: "essay_question",
question_text: "<p>Second Question</p>",
correct_comments: "",
incorrect_comments: "",
neutral_comments: "",
},
]);
// Create the quiz and trigger verification
const result = await canvasQuizService.create(12345, testQuiz, {
name: "Test Course",
canvasId: 12345,
assignmentGroups: [],
daysOfWeek: [DayOfWeek.Monday, DayOfWeek.Wednesday, DayOfWeek.Friday],
startDate: "2023-08-15",
endDate: "2023-12-15",
defaultDueTime: { hour: 23, minute: 59 },
defaultAssignmentSubmissionTypes: [AssignmentSubmissionType.ONLINE_TEXT_ENTRY],
defaultFileUploadTypes: [],
holidays: [],
assets: []
});
// Verify the quiz was created
expect(result).toBe(123);
// Verify that the question verification API call was made
expect(vi.mocked(paginatedRequest)).toHaveBeenCalledWith({
url: "https://test.instructure.com/api/v1/courses/12345/quizzes/123/questions",
});
// The verification would have run and logged success/failure
// In a real scenario, this would catch order mismatches
});
it("demonstrates successful verification workflow", async () => {
const { canvasQuizService } = await import("./canvasQuizService");
const { paginatedRequest } = await import("./canvasServiceUtils");
// Mock questions returned from Canvas in correct order
vi.mocked(paginatedRequest).mockResolvedValueOnce([
{
id: 1,
quiz_id: 1,
position: 1,
question_name: "Question 1",
question_type: "short_answer_question",
question_text: "First question",
correct_comments: "",
incorrect_comments: "",
neutral_comments: "",
},
{
id: 2,
quiz_id: 1,
position: 2,
question_name: "Question 2",
question_type: "essay_question",
question_text: "Second question",
correct_comments: "",
incorrect_comments: "",
neutral_comments: "",
},
]);
const result = await canvasQuizService.getQuizQuestions(1, 1);
// Verify questions are returned in correct order
expect(result).toHaveLength(2);
expect(result[0].position).toBe(1);
expect(result[1].position).toBe(2);
expect(result[0].question_text).toBe("First question");
expect(result[1].question_text).toBe("Second question");
});
});

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