mirror of
https://github.com/alexmickelson/canvasManagement.git
synced 2026-03-25 23:28:33 -06:00
Compare commits
3 Commits
copilot/fi
...
copilot/fi
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5359c7a4be | ||
|
|
6db2c5b66b | ||
|
|
4cdafb5ffc |
31
.github/workflows/docker-deploy.yml
vendored
31
.github/workflows/docker-deploy.yml
vendored
@@ -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"
|
|
||||||
34
README.md
34
README.md
@@ -4,40 +4,12 @@
|
|||||||
|
|
||||||
<https://nowucca.com/2020/07/04/working-around-canvas-limitations.html>
|
<https://nowucca.com/2020/07/04/working-around-canvas-limitations.html>
|
||||||
|
|
||||||
## Getting Started and Usage (v3)
|
## Getting Started and Usage
|
||||||
|
|
||||||
All class data files are stored in markdown files in a folder. I recommend making this folder a git repo. Here's an example docker compose:
|
<!-- draft -->
|
||||||
|
|
||||||
```yml
|
|
||||||
services:
|
|
||||||
canvas_manager:
|
|
||||||
image: alexmickelson/canvas_management:3
|
|
||||||
user: "1000:1000"
|
|
||||||
container_name: canvas-manager
|
|
||||||
ports:
|
|
||||||
- 3000:3000
|
|
||||||
env_file:
|
|
||||||
- .env
|
|
||||||
environment:
|
|
||||||
- storageDirectory=/app/storage
|
|
||||||
- TZ=America/Denver
|
|
||||||
- NEXT_PUBLIC_ENABLE_FILE_SYNC=true
|
|
||||||
volumes:
|
|
||||||
- ./globalSettings.yml:/app/globalSettings.yml
|
|
||||||
- ~/projects/faculty:/app/storage
|
|
||||||
- ~/projects/facultyFiles:/app/public/images/facultyFiles
|
|
||||||
```
|
|
||||||
|
|
||||||
The `globalSettings.yml` file specifies which folders in your storage directory you want to display in the UI. This way you can have old classes files stored, but not bring them into the UI unless you need to (like when you are planning a new semester, you might want to see the old semester).
|
|
||||||
|
|
||||||
`globalSettings.yml` can start like this, this file will be edited as you add classes to manage.
|
|
||||||
|
|
||||||
```yml
|
|
||||||
courses: []
|
|
||||||
```
|
|
||||||
|
|
||||||
|
|
||||||
## Enable Image Support
|
### Enable Image Support
|
||||||
|
|
||||||
|
|
||||||
You must set the `NEXT_PUBLIC_ENABLE_FILE_SYNC` environment variable to true. Images need to be available in the `/app/public/` directory in the container so that nextjs will serve them as static files. Images can also be set to public URL's on the web.
|
You must set the `NEXT_PUBLIC_ENABLE_FILE_SYNC` environment variable to true. Images need to be available in the `/app/public/` directory in the container so that nextjs will serve them as static files. Images can also be set to public URL's on the web.
|
||||||
|
|||||||
85
build.sh
85
build.sh
@@ -1,14 +1,13 @@
|
|||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
|
|
||||||
MAJOR_VERSION="3"
|
MAJOR_VERSION="2"
|
||||||
MINOR_VERSION="0"
|
MINOR_VERSION="8"
|
||||||
VERSION="$MAJOR_VERSION.$MINOR_VERSION"
|
VERSION="$MAJOR_VERSION.$MINOR_VERSION"
|
||||||
BRANCH=""
|
|
||||||
|
|
||||||
TAG_FLAG=false
|
TAG_FLAG=false
|
||||||
PUSH_FLAG=false
|
PUSH_FLAG=false
|
||||||
|
|
||||||
while getopts ":tpb:" opt; do
|
while getopts ":tp" opt; do
|
||||||
case ${opt} in
|
case ${opt} in
|
||||||
t)
|
t)
|
||||||
TAG_FLAG=true
|
TAG_FLAG=true
|
||||||
@@ -16,12 +15,9 @@ while getopts ":tpb:" opt; do
|
|||||||
p)
|
p)
|
||||||
PUSH_FLAG=true
|
PUSH_FLAG=true
|
||||||
;;
|
;;
|
||||||
b)
|
|
||||||
BRANCH="$OPTARG"
|
|
||||||
;;
|
|
||||||
\?)
|
\?)
|
||||||
echo "Invalid option: -$OPTARG" >&2
|
echo "Invalid option: -$OPTARG" >&2
|
||||||
echo "Usage: $0 [-t] [-p] [-b branch]"
|
echo "Usage: $0 [-t] [-p]"
|
||||||
exit 1
|
exit 1
|
||||||
;;
|
;;
|
||||||
esac
|
esac
|
||||||
@@ -33,54 +29,17 @@ docker build -t canvas_management:$VERSION .
|
|||||||
|
|
||||||
if [ "$TAG_FLAG" = true ]; then
|
if [ "$TAG_FLAG" = true ]; then
|
||||||
echo "Tagging images..."
|
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:$VERSION"
|
||||||
echo "alexmickelson/canvas_management:$MAJOR_VERSION"
|
echo "alexmickelson/canvas_management:$MAJOR_VERSION"
|
||||||
echo "alexmickelson/canvas_management:latest"
|
echo "alexmickelson/canvas_management:latest"
|
||||||
|
|
||||||
docker image tag canvas_management:"$VERSION" alexmickelson/canvas_management:"$VERSION"
|
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:"$MAJOR_VERSION"
|
||||||
docker image tag canvas_management:"$VERSION" alexmickelson/canvas_management:latest
|
docker image tag canvas_management:latest 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
|
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if [ "$PUSH_FLAG" = true ]; then
|
if [ "$PUSH_FLAG" = true ]; then
|
||||||
echo "Pushing images..."
|
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:$VERSION"
|
||||||
echo "alexmickelson/canvas_management:$MAJOR_VERSION"
|
echo "alexmickelson/canvas_management:$MAJOR_VERSION"
|
||||||
echo "alexmickelson/canvas_management:latest"
|
echo "alexmickelson/canvas_management:latest"
|
||||||
@@ -88,17 +47,6 @@ if [ "$PUSH_FLAG" = true ]; then
|
|||||||
docker push alexmickelson/canvas_management:"$VERSION"
|
docker push alexmickelson/canvas_management:"$VERSION"
|
||||||
docker push alexmickelson/canvas_management:"$MAJOR_VERSION"
|
docker push alexmickelson/canvas_management:"$MAJOR_VERSION"
|
||||||
docker push alexmickelson/canvas_management:latest
|
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
|
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if [ "$TAG_FLAG" = false ] && [ "$PUSH_FLAG" = false ]; then
|
if [ "$TAG_FLAG" = false ] && [ "$PUSH_FLAG" = false ]; then
|
||||||
@@ -106,33 +54,12 @@ if [ "$TAG_FLAG" = false ] && [ "$PUSH_FLAG" = false ]; then
|
|||||||
echo "Build complete."
|
echo "Build complete."
|
||||||
echo "To tag, run with -t flag."
|
echo "To tag, run with -t flag."
|
||||||
echo "To push, run with -p 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 "Or manually run:"
|
||||||
echo ""
|
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:$VERSION"
|
||||||
echo "docker image tag canvas_management:$VERSION alexmickelson/canvas_management:$MAJOR_VERSION"
|
echo "docker image tag canvas_management:$VERSION alexmickelson/canvas_management:$MAJOR_VERSION"
|
||||||
echo "docker image tag canvas_management:$VERSION alexmickelson/canvas_management:latest"
|
echo "docker image tag canvas_management:latest alexmickelson/canvas_management:latest"
|
||||||
echo "docker push alexmickelson/canvas_management:$VERSION"
|
echo "docker push alexmickelson/canvas_management:$VERSION"
|
||||||
echo "docker push alexmickelson/canvas_management:$MAJOR_VERSION"
|
echo "docker push alexmickelson/canvas_management:$MAJOR_VERSION"
|
||||||
echo "docker push alexmickelson/canvas_management:latest"
|
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
|
|
||||||
fi
|
fi
|
||||||
|
|||||||
@@ -15,20 +15,17 @@ services:
|
|||||||
- NEXT_PUBLIC_ENABLE_FILE_SYNC=true
|
- NEXT_PUBLIC_ENABLE_FILE_SYNC=true
|
||||||
- REDIS_URL=redis://redis:6379
|
- REDIS_URL=redis://redis:6379
|
||||||
volumes:
|
volumes:
|
||||||
# - ./globalSettings.dev.yml:/app/globalSettings.yml
|
|
||||||
- ./globalSettings.yml:/app/globalSettings.yml
|
|
||||||
- .:/app
|
- .:/app
|
||||||
- ~/projects/faculty:/app/storage
|
- ~/projects/faculty/1810/2025-spring-alex/in-person:/app/storage/intro_to_web_old
|
||||||
# - ~/projects/faculty/1810/2025-spring-alex/in-person:/app/storage/intro_to_web_old
|
- ~/projects/faculty/1810/2025-fall-alex/modules:/app/storage/intro_to_web
|
||||||
# - ~/projects/faculty/1810/2025-fall-alex/modules:/app/storage/intro_to_web
|
- ~/projects/faculty/4850_AdvancedFE/2025-fall-alex/modules:/app/storage/advanced_frontend
|
||||||
# - ~/projects/faculty/4850_AdvancedFE/2025-fall-alex/modules:/app/storage/advanced_frontend
|
- ~/projects/faculty/4850_AdvancedFE/2024-fall-alex/modules:/app/storage/advanced_frontend_old
|
||||||
# - ~/projects/faculty/4850_AdvancedFE/2024-fall-alex/modules:/app/storage/advanced_frontend_old
|
- ~/projects/faculty/1430/2024-fall-alex/modules:/app/storage/ux_old
|
||||||
# - ~/projects/faculty/1430/2024-fall-alex/modules:/app/storage/ux_old
|
- ~/projects/faculty/1430/2025-fall-alex/modules:/app/storage/ux
|
||||||
# - ~/projects/faculty/1430/2025-fall-alex/modules:/app/storage/ux
|
- ~/projects/faculty/1420/2024-fall/Modules:/app/storage/1420_old
|
||||||
# - ~/projects/faculty/1420/2024-fall/Modules:/app/storage/1420_old
|
- ~/projects/faculty/1420/2025-fall-alex/modules:/app/storage/1420
|
||||||
# - ~/projects/faculty/1420/2025-fall-alex/modules:/app/storage/1420
|
- ~/projects/faculty/1425/2024-fall/Modules:/app/storage/1425_old
|
||||||
# - ~/projects/faculty/1425/2024-fall/Modules:/app/storage/1425_old
|
- ~/projects/faculty/1425/2025-fall-alex/modules:/app/storage/1425
|
||||||
# - ~/projects/faculty/1425/2025-fall-alex/modules:/app/storage/1425
|
|
||||||
- ~/projects/facultyFiles:/app/public/images/facultyFiles
|
- ~/projects/facultyFiles:/app/public/images/facultyFiles
|
||||||
|
|
||||||
redis:
|
redis:
|
||||||
@@ -50,7 +47,7 @@ services:
|
|||||||
--api-key "$MCP_TOKEN" \
|
--api-key "$MCP_TOKEN" \
|
||||||
--server-type "streamable_http" \
|
--server-type "streamable_http" \
|
||||||
--cors-allow-origins "*" \
|
--cors-allow-origins "*" \
|
||||||
-- http://canvas-dev:3000/api/mcp/mcp/
|
-- http://canvas-dev:3000/api/mcp
|
||||||
'
|
'
|
||||||
working_dir: /app
|
working_dir: /app
|
||||||
ports:
|
ports:
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
services:
|
services:
|
||||||
canvas_manager:
|
canvas_manager:
|
||||||
image: alexmickelson/canvas_management:3
|
image: alexmickelson/canvas_management:2.7
|
||||||
user: "1000:1000"
|
user: "1000:1000"
|
||||||
container_name: canvas-manager
|
container_name: canvas-manager-2
|
||||||
ports:
|
ports:
|
||||||
- 3000:3000
|
- 3000:3000
|
||||||
env_file:
|
env_file:
|
||||||
@@ -14,8 +14,30 @@ services:
|
|||||||
- REDIS_URL=redis://redis:6379
|
- REDIS_URL=redis://redis:6379
|
||||||
# - FILE_POLLING=true
|
# - FILE_POLLING=true
|
||||||
volumes:
|
volumes:
|
||||||
- ./globalSettings.yml:/app/globalSettings.yml
|
# - ~/projects/faculty/3840_Telemetry/2024Spring_alex/modules:/app/storage/spring_2024_telemetry
|
||||||
- ~/projects/faculty:/app/storage
|
|
||||||
|
# - ~/projects/faculty/1400/2025_spring_alex/modules:/app/storage/1400
|
||||||
|
# - ~/projects/faculty/1405/2025_spring_alex:/app/storage/1405
|
||||||
|
# - ~/projects/faculty/3840_Telemetry/2025_spring_alex/modules:/app/storage/telemetry
|
||||||
|
# - ~/projects/faculty/4620_Distributed/2025Spring/modules:/app/storage/distributed
|
||||||
|
# - ~/projects/faculty/4620_Distributed/2024Spring/modules:/app/storage/distributed_old
|
||||||
|
|
||||||
|
|
||||||
|
- ~/projects/faculty/1810/2025-spring-alex/in-person:/app/storage/intro_to_web_old
|
||||||
|
- ~/projects/faculty/1810/2025-fall-alex/modules:/app/storage/intro_to_web
|
||||||
|
|
||||||
|
- ~/projects/faculty/4850_AdvancedFE/2025-fall-alex/modules:/app/storage/advanced_frontend
|
||||||
|
- ~/projects/faculty/4850_AdvancedFE/2024-fall-alex/modules:/app/storage/advanced_frontend_old
|
||||||
|
|
||||||
|
- ~/projects/faculty/1430/2024-fall-alex/modules:/app/storage/ux_old
|
||||||
|
- ~/projects/faculty/1430/2025-fall-alex/modules:/app/storage/ux
|
||||||
|
|
||||||
|
- ~/projects/faculty/1420/2024-fall/Modules:/app/storage/1420_old
|
||||||
|
- ~/projects/faculty/1420/2025-fall-alex/modules:/app/storage/1420
|
||||||
|
|
||||||
|
- ~/projects/faculty/1425/2024-fall/Modules:/app/storage/1425_old
|
||||||
|
- ~/projects/faculty/1425/2025-fall-alex/modules:/app/storage/1425
|
||||||
|
|
||||||
- ~/projects/facultyFiles:/app/public/images/facultyFiles
|
- ~/projects/facultyFiles:/app/public/images/facultyFiles
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -10,11 +10,13 @@ const compat = new FlatCompat({
|
|||||||
});
|
});
|
||||||
|
|
||||||
const eslintConfig = [
|
const eslintConfig = [
|
||||||
{
|
|
||||||
ignores: ["**/node_modules/**", "**/.next/**", "storage/**"],
|
|
||||||
},
|
|
||||||
...compat.config({
|
...compat.config({
|
||||||
extends: ["next/core-web-vitals", "next/typescript", "prettier"],
|
extends: ["next/core-web-vitals", "next/typescript", "prettier"],
|
||||||
|
ignores: [
|
||||||
|
"**/node_modules/**",
|
||||||
|
"**/.next/**",
|
||||||
|
"storage/**"
|
||||||
|
],
|
||||||
rules: {
|
rules: {
|
||||||
"react-refresh/only-export-components": "off", // Disabled the rule
|
"react-refresh/only-export-components": "off", // Disabled the rule
|
||||||
"@typescript-eslint/no-unused-vars": [
|
"@typescript-eslint/no-unused-vars": [
|
||||||
|
|||||||
@@ -1,5 +0,0 @@
|
|||||||
courses:
|
|
||||||
- path: ./3820_BackEnd/2025-fall/Modules/
|
|
||||||
name: Back-End
|
|
||||||
- path: ./3820_BackEnd/2024-fall/Modules/
|
|
||||||
name: Back-End
|
|
||||||
@@ -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
10212
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
@@ -23,6 +23,7 @@
|
|||||||
"@trpc/react-query": "11.4.3",
|
"@trpc/react-query": "11.4.3",
|
||||||
"@trpc/server": "11.4.3",
|
"@trpc/server": "11.4.3",
|
||||||
"@trpc/tanstack-react-query": "^11.4.3",
|
"@trpc/tanstack-react-query": "^11.4.3",
|
||||||
|
"@types/prismjs": "^1.26.5",
|
||||||
"@types/ws": "^8.18.1",
|
"@types/ws": "^8.18.1",
|
||||||
"@typescript-eslint/parser": "^8.37.0",
|
"@typescript-eslint/parser": "^8.37.0",
|
||||||
"chokidar": "^4.0.3",
|
"chokidar": "^4.0.3",
|
||||||
@@ -32,6 +33,7 @@
|
|||||||
"marked-katex-extension": "^5.1.5",
|
"marked-katex-extension": "^5.1.5",
|
||||||
"mcp-handler": "^1.0.0",
|
"mcp-handler": "^1.0.0",
|
||||||
"next": "^15.3.5",
|
"next": "^15.3.5",
|
||||||
|
"prismjs": "^1.30.0",
|
||||||
"react": "^19.1.0",
|
"react": "^19.1.0",
|
||||||
"react-dom": "^19.1.0",
|
"react-dom": "^19.1.0",
|
||||||
"socket.io": "^4.8.1",
|
"socket.io": "^4.8.1",
|
||||||
|
|||||||
@@ -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
|
|
||||||
]
|
|
||||||
}
|
|
||||||
|
|
||||||
@@ -1,10 +1,6 @@
|
|||||||
"use client";
|
"use client";
|
||||||
import { useLocalCoursesSettingsQuery } from "@/features/local/course/localCoursesHooks";
|
import { useLocalCoursesSettingsQuery } from "@/hooks/localCourse/localCoursesHooks";
|
||||||
import {
|
import { getDateKey, getTermName, groupByStartDate } from "@/models/local/utils/timeUtils";
|
||||||
getDateKey,
|
|
||||||
getTermName,
|
|
||||||
groupByStartDate,
|
|
||||||
} from "@/features/local/utils/timeUtils";
|
|
||||||
import { getCourseUrl } from "@/services/urlUtils";
|
import { getCourseUrl } from "@/services/urlUtils";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
|
|
||||||
@@ -15,8 +11,6 @@ export default function CourseList() {
|
|||||||
|
|
||||||
const sortedDates = Object.keys(coursesByStartDate).sort();
|
const sortedDates = Object.keys(coursesByStartDate).sort();
|
||||||
|
|
||||||
console.log(allSettings, coursesByStartDate);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-row ">
|
<div className="flex flex-row ">
|
||||||
{sortedDates.map((startDate) => (
|
{sortedDates.map((startDate) => (
|
||||||
@@ -30,7 +24,6 @@ export default function CourseList() {
|
|||||||
<Link
|
<Link
|
||||||
href={getCourseUrl(settings.name)}
|
href={getCourseUrl(settings.name)}
|
||||||
shallow={true}
|
shallow={true}
|
||||||
prefetch={true}
|
|
||||||
className="
|
className="
|
||||||
font-bold text-xl block
|
font-bold text-xl block
|
||||||
transition-all hover:scale-105 hover:underline hover:text-slate-200
|
transition-all hover:scale-105 hover:underline hover:text-slate-200
|
||||||
|
|||||||
@@ -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>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -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>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -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
|
|
||||||
`
|
|
||||||
@@ -1,11 +1,10 @@
|
|||||||
import { assignmentMarkdownSerializer } from "@/features/local/assignments/models/utils/assignmentMarkdownSerializer";
|
import { assignmentMarkdownSerializer } from "@/models/local/assignment/utils/assignmentMarkdownSerializer";
|
||||||
import { groupByStartDate } from "@/features/local/utils/timeUtils";
|
import { LocalCourseSettings } from "@/models/local/localCourseSettings";
|
||||||
|
import { groupByStartDate } from "@/models/local/utils/timeUtils";
|
||||||
|
import { fileStorageService } from "@/services/fileStorage/fileStorageService";
|
||||||
|
import { ResourceTemplate } from "@modelcontextprotocol/sdk/server/mcp.js";
|
||||||
import { createMcpHandler } from "mcp-handler";
|
import { createMcpHandler } from "mcp-handler";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { githubClassroomUrlPrompt } from "./github-classroom-prompt";
|
|
||||||
import { courseItemFileStorageService } from "@/features/local/course/courseItemFileStorageService";
|
|
||||||
import { fileStorageService } from "@/features/local/utils/fileStorageService";
|
|
||||||
import { getModuleNamesFromFiles } from "@/features/local/modules/moduleRouter";
|
|
||||||
|
|
||||||
const handler = createMcpHandler(
|
const handler = createMcpHandler(
|
||||||
(server) => {
|
(server) => {
|
||||||
@@ -43,17 +42,17 @@ const handler = createMcpHandler(
|
|||||||
courseName: z.string(),
|
courseName: z.string(),
|
||||||
},
|
},
|
||||||
async ({ courseName }) => {
|
async ({ courseName }) => {
|
||||||
const modules = await getModuleNamesFromFiles(
|
const modules = await fileStorageService.modules.getModuleNames(
|
||||||
courseName
|
courseName
|
||||||
);
|
);
|
||||||
const assignments = (
|
const assignments = (
|
||||||
await Promise.all(
|
await Promise.all(
|
||||||
modules.map(async (moduleName) => {
|
modules.map(async (moduleName) => {
|
||||||
const assignments = await courseItemFileStorageService.getItems({
|
const assignments =
|
||||||
|
await fileStorageService.assignments.getAssignments(
|
||||||
courseName,
|
courseName,
|
||||||
moduleName,
|
moduleName
|
||||||
type: "Assignment",
|
);
|
||||||
});
|
|
||||||
return assignments.map((assignment) => ({
|
return assignments.map((assignment) => ({
|
||||||
assignmentName: assignment.name,
|
assignmentName: assignment.name,
|
||||||
moduleName,
|
moduleName,
|
||||||
@@ -102,12 +101,11 @@ const handler = createMcpHandler(
|
|||||||
"courseName, moduleName, and assignmentName must be strings"
|
"courseName, moduleName, and assignmentName must be strings"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
const assignment = await courseItemFileStorageService.getItem({
|
const assignment = await fileStorageService.assignments.getAssignment(
|
||||||
courseName,
|
courseName,
|
||||||
moduleName,
|
moduleName,
|
||||||
name: assignmentName,
|
assignmentName
|
||||||
type: "Assignment",
|
);
|
||||||
});
|
|
||||||
|
|
||||||
console.log("mcp assignment", assignment);
|
console.log("mcp assignment", assignment);
|
||||||
return {
|
return {
|
||||||
@@ -120,58 +118,42 @@ const handler = createMcpHandler(
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
server.tool(
|
|
||||||
"get_github_classroom_url_instructions",
|
server.registerResource(
|
||||||
"gets instructions for creating a GitHub Classroom assignment, call this to get a prompt showing how to create a GitHub Classroom assignment",
|
"course_assignment",
|
||||||
{},
|
new ResourceTemplate(
|
||||||
async () => {
|
"canvas:///courses/{courseName}/module/{moduleName}/assignments/{assignmentName}",
|
||||||
return {
|
{ list: undefined }
|
||||||
content: [
|
),
|
||||||
{
|
{
|
||||||
type: "text",
|
title: "Course Assignment",
|
||||||
text: githubClassroomUrlPrompt,
|
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,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
// resources dont integrate well right now
|
|
||||||
// server.registerResource(
|
|
||||||
// "course_assignment",
|
|
||||||
// new ResourceTemplate(
|
|
||||||
// "canvas:///courses/{courseName}/module/{moduleName}/assignments/{assignmentName}",
|
|
||||||
// { list: undefined }
|
|
||||||
// ),
|
|
||||||
// {
|
|
||||||
// title: "Course Assignment",
|
|
||||||
// description: "Markdown representation of a course assignment",
|
|
||||||
// },
|
|
||||||
// async (uri, { courseName, moduleName, assignmentName }) => {
|
|
||||||
// if (
|
|
||||||
// typeof courseName !== "string" ||
|
|
||||||
// typeof moduleName !== "string" ||
|
|
||||||
// typeof assignmentName !== "string"
|
|
||||||
// ) {
|
|
||||||
// throw new Error(
|
|
||||||
// "courseName, moduleName, and assignmentName must be strings"
|
|
||||||
// );
|
|
||||||
// }
|
|
||||||
// const assignment = await fileStorageService.assignments.getAssignment(
|
|
||||||
// courseName,
|
|
||||||
// moduleName,
|
|
||||||
// assignmentName
|
|
||||||
// );
|
|
||||||
// return {
|
|
||||||
// contents: [
|
|
||||||
// {
|
|
||||||
// uri: uri.href,
|
|
||||||
// text: assignment.description,
|
|
||||||
// },
|
|
||||||
// ],
|
|
||||||
// };
|
|
||||||
// }
|
|
||||||
// );
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
serverInfo: {
|
serverInfo: {
|
||||||
@@ -1,8 +1,10 @@
|
|||||||
import { createTrpcContext } from "@/services/serverFunctions/context";
|
import { createTrpcContext } from "@/services/serverFunctions/context";
|
||||||
import { trpcAppRouter } from "@/services/serverFunctions/appRouter";
|
import { trpcAppRouter } from "@/services/serverFunctions/router/app";
|
||||||
import { fetchRequestHandler } from "@trpc/server/adapters/fetch";
|
import { fetchRequestHandler } from "@trpc/server/adapters/fetch";
|
||||||
|
|
||||||
const handler = async (request: Request) => {
|
const handler = async (request: Request) => {
|
||||||
|
|
||||||
|
// await new Promise(r => setTimeout(r, 1000)); // delay for testing
|
||||||
return fetchRequestHandler({
|
return fetchRequestHandler({
|
||||||
endpoint: "/api/trpc",
|
endpoint: "/api/trpc",
|
||||||
req: request,
|
req: request,
|
||||||
|
|||||||
@@ -1,60 +1,25 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { useState, useEffect } from "react";
|
import { useState } from "react";
|
||||||
import CourseSettingsLink from "./CourseSettingsLink";
|
import CourseSettingsLink from "./CourseSettingsLink";
|
||||||
import ModuleList from "./modules/ModuleList";
|
import ModuleList from "./modules/ModuleList";
|
||||||
import LeftChevron from "@/components/icons/LeftChevron";
|
import LeftChevron from "@/components/icons/LeftChevron";
|
||||||
import RightChevron from "@/components/icons/RightChevron";
|
import RightChevron from "@/components/icons/RightChevron";
|
||||||
|
|
||||||
const collapseThreshold = 1400;
|
|
||||||
|
|
||||||
export default function CollapsableSidebar() {
|
export default function CollapsableSidebar() {
|
||||||
const [windowCollapseRecommended, setWindowCollapseRecommended] =
|
const [isCollapsed, setIsCollapsed] = useState(false);
|
||||||
useState(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 widthClass = isCollapsed ? "w-0" : "w-96";
|
||||||
|
const visibilityClass = isCollapsed ? "invisible " : "visible";
|
||||||
return (
|
return (
|
||||||
<div className="h-full flex flex-col">
|
<div className="h-full flex flex-col">
|
||||||
<div className="flex flex-row justify-between mb-2">
|
<div className="flex flex-row justify-between mb-2">
|
||||||
<div className="visible mx-3 mt-2">
|
<div className="visible mx-3 mt-2">
|
||||||
<button
|
<button onClick={() => setIsCollapsed((i) => !i)}>
|
||||||
onClick={() => {
|
{isCollapsed ? <LeftChevron /> : <RightChevron />}
|
||||||
setUserCollapsed((prev) => {
|
|
||||||
if (prev === "unset") {
|
|
||||||
return collapsed ? "uncollapsed" : "collapsed";
|
|
||||||
}
|
|
||||||
return prev === "collapsed" ? "uncollapsed" : "collapsed";
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{collapsed ? <LeftChevron /> : <RightChevron />}
|
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div className={" " + (collapsed ? "w-0 invisible hidden" : "")}>
|
<div className={" " + (isCollapsed ? "w-0 invisible hidden" : "")}>
|
||||||
<CourseSettingsLink />
|
<CourseSettingsLink />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,23 +1,23 @@
|
|||||||
"use client";
|
"use client";
|
||||||
import { Spinner } from "@/components/Spinner";
|
import { Spinner } from "@/components/Spinner";
|
||||||
import {
|
import {
|
||||||
useCanvasAssignmentsQuery,
|
|
||||||
canvasAssignmentKeys,
|
canvasAssignmentKeys,
|
||||||
} from "@/features/canvas/hooks/canvasAssignmentHooks";
|
useCanvasAssignmentsQuery,
|
||||||
import { canvasCourseKeys } from "@/features/canvas/hooks/canvasCourseHooks";
|
} from "@/hooks/canvas/canvasAssignmentHooks";
|
||||||
|
import { canvasCourseKeys } from "@/hooks/canvas/canvasCourseHooks";
|
||||||
import {
|
import {
|
||||||
useCanvasModulesQuery,
|
|
||||||
canvasCourseModuleKeys,
|
canvasCourseModuleKeys,
|
||||||
} from "@/features/canvas/hooks/canvasModuleHooks";
|
useCanvasModulesQuery,
|
||||||
|
} from "@/hooks/canvas/canvasModuleHooks";
|
||||||
import {
|
import {
|
||||||
useCanvasPagesQuery,
|
|
||||||
canvasPageKeys,
|
canvasPageKeys,
|
||||||
} from "@/features/canvas/hooks/canvasPageHooks";
|
useCanvasPagesQuery,
|
||||||
|
} from "@/hooks/canvas/canvasPageHooks";
|
||||||
import {
|
import {
|
||||||
useCanvasQuizzesQuery,
|
|
||||||
canvasQuizKeys,
|
canvasQuizKeys,
|
||||||
} from "@/features/canvas/hooks/canvasQuizHooks";
|
useCanvasQuizzesQuery,
|
||||||
import { useLocalCourseSettingsQuery } from "@/features/local/course/localCoursesHooks";
|
} from "@/hooks/canvas/canvasQuizHooks";
|
||||||
|
import { useLocalCourseSettingsQuery } from "@/hooks/localCourse/localCoursesHooks";
|
||||||
import { useQueryClient } from "@tanstack/react-query";
|
import { useQueryClient } from "@tanstack/react-query";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { useLocalCourseSettingsQuery } from "@/features/local/course/localCoursesHooks";
|
import { useLocalCourseSettingsQuery } from "@/hooks/localCourse/localCoursesHooks";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { useCourseContext } from "./context/courseContext";
|
import { useCourseContext } from "./context/courseContext";
|
||||||
import { getCourseSettingsUrl } from "@/services/urlUtils";
|
import { getCourseSettingsUrl } from "@/services/urlUtils";
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
"use client";
|
"use client";
|
||||||
import { CalendarMonthModel, getWeekNumber } from "./calendarMonthUtils";
|
import { CalendarMonthModel, getWeekNumber } from "./calendarMonthUtils";
|
||||||
|
import { DayOfWeek } from "@/models/local/localCourseSettings";
|
||||||
import { Expandable } from "@/components/Expandable";
|
import { Expandable } from "@/components/Expandable";
|
||||||
import { CalendarWeek } from "./CalendarWeek";
|
import { CalendarWeek } from "./CalendarWeek";
|
||||||
import { useLocalCourseSettingsQuery } from "@/features/local/course/localCoursesHooks";
|
import { useLocalCourseSettingsQuery } from "@/hooks/localCourse/localCoursesHooks";
|
||||||
import { getDateFromStringOrThrow } from "@/features/local/utils/timeUtils";
|
import { getDateFromStringOrThrow } from "@/models/local/utils/timeUtils";
|
||||||
import UpChevron from "@/components/icons/UpChevron";
|
import UpChevron from "@/components/icons/UpChevron";
|
||||||
import DownChevron from "@/components/icons/DownChevron";
|
import DownChevron from "@/components/icons/DownChevron";
|
||||||
import { DayOfWeek } from "@/features/local/course/localCourseSettings";
|
|
||||||
|
|
||||||
export const CalendarMonth = ({ month }: { month: CalendarMonthModel }) => {
|
export const CalendarMonth = ({ month }: { month: CalendarMonthModel }) => {
|
||||||
// const weekInMilliseconds = 604_800_000;
|
// const weekInMilliseconds = 604_800_000;
|
||||||
@@ -29,8 +29,7 @@ export const CalendarMonth = ({ month }: { month: CalendarMonthModel }) => {
|
|||||||
new Date(month.year, month.month, 1)
|
new Date(month.year, month.month, 1)
|
||||||
);
|
);
|
||||||
|
|
||||||
const shouldCollapse =
|
const shouldCollapse = (pastWeekNumber >= startOfMonthWeekNumber) && !isPastSemester;
|
||||||
pastWeekNumber >= startOfMonthWeekNumber && !isPastSemester;
|
|
||||||
|
|
||||||
const monthName = new Date(month.year, month.month - 1, 1).toLocaleString(
|
const monthName = new Date(month.year, month.month - 1, 1).toLocaleString(
|
||||||
"default",
|
"default",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
"use client";
|
"use client";
|
||||||
import { useLocalCourseSettingsQuery } from "@/features/local/course/localCoursesHooks";
|
import { useLocalCourseSettingsQuery } from "@/hooks/localCourse/localCoursesHooks";
|
||||||
import { getDateFromStringOrThrow } from "@/features/local/utils/timeUtils";
|
import { getDateFromStringOrThrow } from "@/models/local/utils/timeUtils";
|
||||||
import { getWeekNumber } from "./calendarMonthUtils";
|
import { getWeekNumber } from "./calendarMonthUtils";
|
||||||
import Day from "./day/Day";
|
import Day from "./day/Day";
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
"use client";
|
"use client";
|
||||||
import { getDateFromStringOrThrow } from "@/features/local/utils/timeUtils";
|
import { getDateFromStringOrThrow } from "@/models/local/utils/timeUtils";
|
||||||
import { getMonthsBetweenDates } from "./calendarMonthUtils";
|
import { getMonthsBetweenDates } from "./calendarMonthUtils";
|
||||||
import { CalendarMonth } from "./CalendarMonth";
|
import { CalendarMonth } from "./CalendarMonth";
|
||||||
import { useLocalCourseSettingsQuery } from "@/features/local/course/localCoursesHooks";
|
import { useLocalCourseSettingsQuery } from "@/hooks/localCourse/localCoursesHooks";
|
||||||
import { useEffect, useMemo, useRef } from "react";
|
import { useEffect, useMemo, useRef } from "react";
|
||||||
import CalendarItemsContextProvider from "../context/CalendarItemsContextProvider";
|
import CalendarItemsContextProvider from "../context/CalendarItemsContextProvider";
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import {
|
import {
|
||||||
dateToMarkdownString,
|
dateToMarkdownString,
|
||||||
getDateFromStringOrThrow,
|
getDateFromStringOrThrow,
|
||||||
} from "@/features/local/utils/timeUtils";
|
} from "@/models/local/utils/timeUtils";
|
||||||
|
|
||||||
export interface CalendarMonthModel {
|
export interface CalendarMonthModel {
|
||||||
year: number;
|
year: number;
|
||||||
|
|||||||
@@ -2,13 +2,13 @@
|
|||||||
import {
|
import {
|
||||||
getDateFromStringOrThrow,
|
getDateFromStringOrThrow,
|
||||||
getDateOnlyMarkdownString,
|
getDateOnlyMarkdownString,
|
||||||
} from "@/features/local/utils/timeUtils";
|
} from "@/models/local/utils/timeUtils";
|
||||||
import { useDraggingContext } from "../../context/drag/draggingContext";
|
import { useDraggingContext } from "../../context/drag/draggingContext";
|
||||||
import { useLocalCourseSettingsQuery } from "@/features/local/course/localCoursesHooks";
|
import { useLocalCourseSettingsQuery } from "@/hooks/localCourse/localCoursesHooks";
|
||||||
|
import { getDayOfWeek } from "@/models/local/localCourseSettings";
|
||||||
import { ItemInDay } from "./ItemInDay";
|
import { ItemInDay } from "./ItemInDay";
|
||||||
import { useTodaysItems } from "./useTodaysItems";
|
import { useTodaysItems } from "./useTodaysItems";
|
||||||
import { DayTitle } from "./DayTitle";
|
import { DayTitle } from "./DayTitle";
|
||||||
import { getDayOfWeek } from "@/features/local/course/localCourseSettings";
|
|
||||||
|
|
||||||
export default function Day({ day, month }: { day: string; month: number }) {
|
export default function Day({ day, month }: { day: string; month: number }) {
|
||||||
const dayAsDate = getDateFromStringOrThrow(
|
const dayAsDate = getDateFromStringOrThrow(
|
||||||
|
|||||||
@@ -5,8 +5,8 @@ import { useCourseContext } from "../../context/courseContext";
|
|||||||
import NewItemForm from "../../modules/NewItemForm";
|
import NewItemForm from "../../modules/NewItemForm";
|
||||||
import { DraggableItem } from "../../context/drag/draggingContext";
|
import { DraggableItem } from "../../context/drag/draggingContext";
|
||||||
import { useDragStyleContext } from "../../context/drag/dragStyleContext";
|
import { useDragStyleContext } from "../../context/drag/dragStyleContext";
|
||||||
import { getLectureForDay } from "@/features/local/utils/lectureUtils";
|
import { getLectureForDay } from "@/models/local/utils/lectureUtils";
|
||||||
import { useLecturesSuspenseQuery } from "@/features/local/lectures/lectureHooks";
|
import { useLecturesSuspenseQuery } from "@/hooks/localCourse/lectureHooks";
|
||||||
import ClientOnly from "@/components/ClientOnly";
|
import ClientOnly from "@/components/ClientOnly";
|
||||||
import { Tooltip } from "@/components/Tooltip";
|
import { Tooltip } from "@/components/Tooltip";
|
||||||
import { useRef, useState } from "react";
|
import { useRef, useState } from "react";
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { IModuleItem } from "@/features/local/modules/IModuleItem";
|
import { IModuleItem } from "@/models/local/IModuleItem";
|
||||||
import { getModuleItemUrl } from "@/services/urlUtils";
|
import { getModuleItemUrl } from "@/services/urlUtils";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { ReactNode, useRef, useState } from "react";
|
import { ReactNode, useRef, useState } from "react";
|
||||||
|
|||||||
@@ -1,18 +1,18 @@
|
|||||||
"use client";
|
"use client";
|
||||||
import { CanvasAssignment } from "@/features/canvas/models/assignments/canvasAssignment";
|
import { CanvasAssignment } from "@/models/canvas/assignments/canvasAssignment";
|
||||||
import { CanvasPage } from "@/features/canvas/models/pages/canvasPageModel";
|
import { CanvasPage } from "@/models/canvas/pages/canvasPageModel";
|
||||||
import { CanvasQuiz } from "@/features/canvas/models/quizzes/canvasQuizModel";
|
import { CanvasQuiz } from "@/models/canvas/quizzes/canvasQuizModel";
|
||||||
import { LocalAssignment } from "@/features/local/assignments/models/localAssignment";
|
import { LocalAssignment } from "@/models/local/assignment/localAssignment";
|
||||||
|
import { LocalCourseSettings } from "@/models/local/localCourseSettings";
|
||||||
|
import { LocalCoursePage } from "@/models/local/page/localCoursePage";
|
||||||
|
import { LocalQuiz } from "@/models/local/quiz/localQuiz";
|
||||||
import {
|
import {
|
||||||
dateToMarkdownString,
|
dateToMarkdownString,
|
||||||
getDateFromStringOrThrow,
|
getDateFromStringOrThrow,
|
||||||
} from "@/features/local/utils/timeUtils";
|
} from "@/models/local/utils/timeUtils";
|
||||||
import { markdownToHTMLSafe } from "@/services/htmlMarkdownUtils";
|
import { markdownToHTMLSafe } from "@/services/htmlMarkdownUtils";
|
||||||
import { htmlIsCloseEnough } from "@/services/utils/htmlIsCloseEnough";
|
import { htmlIsCloseEnough } from "@/services/utils/htmlIsCloseEnough";
|
||||||
import { ReactNode } from "react";
|
import { ReactNode } from "react";
|
||||||
import { LocalCoursePage } from "@/features/local/pages/localCoursePageModels";
|
|
||||||
import { LocalQuiz } from "@/features/local/quizzes/models/localQuiz";
|
|
||||||
import { LocalCourseSettings } from "@/features/local/course/localCourseSettings";
|
|
||||||
|
|
||||||
export const getStatus = ({
|
export const getStatus = ({
|
||||||
item,
|
item,
|
||||||
@@ -105,16 +105,7 @@ export const getStatus = ({
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
const htmlIsSame = htmlIsCloseEnough(
|
const htmlIsSame = htmlIsCloseEnough(
|
||||||
markdownToHTMLSafe({
|
markdownToHTMLSafe(assignment.description, settings),
|
||||||
markdownString: assignment.description,
|
|
||||||
settings,
|
|
||||||
replaceText: [
|
|
||||||
{
|
|
||||||
source: "insert_github_classroom_url",
|
|
||||||
destination: assignment.githubClassroomAssignmentShareLink || "",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
}),
|
|
||||||
canvasAssignment.description
|
canvasAssignment.description
|
||||||
);
|
);
|
||||||
if (!htmlIsSame)
|
if (!htmlIsSame)
|
||||||
|
|||||||
@@ -1,19 +1,18 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
import { useCanvasAssignmentsQuery } from "@/hooks/canvas/canvasAssignmentHooks";
|
||||||
import { LocalAssignment } from "@/features/local/assignments/models/localAssignment";
|
import { useCanvasPagesQuery } from "@/hooks/canvas/canvasPageHooks";
|
||||||
|
import { useCanvasQuizzesQuery } from "@/hooks/canvas/canvasQuizHooks";
|
||||||
|
import { LocalAssignment } from "@/models/local/assignment/localAssignment";
|
||||||
|
import { LocalCoursePage } from "@/models/local/page/localCoursePage";
|
||||||
|
import { LocalQuiz } from "@/models/local/quiz/localQuiz";
|
||||||
import {
|
import {
|
||||||
getDateFromStringOrThrow,
|
getDateFromStringOrThrow,
|
||||||
getDateOnlyMarkdownString,
|
getDateOnlyMarkdownString,
|
||||||
} from "@/features/local/utils/timeUtils";
|
} from "@/models/local/utils/timeUtils";
|
||||||
import { ReactNode } from "react";
|
import { ReactNode } from "react";
|
||||||
import { useCalendarItemsContext } from "../../context/calendarItemsContext";
|
import { useCalendarItemsContext } from "../../context/calendarItemsContext";
|
||||||
import { getStatus } from "./getStatus";
|
import { getStatus } from "./getStatus";
|
||||||
import { useLocalCourseSettingsQuery } from "@/features/local/course/localCoursesHooks";
|
import { useLocalCourseSettingsQuery } from "@/hooks/localCourse/localCoursesHooks";
|
||||||
import { LocalCoursePage } from "@/features/local/pages/localCoursePageModels";
|
|
||||||
import { LocalQuiz } from "@/features/local/quizzes/models/localQuiz";
|
|
||||||
import { useCanvasAssignmentsQuery } from "@/features/canvas/hooks/canvasAssignmentHooks";
|
|
||||||
import { useCanvasPagesQuery } from "@/features/canvas/hooks/canvasPageHooks";
|
|
||||||
import { useCanvasQuizzesQuery } from "@/features/canvas/hooks/canvasQuizHooks";
|
|
||||||
|
|
||||||
export function useTodaysItems(day: string) {
|
export function useTodaysItems(day: string) {
|
||||||
const { data: settings } = useLocalCourseSettingsQuery();
|
const { data: settings } = useLocalCourseSettingsQuery();
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
"use client";
|
"use client"
|
||||||
import { ReactNode } from "react";
|
import { ReactNode } from "react";
|
||||||
import {
|
import {
|
||||||
CalendarItemsContext,
|
CalendarItemsContext,
|
||||||
@@ -8,7 +8,7 @@ import {
|
|||||||
useCourseQuizzesByModuleByDateQuery,
|
useCourseQuizzesByModuleByDateQuery,
|
||||||
useCourseAssignmentsByModuleByDateQuery,
|
useCourseAssignmentsByModuleByDateQuery,
|
||||||
useCoursePagesByModuleByDateQuery,
|
useCoursePagesByModuleByDateQuery,
|
||||||
} from "@/features/local/modules/localCourseModuleHooks";
|
} from "@/hooks/localCourse/localCourseModuleHooks";
|
||||||
|
|
||||||
export default function CalendarItemsContextProvider({
|
export default function CalendarItemsContextProvider({
|
||||||
children,
|
children,
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { LocalAssignment } from "@/features/local/assignments/models/localAssignment";
|
import { LocalAssignment } from "@/models/local/assignment/localAssignment";
|
||||||
import { LocalCoursePage } from "@/features/local/pages/localCoursePageModels";
|
import { LocalCoursePage } from "@/models/local/page/localCoursePage";
|
||||||
import { LocalQuiz } from "@/features/local/quizzes/models/localQuiz";
|
import { LocalQuiz } from "@/models/local/quiz/localQuiz";
|
||||||
import { createContext, useContext } from "react";
|
import { createContext, useContext } from "react";
|
||||||
|
|
||||||
export interface CalendarItemsInterface {
|
export interface CalendarItemsInterface {
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
"use client";
|
"use client";
|
||||||
import { IModuleItem } from "@/features/local/modules/IModuleItem";
|
import { IModuleItem } from "@/models/local/IModuleItem";
|
||||||
import { createContext, useContext, DragEvent } from "react";
|
import { createContext, useContext, DragEvent } from "react";
|
||||||
|
|
||||||
export interface DraggableItem {
|
export interface DraggableItem {
|
||||||
|
|||||||
@@ -1,8 +1,6 @@
|
|||||||
"use client";
|
"use client";
|
||||||
import {
|
import { getDateFromStringOrThrow, dateToMarkdownString } from "@/models/local/utils/timeUtils";
|
||||||
getDateFromStringOrThrow,
|
|
||||||
dateToMarkdownString,
|
|
||||||
} from "@/features/local/utils/timeUtils";
|
|
||||||
|
|
||||||
export function getNewLockDate(
|
export function getNewLockDate(
|
||||||
originalDueDate: string,
|
originalDueDate: string,
|
||||||
@@ -11,16 +9,13 @@ export function getNewLockDate(
|
|||||||
): string | undefined {
|
): string | undefined {
|
||||||
// todo: preserve previous due date / lock date offset
|
// todo: preserve previous due date / lock date offset
|
||||||
const dueDate = getDateFromStringOrThrow(originalDueDate, "dueAt date");
|
const dueDate = getDateFromStringOrThrow(originalDueDate, "dueAt date");
|
||||||
const lockDate =
|
const lockDate = originalLockDate === undefined
|
||||||
originalLockDate === undefined
|
|
||||||
? undefined
|
? undefined
|
||||||
: getDateFromStringOrThrow(originalLockDate, "lockAt date");
|
: getDateFromStringOrThrow(originalLockDate, "lockAt date");
|
||||||
|
|
||||||
const originalOffset =
|
const originalOffset = lockDate === undefined ? undefined : lockDate.getTime() - dueDate.getTime();
|
||||||
lockDate === undefined ? undefined : lockDate.getTime() - dueDate.getTime();
|
|
||||||
|
|
||||||
const newLockDate =
|
const newLockDate = originalOffset === undefined
|
||||||
originalOffset === undefined
|
|
||||||
? undefined
|
? undefined
|
||||||
: new Date(dayAsDate.getTime() + originalOffset);
|
: new Date(dayAsDate.getTime() + originalOffset);
|
||||||
|
|
||||||
|
|||||||
@@ -1,26 +1,26 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
import { useUpdateAssignmentMutation } from "@/hooks/localCourse/assignmentHooks";
|
||||||
import {
|
import {
|
||||||
useLecturesSuspenseQuery,
|
useLecturesSuspenseQuery,
|
||||||
useLectureUpdateMutation,
|
useLectureUpdateMutation,
|
||||||
} from "@/features/local/lectures/lectureHooks";
|
} from "@/hooks/localCourse/lectureHooks";
|
||||||
import { useLocalCourseSettingsQuery } from "@/features/local/course/localCoursesHooks";
|
import { useLocalCourseSettingsQuery } from "@/hooks/localCourse/localCoursesHooks";
|
||||||
import { useUpdatePageMutation } from "@/features/local/pages/pageHooks";
|
import { useUpdatePageMutation } from "@/hooks/localCourse/pageHooks";
|
||||||
import { LocalAssignment } from "@/features/local/assignments/models/localAssignment";
|
import { LocalAssignment } from "@/models/local/assignment/localAssignment";
|
||||||
import { Lecture } from "@/features/local/lectures/lectureModel";
|
import { Lecture } from "@/models/local/lecture";
|
||||||
import { getLectureForDay } from "@/features/local/utils/lectureUtils";
|
import { getLectureForDay } from "@/models/local/utils/lectureUtils";
|
||||||
|
import { LocalCoursePage } from "@/models/local/page/localCoursePage";
|
||||||
|
import { LocalQuiz } from "@/models/local/quiz/localQuiz";
|
||||||
import {
|
import {
|
||||||
getDateFromStringOrThrow,
|
getDateFromStringOrThrow,
|
||||||
getDateOnlyMarkdownString,
|
getDateOnlyMarkdownString,
|
||||||
dateToMarkdownString,
|
dateToMarkdownString,
|
||||||
} from "@/features/local/utils/timeUtils";
|
} from "@/models/local/utils/timeUtils";
|
||||||
import { Dispatch, SetStateAction, useCallback, DragEvent } from "react";
|
import { Dispatch, SetStateAction, useCallback, DragEvent } from "react";
|
||||||
import { DraggableItem } from "./draggingContext";
|
import { DraggableItem } from "./draggingContext";
|
||||||
import { getNewLockDate } from "./getNewLockDate";
|
import { getNewLockDate } from "./getNewLockDate";
|
||||||
import { useUpdateQuizMutation } from "@/features/local/quizzes/quizHooks";
|
import { useUpdateQuizMutation } from "@/hooks/localCourse/quizHooks";
|
||||||
import { useCourseContext } from "../courseContext";
|
import { useCourseContext } from "../courseContext";
|
||||||
import { useUpdateAssignmentMutation } from "@/features/local/assignments/assignmentHooks";
|
|
||||||
import { LocalCoursePage } from "@/features/local/pages/localCoursePageModels";
|
|
||||||
import { LocalQuiz } from "@/features/local/quizzes/models/localQuiz";
|
|
||||||
|
|
||||||
export function useItemDropOnDay({
|
export function useItemDropOnDay({
|
||||||
setIsDragging,
|
setIsDragging,
|
||||||
|
|||||||
@@ -1,13 +1,13 @@
|
|||||||
"use client";
|
"use client";
|
||||||
import { useUpdatePageMutation } from "@/features/local/pages/pageHooks";
|
import { useUpdateAssignmentMutation } from "@/hooks/localCourse/assignmentHooks";
|
||||||
import { LocalAssignment } from "@/features/local/assignments/models/localAssignment";
|
import { useUpdatePageMutation } from "@/hooks/localCourse/pageHooks";
|
||||||
|
import { LocalAssignment } from "@/models/local/assignment/localAssignment";
|
||||||
|
import { LocalCoursePage } from "@/models/local/page/localCoursePage";
|
||||||
|
import { LocalQuiz } from "@/models/local/quiz/localQuiz";
|
||||||
import { Dispatch, SetStateAction, useCallback, DragEvent } from "react";
|
import { Dispatch, SetStateAction, useCallback, DragEvent } from "react";
|
||||||
import { DraggableItem } from "./draggingContext";
|
import { DraggableItem } from "./draggingContext";
|
||||||
import { useCourseContext } from "../courseContext";
|
import { useCourseContext } from "../courseContext";
|
||||||
import { useUpdateQuizMutation } from "@/features/local/quizzes/quizHooks";
|
import { useUpdateQuizMutation } from "@/hooks/localCourse/quizHooks";
|
||||||
import { useUpdateAssignmentMutation } from "@/features/local/assignments/assignmentHooks";
|
|
||||||
import { LocalCoursePage } from "@/features/local/pages/localCoursePageModels";
|
|
||||||
import { LocalQuiz } from "@/features/local/quizzes/models/localQuiz";
|
|
||||||
|
|
||||||
export function useItemDropOnModule({
|
export function useItemDropOnModule({
|
||||||
setIsDragging,
|
setIsDragging,
|
||||||
|
|||||||
@@ -9,9 +9,8 @@ export async function generateMetadata({
|
|||||||
params: Promise<{ courseName: string }>;
|
params: Promise<{ courseName: string }>;
|
||||||
}): Promise<Metadata> {
|
}): Promise<Metadata> {
|
||||||
const { courseName } = await params;
|
const { courseName } = await params;
|
||||||
const decodedCourseName = decodeURIComponent(getTitle(courseName));
|
|
||||||
return {
|
return {
|
||||||
title: decodedCourseName,
|
title: getTitle(courseName),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -3,18 +3,18 @@ import { MonacoEditor } from "@/components/editor/MonacoEditor";
|
|||||||
import {
|
import {
|
||||||
useLecturesSuspenseQuery,
|
useLecturesSuspenseQuery,
|
||||||
useLectureUpdateMutation,
|
useLectureUpdateMutation,
|
||||||
} from "@/features/local/lectures/lectureHooks";
|
} from "@/hooks/localCourse/lectureHooks";
|
||||||
import {
|
import {
|
||||||
lectureToString,
|
lectureToString,
|
||||||
parseLecture,
|
parseLecture,
|
||||||
} from "@/features/local/lectures/lectureUtils";
|
} from "@/services/fileStorage/utils/lectureUtils";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import LecturePreview from "./LecturePreview";
|
import LecturePreview from "./LecturePreview";
|
||||||
import EditLectureTitle from "./EditLectureTitle";
|
import EditLectureTitle from "./EditLectureTitle";
|
||||||
import LectureButtons from "./LectureButtons";
|
import LectureButtons from "./LectureButtons";
|
||||||
import { useCourseContext } from "../../context/courseContext";
|
import { useCourseContext } from "../../context/courseContext";
|
||||||
import { useLocalCourseSettingsQuery } from "@/features/local/course/localCoursesHooks";
|
import { useLocalCourseSettingsQuery } from "@/hooks/localCourse/localCoursesHooks";
|
||||||
import { Lecture } from "@/features/local/lectures/lectureModel";
|
import { Lecture } from "@/models/local/lecture";
|
||||||
import { useAuthoritativeUpdates } from "../../utils/useAuthoritativeUpdates";
|
import { useAuthoritativeUpdates } from "../../utils/useAuthoritativeUpdates";
|
||||||
import { EditLayout } from "@/components/EditLayout";
|
import { EditLayout } from "@/components/EditLayout";
|
||||||
|
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
import { useLocalCourseSettingsQuery } from "@/features/local/course/localCoursesHooks";
|
import { useLocalCourseSettingsQuery } from "@/hooks/localCourse/localCoursesHooks";
|
||||||
import { getDateFromString } from "@/features/local/utils/timeUtils";
|
import { getDayOfWeek } from "@/models/local/localCourseSettings";
|
||||||
import { getLectureWeekName } from "@/features/local/lectures/lectureUtils";
|
import { getDateFromString } from "@/models/local/utils/timeUtils";
|
||||||
|
import { getLectureWeekName } from "@/services/fileStorage/utils/lectureUtils";
|
||||||
import { getCourseUrl, getLecturePreviewUrl } from "@/services/urlUtils";
|
import { getCourseUrl, getLecturePreviewUrl } from "@/services/urlUtils";
|
||||||
import { useCourseContext } from "../../context/courseContext";
|
import { useCourseContext } from "../../context/courseContext";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { getDayOfWeek } from "@/features/local/course/localCourseSettings";
|
|
||||||
|
|
||||||
export default function EditLectureTitle({
|
export default function EditLectureTitle({
|
||||||
lectureDay,
|
lectureDay,
|
||||||
@@ -22,7 +22,6 @@ export default function EditLectureTitle({
|
|||||||
className="btn hidden sm:inline"
|
className="btn hidden sm:inline"
|
||||||
href={getCourseUrl(courseName)}
|
href={getCourseUrl(courseName)}
|
||||||
shallow={true}
|
shallow={true}
|
||||||
prefetch={true}
|
|
||||||
>
|
>
|
||||||
{courseName}
|
{courseName}
|
||||||
</Link>
|
</Link>
|
||||||
|
|||||||
@@ -6,8 +6,8 @@ import { getCourseUrl } from "@/services/urlUtils";
|
|||||||
import { useRouter } from "next/navigation";
|
import { useRouter } from "next/navigation";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { useCourseContext } from "../../context/courseContext";
|
import { useCourseContext } from "../../context/courseContext";
|
||||||
import { useLocalCourseSettingsQuery } from "@/features/local/course/localCoursesHooks";
|
import { useLocalCourseSettingsQuery } from "@/hooks/localCourse/localCoursesHooks";
|
||||||
import { useDeleteLectureMutation } from "@/features/local/lectures/lectureHooks";
|
import { useDeleteLectureMutation } from "@/hooks/localCourse/lectureHooks";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
|
|
||||||
export default function LectureButtons({ lectureDay }: { lectureDay: string }) {
|
export default function LectureButtons({ lectureDay }: { lectureDay: string }) {
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import MarkdownDisplay from "@/components/MarkdownDisplay";
|
import MarkdownDisplay from "@/components/MarkdownDisplay";
|
||||||
import { Lecture } from "@/features/local/lectures/lectureModel";
|
import { Lecture } from "@/models/local/lecture";
|
||||||
|
|
||||||
export default function LecturePreview({ lecture }: { lecture: Lecture }) {
|
export default function LecturePreview({ lecture }: { lecture: Lecture }) {
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -11,9 +11,8 @@ export async function generateMetadata({
|
|||||||
const { courseName, lectureDay } = await params;
|
const { courseName, lectureDay } = await params;
|
||||||
const decodedDay = decodeURIComponent(lectureDay);
|
const decodedDay = decodeURIComponent(lectureDay);
|
||||||
const dayOnly = decodedDay.split(" ")[0];
|
const dayOnly = decodedDay.split(" ")[0];
|
||||||
const decodedCourseName = decodeURIComponent(getTitle(courseName));
|
|
||||||
return {
|
return {
|
||||||
title: getTitle(`${decodedCourseName} lecture ${dayOnly}`),
|
title: getTitle(`${courseName} lecture ${dayOnly}`),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import EditLecture from "./EditLecture";
|
|||||||
import {
|
import {
|
||||||
getDateFromStringOrThrow,
|
getDateFromStringOrThrow,
|
||||||
getDateOnlyMarkdownString,
|
getDateOnlyMarkdownString,
|
||||||
} from "@/features/local/utils/timeUtils";
|
} from "@/models/local/utils/timeUtils";
|
||||||
export const dynamic = "force-dynamic";
|
export const dynamic = "force-dynamic";
|
||||||
|
|
||||||
export default async function page({
|
export default async function page({
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import LecturePreview from "../LecturePreview";
|
|||||||
import { getCourseUrl, getLectureUrl } from "@/services/urlUtils";
|
import { getCourseUrl, getLectureUrl } from "@/services/urlUtils";
|
||||||
import { useCourseContext } from "../../../context/courseContext";
|
import { useCourseContext } from "../../../context/courseContext";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { useLecturesSuspenseQuery } from "@/features/local/lectures/lectureHooks";
|
import { useLecturesSuspenseQuery } from "@/hooks/localCourse/lectureHooks";
|
||||||
|
|
||||||
export default function LecturePreviewPage({
|
export default function LecturePreviewPage({
|
||||||
lectureDay,
|
lectureDay,
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import {
|
import {
|
||||||
getDateFromStringOrThrow,
|
getDateFromStringOrThrow,
|
||||||
getDateOnlyMarkdownString,
|
getDateOnlyMarkdownString,
|
||||||
} from "@/features/local/utils/timeUtils";
|
} from "@/models/local/utils/timeUtils";
|
||||||
import LecturePreviewPage from "./LecturePreviewPage";
|
import LecturePreviewPage from "./LecturePreviewPage";
|
||||||
export const dynamic = "force-dynamic";
|
export const dynamic = "force-dynamic";
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { Expandable } from "@/components/Expandable";
|
import { Expandable } from "@/components/Expandable";
|
||||||
import TextInput from "@/components/form/TextInput";
|
import TextInput from "@/components/form/TextInput";
|
||||||
import { useCreateModuleMutation } from "@/features/local/modules/localCourseModuleHooks";
|
import { useCreateModuleMutation } from "@/hooks/localCourse/localCourseModuleHooks";
|
||||||
import React, { useState } from "react";
|
import React, { useState } from "react";
|
||||||
import { useCourseContext } from "../context/courseContext";
|
import { useCourseContext } from "../context/courseContext";
|
||||||
|
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
"use client";
|
"use client";
|
||||||
import { usePagesQueries } from "@/features/local/pages/pageHooks";
|
import { usePagesQueries } from "@/hooks/localCourse/pageHooks";
|
||||||
import { IModuleItem } from "@/features/local/modules/IModuleItem";
|
import { IModuleItem } from "@/models/local/IModuleItem";
|
||||||
import {
|
import {
|
||||||
getDateFromString,
|
getDateFromString,
|
||||||
getDateFromStringOrThrow,
|
getDateFromStringOrThrow,
|
||||||
getDateOnlyMarkdownString,
|
getDateOnlyMarkdownString,
|
||||||
} from "@/features/local/utils/timeUtils";
|
} from "@/models/local/utils/timeUtils";
|
||||||
import { Fragment } from "react";
|
import { Fragment } from "react";
|
||||||
import Modal, { useModal } from "../../../../components/Modal";
|
import Modal, { useModal } from "../../../../components/Modal";
|
||||||
import NewItemForm from "./NewItemForm";
|
import NewItemForm from "./NewItemForm";
|
||||||
@@ -21,13 +21,10 @@ import { getModuleItemUrl } from "@/services/urlUtils";
|
|||||||
import { useCourseContext } from "../context/courseContext";
|
import { useCourseContext } from "../context/courseContext";
|
||||||
import { Expandable } from "../../../../components/Expandable";
|
import { Expandable } from "../../../../components/Expandable";
|
||||||
import { useDragStyleContext } from "../context/drag/dragStyleContext";
|
import { useDragStyleContext } from "../context/drag/dragStyleContext";
|
||||||
import { useQuizzesQueries } from "@/features/local/quizzes/quizHooks";
|
import { useQuizzesQueries } from "@/hooks/localCourse/quizHooks";
|
||||||
|
import { useAssignmentNamesQuery } from "@/hooks/localCourse/assignmentHooks";
|
||||||
import { useTRPC } from "@/services/serverFunctions/trpcClient";
|
import { useTRPC } from "@/services/serverFunctions/trpcClient";
|
||||||
import { useSuspenseQueries } from "@tanstack/react-query";
|
import { useSuspenseQueries } from "@tanstack/react-query";
|
||||||
import { useAssignmentNamesQuery } from "@/features/local/assignments/assignmentHooks";
|
|
||||||
import { useReorderCanvasModuleItemsMutation } from "@/features/canvas/hooks/canvasModuleHooks";
|
|
||||||
import { useCanvasModulesQuery } from "@/features/canvas/hooks/canvasModuleHooks";
|
|
||||||
import { Spinner } from "@/components/Spinner";
|
|
||||||
|
|
||||||
export default function ExpandableModule({
|
export default function ExpandableModule({
|
||||||
moduleName,
|
moduleName,
|
||||||
@@ -53,8 +50,6 @@ export default function ExpandableModule({
|
|||||||
const { data: quizzes } = useQuizzesQueries(moduleName);
|
const { data: quizzes } = useQuizzesQueries(moduleName);
|
||||||
const { data: pages } = usePagesQueries(moduleName);
|
const { data: pages } = usePagesQueries(moduleName);
|
||||||
const modal = useModal();
|
const modal = useModal();
|
||||||
const reorderMutation = useReorderCanvasModuleItemsMutation();
|
|
||||||
const { data: canvasModules } = useCanvasModulesQuery();
|
|
||||||
|
|
||||||
const moduleItems: {
|
const moduleItems: {
|
||||||
type: "assignment" | "quiz" | "page";
|
type: "assignment" | "quiz" | "page";
|
||||||
@@ -115,30 +110,6 @@ export default function ExpandableModule({
|
|||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<>
|
<>
|
||||||
{!reorderMutation.isPending && (
|
|
||||||
<button
|
|
||||||
className=" me-3"
|
|
||||||
onClick={() => {
|
|
||||||
const canvasModuleId = canvasModules?.find(
|
|
||||||
(m) => m.name === moduleName
|
|
||||||
)?.id;
|
|
||||||
if (!canvasModuleId) {
|
|
||||||
console.error(
|
|
||||||
"Canvas module ID not found for",
|
|
||||||
moduleName
|
|
||||||
);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
reorderMutation.mutate({
|
|
||||||
moduleId: canvasModuleId,
|
|
||||||
items: moduleItems.map((item) => item.item),
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Sort by Due Date
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
{reorderMutation.isPending && <Spinner />}
|
|
||||||
<Modal
|
<Modal
|
||||||
modalControl={modal}
|
modalControl={modal}
|
||||||
buttonText="New Item"
|
buttonText="New Item"
|
||||||
|
|||||||
@@ -2,9 +2,9 @@
|
|||||||
import CheckIcon from "@/components/icons/CheckIcon";
|
import CheckIcon from "@/components/icons/CheckIcon";
|
||||||
import { Spinner } from "@/components/Spinner";
|
import { Spinner } from "@/components/Spinner";
|
||||||
import {
|
import {
|
||||||
useCanvasModulesQuery,
|
|
||||||
useAddCanvasModuleMutation,
|
useAddCanvasModuleMutation,
|
||||||
} from "@/features/canvas/hooks/canvasModuleHooks";
|
useCanvasModulesQuery,
|
||||||
|
} from "@/hooks/canvas/canvasModuleHooks";
|
||||||
|
|
||||||
export function ModuleCanvasStatus({ moduleName }: { moduleName: string }) {
|
export function ModuleCanvasStatus({ moduleName }: { moduleName: string }) {
|
||||||
const { data: canvasModules } = useCanvasModulesQuery();
|
const { data: canvasModules } = useCanvasModulesQuery();
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
"use client";
|
"use client";
|
||||||
import { useModuleNamesQuery } from "@/features/local/modules/localCourseModuleHooks";
|
import { useModuleNamesQuery } from "@/hooks/localCourse/localCourseModuleHooks";
|
||||||
import ExpandableModule from "./ExpandableModule";
|
import ExpandableModule from "./ExpandableModule";
|
||||||
import CreateModule from "./CreateModule";
|
import CreateModule from "./CreateModule";
|
||||||
|
|
||||||
|
|||||||
@@ -3,20 +3,20 @@ import ButtonSelect from "@/components/ButtonSelect";
|
|||||||
import SelectInput from "@/components/form/SelectInput";
|
import SelectInput from "@/components/form/SelectInput";
|
||||||
import TextInput from "@/components/form/TextInput";
|
import TextInput from "@/components/form/TextInput";
|
||||||
import { Spinner } from "@/components/Spinner";
|
import { Spinner } from "@/components/Spinner";
|
||||||
import { useModuleNamesQuery } from "@/features/local/modules/localCourseModuleHooks";
|
import { useCreateAssignmentMutation } from "@/hooks/localCourse/assignmentHooks";
|
||||||
import { useLocalCourseSettingsQuery } from "@/features/local/course/localCoursesHooks";
|
import { useModuleNamesQuery } from "@/hooks/localCourse/localCourseModuleHooks";
|
||||||
import { useCreatePageMutation } from "@/features/local/pages/pageHooks";
|
import { useLocalCourseSettingsQuery } from "@/hooks/localCourse/localCoursesHooks";
|
||||||
import { LocalAssignmentGroup } from "@/features/local/assignments/models/localAssignmentGroup";
|
import { useCreatePageMutation } from "@/hooks/localCourse/pageHooks";
|
||||||
|
import { LocalAssignmentGroup } from "@/models/local/assignment/localAssignmentGroup";
|
||||||
|
|
||||||
import React, { useState } from "react";
|
import React, { useState } from "react";
|
||||||
import { useCourseContext } from "../context/courseContext";
|
import { useCourseContext } from "../context/courseContext";
|
||||||
import { useCreateQuizMutation } from "@/features/local/quizzes/quizHooks";
|
import { useCreateQuizMutation } from "@/hooks/localCourse/quizHooks";
|
||||||
import {
|
import {
|
||||||
getDateFromString,
|
getDateFromString,
|
||||||
dateToMarkdownString,
|
dateToMarkdownString,
|
||||||
getDateFromStringOrThrow,
|
getDateFromStringOrThrow,
|
||||||
} from "@/features/local/utils/timeUtils";
|
} from "@/models/local/utils/timeUtils";
|
||||||
import { useCreateAssignmentMutation } from "@/features/local/assignments/assignmentHooks";
|
|
||||||
|
|
||||||
export default function NewItemForm({
|
export default function NewItemForm({
|
||||||
moduleName: defaultModuleName,
|
moduleName: defaultModuleName,
|
||||||
@@ -153,9 +153,9 @@ export default function NewItemForm({
|
|||||||
<div>
|
<div>
|
||||||
<ButtonSelect<"Assignment" | "Quiz" | "Page">
|
<ButtonSelect<"Assignment" | "Quiz" | "Page">
|
||||||
options={["Assignment", "Quiz", "Page"]}
|
options={["Assignment", "Quiz", "Page"]}
|
||||||
getOptionName={(o) => o?.toString() ?? ""}
|
getName={(o) => o?.toString() ?? ""}
|
||||||
setValue={(t) => setType(t ?? "Assignment")}
|
setSelectedOption={(t) => setType(t ?? "Assignment")}
|
||||||
value={type}
|
selectedOption={type}
|
||||||
label="Type"
|
label="Type"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -166,9 +166,9 @@ export default function NewItemForm({
|
|||||||
{type !== "Page" && (
|
{type !== "Page" && (
|
||||||
<ButtonSelect
|
<ButtonSelect
|
||||||
options={settings.assignmentGroups}
|
options={settings.assignmentGroups}
|
||||||
getOptionName={(g) => g?.name ?? ""}
|
getName={(g) => g?.name ?? ""}
|
||||||
setValue={setAssignmentGroup}
|
setSelectedOption={setAssignmentGroup}
|
||||||
value={assignmentGroup}
|
selectedOption={assignmentGroup}
|
||||||
label="Assignment Group"
|
label="Assignment Group"
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -6,13 +6,13 @@ import {
|
|||||||
useAddAssignmentToCanvasMutation,
|
useAddAssignmentToCanvasMutation,
|
||||||
useDeleteAssignmentFromCanvasMutation,
|
useDeleteAssignmentFromCanvasMutation,
|
||||||
useUpdateAssignmentInCanvasMutation,
|
useUpdateAssignmentInCanvasMutation,
|
||||||
} from "@/features/canvas/hooks/canvasAssignmentHooks";
|
} from "@/hooks/canvas/canvasAssignmentHooks";
|
||||||
import { baseCanvasUrl } from "@/features/canvas/services/canvasServiceUtils";
|
|
||||||
import {
|
import {
|
||||||
useAssignmentQuery,
|
useAssignmentQuery,
|
||||||
useDeleteAssignmentMutation,
|
useDeleteAssignmentMutation,
|
||||||
} from "@/features/local/assignments/assignmentHooks";
|
} from "@/hooks/localCourse/assignmentHooks";
|
||||||
import { useLocalCourseSettingsQuery } from "@/features/local/course/localCoursesHooks";
|
import { useLocalCourseSettingsQuery } from "@/hooks/localCourse/localCoursesHooks";
|
||||||
|
import { baseCanvasUrl } from "@/services/canvas/canvasServiceUtils";
|
||||||
import { getCourseUrl } from "@/services/urlUtils";
|
import { getCourseUrl } from "@/services/urlUtils";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { useRouter } from "next/navigation";
|
import { useRouter } from "next/navigation";
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import MarkdownDisplay from "@/components/MarkdownDisplay";
|
import MarkdownDisplay from "@/components/MarkdownDisplay";
|
||||||
import { LocalAssignment } from "@/features/local/assignments/models/localAssignment";
|
import { LocalAssignment } from "@/models/local/assignment/localAssignment";
|
||||||
import { rubricItemIsExtraCredit } from "@/features/local/assignments/models/rubricItem";
|
import { rubricItemIsExtraCredit } from "@/models/local/assignment/rubricItem";
|
||||||
import { assignmentPoints } from "@/features/local/assignments/models/utils/assignmentPointsUtils";
|
import { assignmentPoints } from "@/models/local/assignment/utils/assignmentPointsUtils";
|
||||||
import { formatHumanReadableDate } from "@/services/utils/dateFormat";
|
import { formatHumanReadableDate } from "@/services/utils/dateFormat";
|
||||||
import React, { Fragment } from "react";
|
import React, { Fragment } from "react";
|
||||||
|
|
||||||
@@ -59,15 +59,13 @@ export default function AssignmentPreview({
|
|||||||
<hr />
|
<hr />
|
||||||
<br />
|
<br />
|
||||||
<section>
|
<section>
|
||||||
<MarkdownDisplay
|
<MarkdownDisplay markdown={assignment.description} />
|
||||||
markdown={assignment.description}
|
{/* <div
|
||||||
replaceText={[
|
className="markdownPreview"
|
||||||
{
|
dangerouslySetInnerHTML={{
|
||||||
source: "insert_github_classroom_url",
|
__html: htmlPreview,
|
||||||
destination: assignment.githubClassroomAssignmentShareLink || "",
|
}}
|
||||||
},
|
></div> */}
|
||||||
]}
|
|
||||||
/>
|
|
||||||
</section>
|
</section>
|
||||||
<hr />
|
<hr />
|
||||||
<section>
|
<section>
|
||||||
|
|||||||
@@ -1,13 +1,18 @@
|
|||||||
"use client";
|
"use client";
|
||||||
import { MonacoEditor } from "@/components/editor/MonacoEditor";
|
import { MonacoEditor } from "@/components/editor/MonacoEditor";
|
||||||
|
import {
|
||||||
|
useAssignmentQuery,
|
||||||
|
useUpdateAssignmentMutation,
|
||||||
|
useUpdateImageSettingsForAssignment,
|
||||||
|
} from "@/hooks/localCourse/assignmentHooks";
|
||||||
import {
|
import {
|
||||||
LocalAssignment,
|
LocalAssignment,
|
||||||
localAssignmentMarkdown,
|
localAssignmentMarkdown,
|
||||||
} from "@/features/local/assignments/models/localAssignment";
|
} from "@/models/local/assignment/localAssignment";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import AssignmentPreview from "./AssignmentPreview";
|
import AssignmentPreview from "./AssignmentPreview";
|
||||||
import { useCourseContext } from "@/app/course/[courseName]/context/courseContext";
|
import { useCourseContext } from "@/app/course/[courseName]/context/courseContext";
|
||||||
import { useLocalCourseSettingsQuery } from "@/features/local/course/localCoursesHooks";
|
import { useLocalCourseSettingsQuery } from "@/hooks/localCourse/localCoursesHooks";
|
||||||
import ClientOnly from "@/components/ClientOnly";
|
import ClientOnly from "@/components/ClientOnly";
|
||||||
import { SuspenseAndErrorHandling } from "@/components/SuspenseAndErrorHandling";
|
import { SuspenseAndErrorHandling } from "@/components/SuspenseAndErrorHandling";
|
||||||
import { useRouter } from "next/navigation";
|
import { useRouter } from "next/navigation";
|
||||||
@@ -17,11 +22,6 @@ import EditAssignmentHeader from "./EditAssignmentHeader";
|
|||||||
import { Spinner } from "@/components/Spinner";
|
import { Spinner } from "@/components/Spinner";
|
||||||
import { getAssignmentHelpString } from "./getAssignmentHelpString";
|
import { getAssignmentHelpString } from "./getAssignmentHelpString";
|
||||||
import { EditLayout } from "@/components/EditLayout";
|
import { EditLayout } from "@/components/EditLayout";
|
||||||
import {
|
|
||||||
useAssignmentQuery,
|
|
||||||
useUpdateAssignmentMutation,
|
|
||||||
useUpdateImageSettingsForAssignment,
|
|
||||||
} from "@/features/local/assignments/assignmentHooks";
|
|
||||||
|
|
||||||
export default function EditAssignment({
|
export default function EditAssignment({
|
||||||
moduleName,
|
moduleName,
|
||||||
@@ -131,7 +131,7 @@ export default function EditAssignment({
|
|||||||
Body={
|
Body={
|
||||||
<>
|
<>
|
||||||
{showHelp && (
|
{showHelp && (
|
||||||
<div className=" max-w-96 flex-1 h-full overflow-y-auto">
|
<div className=" max-w-96">
|
||||||
<pre>
|
<pre>
|
||||||
<code>{getAssignmentHelpString(settings)}</code>
|
<code>{getAssignmentHelpString(settings)}</code>
|
||||||
</pre>
|
</pre>
|
||||||
|
|||||||
@@ -13,12 +13,7 @@ export default function EditAssignmentHeader({
|
|||||||
const { courseName } = useCourseContext();
|
const { courseName } = useCourseContext();
|
||||||
return (
|
return (
|
||||||
<div className="py-1 flex flex-row justify-start gap-3">
|
<div className="py-1 flex flex-row justify-start gap-3">
|
||||||
<Link
|
<Link className="btn" href={getCourseUrl(courseName)} shallow={true}>
|
||||||
className="btn"
|
|
||||||
href={getCourseUrl(courseName)}
|
|
||||||
shallow={true}
|
|
||||||
prefetch={true}
|
|
||||||
>
|
|
||||||
{courseName}
|
{courseName}
|
||||||
</Link>
|
</Link>
|
||||||
<UpdateAssignmentName
|
<UpdateAssignmentName
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import { Spinner } from "@/components/Spinner";
|
|||||||
import {
|
import {
|
||||||
useAssignmentQuery,
|
useAssignmentQuery,
|
||||||
useUpdateAssignmentMutation,
|
useUpdateAssignmentMutation,
|
||||||
} from "@/features/local/assignments/assignmentHooks";
|
} from "@/hooks/localCourse/assignmentHooks";
|
||||||
import { getModuleItemUrl } from "@/services/urlUtils";
|
import { getModuleItemUrl } from "@/services/urlUtils";
|
||||||
import { useRouter } from "next/navigation";
|
import { useRouter } from "next/navigation";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
"use client";
|
"use client";
|
||||||
import { AssignmentSubmissionType } from "@/features/local/assignments/models/assignmentSubmissionType";
|
import { AssignmentSubmissionType } from "@/models/local/assignment/assignmentSubmissionType";
|
||||||
import { LocalCourseSettings } from "@/features/local/course/localCourseSettings";
|
import { LocalCourseSettings } from "@/models/local/localCourseSettings";
|
||||||
|
|
||||||
export function getAssignmentHelpString(settings: LocalCourseSettings) {
|
export function getAssignmentHelpString(settings: LocalCourseSettings) {
|
||||||
const groupNames = settings.assignmentGroups.map((g) => g.name).join("\n- ");
|
const groupNames = settings.assignmentGroups.map((g) => g.name).join("\n- ");
|
||||||
@@ -33,7 +33,6 @@ You can use markdown to format your assignment description. For example, you can
|
|||||||
|
|
||||||
[Link to Canvas](https://canvas.instructure.com)
|
[Link to Canvas](https://canvas.instructure.com)
|
||||||
|
|
||||||
|
|
||||||
\`Inline code\`
|
\`Inline code\`
|
||||||
|
|
||||||
> Blockquote
|
> Blockquote
|
||||||
@@ -55,32 +54,6 @@ flowchart TD
|
|||||||
C -->|Three| F[fa:fa-car Car]
|
C -->|Three| F[fa:fa-car Car]
|
||||||
\`\`\`
|
\`\`\`
|
||||||
|
|
||||||
## LaTeX Math
|
|
||||||
|
|
||||||
**Inline math:** The Fibonacci sequence is defined as: \$F(n) = F(n-1) + F(n-2)\$ where \$F(0) = 0\$ and \$F(1) = 1\$.
|
|
||||||
|
|
||||||
**Block math:**
|
|
||||||
\$\$F(n) = F(n-1) + F(n-2)\$\$
|
|
||||||
|
|
||||||
**Complex equations:**
|
|
||||||
\$\$
|
|
||||||
F(n) = \\begin{cases}
|
|
||||||
0 & \\text{if } n = 0 \\\\
|
|
||||||
1 & \\text{if } n = 1 \\\\
|
|
||||||
F(n-1) + F(n-2) & \\text{if } n > 1
|
|
||||||
\\end{cases}
|
|
||||||
\$\$
|
|
||||||
|
|
||||||
## github classroom links will be replaced by the GithubClassroomAssignmentShareLink setting
|
|
||||||
|
|
||||||
[Github Classroom](insert_github_classroom_url)
|
|
||||||
|
|
||||||
## Files
|
|
||||||
|
|
||||||
If you have mounted a folder in the /app/public/images directory, you can link to files like this:
|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
## Rubric
|
## Rubric
|
||||||
|
|
||||||
- 1pt: singular point
|
- 1pt: singular point
|
||||||
|
|||||||
@@ -14,9 +14,8 @@ export async function generateMetadata({
|
|||||||
}): Promise<Metadata> {
|
}): Promise<Metadata> {
|
||||||
const { courseName, assignmentName } = await params;
|
const { courseName, assignmentName } = await params;
|
||||||
const decodedAssignmentName = decodeURIComponent(assignmentName);
|
const decodedAssignmentName = decodeURIComponent(assignmentName);
|
||||||
const decodedCourseName = decodeURIComponent(courseName);
|
|
||||||
return {
|
return {
|
||||||
title: getTitle(`${decodedAssignmentName}, ${decodedCourseName}`),
|
title: getTitle(`${decodedAssignmentName}, ${courseName}`),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +1,13 @@
|
|||||||
"use client";
|
"use client";
|
||||||
import { MonacoEditor } from "@/components/editor/MonacoEditor";
|
import { MonacoEditor } from "@/components/editor/MonacoEditor";
|
||||||
|
import {
|
||||||
|
usePageQuery,
|
||||||
|
useUpdatePageMutation,
|
||||||
|
} from "@/hooks/localCourse/pageHooks";
|
||||||
|
import { localPageMarkdownUtils } from "@/models/local/page/localCoursePage";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import PagePreview from "./PagePreview";
|
import PagePreview from "./PagePreview";
|
||||||
import { useLocalCourseSettingsQuery } from "@/features/local/course/localCoursesHooks";
|
import { useLocalCourseSettingsQuery } from "@/hooks/localCourse/localCoursesHooks";
|
||||||
import EditPageButtons from "./EditPageButtons";
|
import EditPageButtons from "./EditPageButtons";
|
||||||
import ClientOnly from "@/components/ClientOnly";
|
import ClientOnly from "@/components/ClientOnly";
|
||||||
import { useRouter } from "next/navigation";
|
import { useRouter } from "next/navigation";
|
||||||
@@ -10,11 +15,6 @@ import { useCourseContext } from "@/app/course/[courseName]/context/courseContex
|
|||||||
import { useAuthoritativeUpdates } from "@/app/course/[courseName]/utils/useAuthoritativeUpdates";
|
import { useAuthoritativeUpdates } from "@/app/course/[courseName]/utils/useAuthoritativeUpdates";
|
||||||
import EditPageHeader from "./EditPageHeader";
|
import EditPageHeader from "./EditPageHeader";
|
||||||
import { EditLayout } from "@/components/EditLayout";
|
import { EditLayout } from "@/components/EditLayout";
|
||||||
import { localPageMarkdownUtils } from "@/features/local/pages/localCoursePageModels";
|
|
||||||
import {
|
|
||||||
usePageQuery,
|
|
||||||
useUpdatePageMutation,
|
|
||||||
} from "@/features/local/pages/pageHooks";
|
|
||||||
|
|
||||||
export default function EditPage({
|
export default function EditPage({
|
||||||
moduleName,
|
moduleName,
|
||||||
@@ -125,5 +125,5 @@ export default function EditPage({
|
|||||||
</>
|
</>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
);
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,15 +4,15 @@ import { Spinner } from "@/components/Spinner";
|
|||||||
import {
|
import {
|
||||||
useCanvasPagesQuery,
|
useCanvasPagesQuery,
|
||||||
useCreateCanvasPageMutation,
|
useCreateCanvasPageMutation,
|
||||||
useUpdateCanvasPageMutation,
|
|
||||||
useDeleteCanvasPageMutation,
|
useDeleteCanvasPageMutation,
|
||||||
} from "@/features/canvas/hooks/canvasPageHooks";
|
useUpdateCanvasPageMutation,
|
||||||
import { baseCanvasUrl } from "@/features/canvas/services/canvasServiceUtils";
|
} from "@/hooks/canvas/canvasPageHooks";
|
||||||
import { useLocalCourseSettingsQuery } from "@/features/local/course/localCoursesHooks";
|
import { useLocalCourseSettingsQuery } from "@/hooks/localCourse/localCoursesHooks";
|
||||||
import {
|
import {
|
||||||
useDeletePageMutation,
|
useDeletePageMutation,
|
||||||
usePageQuery,
|
usePageQuery,
|
||||||
} from "@/features/local/pages/pageHooks";
|
} from "@/hooks/localCourse/pageHooks";
|
||||||
|
import { baseCanvasUrl } from "@/services/canvas/canvasServiceUtils";
|
||||||
import { getCourseUrl } from "@/services/urlUtils";
|
import { getCourseUrl } from "@/services/urlUtils";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { useRouter } from "next/navigation";
|
import { useRouter } from "next/navigation";
|
||||||
|
|||||||
@@ -17,7 +17,6 @@ export default function EditPageHeader({
|
|||||||
className="btn"
|
className="btn"
|
||||||
href={getCourseUrl(courseName)}
|
href={getCourseUrl(courseName)}
|
||||||
shallow={true}
|
shallow={true}
|
||||||
prefetch={true}
|
|
||||||
>
|
>
|
||||||
{courseName}
|
{courseName}
|
||||||
</Link>
|
</Link>
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
import MarkdownDisplay from "@/components/MarkdownDisplay";
|
import MarkdownDisplay from "@/components/MarkdownDisplay";
|
||||||
import { LocalCoursePage } from "@/features/local/pages/localCoursePageModels";
|
import { LocalCoursePage } from "@/models/local/page/localCoursePage";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
|
|
||||||
export default function PagePreview({ page }: { page: LocalCoursePage }) {
|
export default function PagePreview({ page }: { page: LocalCoursePage }) {
|
||||||
return <MarkdownDisplay markdown={page.text} />;
|
return (
|
||||||
|
<MarkdownDisplay markdown={page.text} />
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import { Spinner } from "@/components/Spinner";
|
|||||||
import {
|
import {
|
||||||
usePageQuery,
|
usePageQuery,
|
||||||
useUpdatePageMutation,
|
useUpdatePageMutation,
|
||||||
} from "@/features/local/pages/pageHooks";
|
} from "@/hooks/localCourse/pageHooks";
|
||||||
import { getModuleItemUrl } from "@/services/urlUtils";
|
import { getModuleItemUrl } from "@/services/urlUtils";
|
||||||
import { useRouter } from "next/navigation";
|
import { useRouter } from "next/navigation";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
|
|||||||
@@ -14,9 +14,8 @@ export async function generateMetadata({
|
|||||||
}): Promise<Metadata> {
|
}): Promise<Metadata> {
|
||||||
const { courseName, pageName } = await params;
|
const { courseName, pageName } = await params;
|
||||||
const decodedPageName = decodeURIComponent(pageName);
|
const decodedPageName = decodeURIComponent(pageName);
|
||||||
const decodedCourseName = decodeURIComponent(courseName);
|
|
||||||
return {
|
return {
|
||||||
title: getTitle(`${decodedPageName}, ${decodedCourseName}`),
|
title: getTitle(`${decodedPageName}, ${courseName}`),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
"use client";
|
"use client";
|
||||||
import { MonacoEditor } from "@/components/editor/MonacoEditor";
|
import { MonacoEditor } from "@/components/editor/MonacoEditor";
|
||||||
|
import { quizMarkdownUtils } from "@/models/local/quiz/utils/quizMarkdownUtils";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import QuizPreview from "./QuizPreview";
|
import QuizPreview from "./QuizPreview";
|
||||||
import { QuizButtons } from "./QuizButton";
|
import { QuizButtons } from "./QuizButton";
|
||||||
@@ -10,14 +11,13 @@ import { useCourseContext } from "@/app/course/[courseName]/context/courseContex
|
|||||||
import {
|
import {
|
||||||
useQuizQuery,
|
useQuizQuery,
|
||||||
useUpdateQuizMutation,
|
useUpdateQuizMutation,
|
||||||
} from "@/features/local/quizzes/quizHooks";
|
} from "@/hooks/localCourse/quizHooks";
|
||||||
import { useAuthoritativeUpdates } from "../../../../utils/useAuthoritativeUpdates";
|
import { useAuthoritativeUpdates } from "../../../../utils/useAuthoritativeUpdates";
|
||||||
import { extractLabelValue } from "@/features/local/assignments/models/utils/markdownUtils";
|
import { extractLabelValue } from "@/models/local/assignment/utils/markdownUtils";
|
||||||
import EditQuizHeader from "./EditQuizHeader";
|
import EditQuizHeader from "./EditQuizHeader";
|
||||||
import { useLocalCourseSettingsQuery } from "@/features/local/course/localCoursesHooks";
|
import { LocalCourseSettings } from "@/models/local/localCourseSettings";
|
||||||
|
import { useLocalCourseSettingsQuery } from "@/hooks/localCourse/localCoursesHooks";
|
||||||
import { EditLayout } from "@/components/EditLayout";
|
import { EditLayout } from "@/components/EditLayout";
|
||||||
import { quizMarkdownUtils } from "@/features/local/quizzes/models/utils/quizMarkdownUtils";
|
|
||||||
import { LocalCourseSettings } from "@/features/local/course/localCourseSettings";
|
|
||||||
|
|
||||||
const helpString = (settings: LocalCourseSettings) => {
|
const helpString = (settings: LocalCourseSettings) => {
|
||||||
const groupNames = settings.assignmentGroups.map((g) => g.name).join("\n- ");
|
const groupNames = settings.assignmentGroups.map((g) => g.name).join("\n- ");
|
||||||
@@ -61,11 +61,6 @@ points: 4
|
|||||||
the underscore is optional
|
the underscore is optional
|
||||||
short answer
|
short answer
|
||||||
---
|
---
|
||||||
short answer with auto-graded responses
|
|
||||||
*a) answer 1
|
|
||||||
*b) other valid answer
|
|
||||||
short_answer=
|
|
||||||
---
|
|
||||||
this is a matching question
|
this is a matching question
|
||||||
^ left answer - right dropdown
|
^ left answer - right dropdown
|
||||||
^ other thing - another option
|
^ other thing - another option
|
||||||
|
|||||||
@@ -17,7 +17,6 @@ export default function EditQuizHeader({
|
|||||||
className="btn"
|
className="btn"
|
||||||
href={getCourseUrl(courseName)}
|
href={getCourseUrl(courseName)}
|
||||||
shallow={true}
|
shallow={true}
|
||||||
prefetch={true}
|
|
||||||
>
|
>
|
||||||
{courseName}
|
{courseName}
|
||||||
</Link>
|
</Link>
|
||||||
|
|||||||
@@ -5,13 +5,13 @@ import {
|
|||||||
useCanvasQuizzesQuery,
|
useCanvasQuizzesQuery,
|
||||||
useAddQuizToCanvasMutation,
|
useAddQuizToCanvasMutation,
|
||||||
useDeleteQuizFromCanvasMutation,
|
useDeleteQuizFromCanvasMutation,
|
||||||
} from "@/features/canvas/hooks/canvasQuizHooks";
|
} from "@/hooks/canvas/canvasQuizHooks";
|
||||||
import { baseCanvasUrl } from "@/features/canvas/services/canvasServiceUtils";
|
import { useLocalCourseSettingsQuery } from "@/hooks/localCourse/localCoursesHooks";
|
||||||
import { useLocalCourseSettingsQuery } from "@/features/local/course/localCoursesHooks";
|
|
||||||
import {
|
import {
|
||||||
useDeleteQuizMutation,
|
useDeleteQuizMutation,
|
||||||
useQuizQuery,
|
useQuizQuery,
|
||||||
} from "@/features/local/quizzes/quizHooks";
|
} from "@/hooks/localCourse/quizHooks";
|
||||||
|
import { baseCanvasUrl } from "@/services/canvas/canvasServiceUtils";
|
||||||
import { getCourseUrl } from "@/services/urlUtils";
|
import { getCourseUrl } from "@/services/urlUtils";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { useRouter } from "next/navigation";
|
import { useRouter } from "next/navigation";
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
import CheckIcon from "@/components/icons/CheckIcon";
|
import CheckIcon from "@/components/icons/CheckIcon";
|
||||||
import MarkdownDisplay from "@/components/MarkdownDisplay";
|
import MarkdownDisplay from "@/components/MarkdownDisplay";
|
||||||
|
import { useQuizQuery } from "@/hooks/localCourse/quizHooks";
|
||||||
import {
|
import {
|
||||||
LocalQuizQuestion,
|
LocalQuizQuestion,
|
||||||
QuestionType,
|
QuestionType,
|
||||||
} from "@/features/local/quizzes/models/localQuizQuestion";
|
} from "@/models/local/quiz/localQuizQuestion";
|
||||||
import { useQuizQuery } from "@/features/local/quizzes/quizHooks";
|
|
||||||
import { escapeMatchingText } from "@/services/utils/questionHtmlUtils";
|
import { escapeMatchingText } from "@/services/utils/questionHtmlUtils";
|
||||||
|
|
||||||
export default function QuizPreview({
|
export default function QuizPreview({
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import { Spinner } from "@/components/Spinner";
|
|||||||
import {
|
import {
|
||||||
useQuizQuery,
|
useQuizQuery,
|
||||||
useUpdateQuizMutation,
|
useUpdateQuizMutation,
|
||||||
} from "@/features/local/quizzes/quizHooks";
|
} from "@/hooks/localCourse/quizHooks";
|
||||||
import { getModuleItemUrl } from "@/services/urlUtils";
|
import { getModuleItemUrl } from "@/services/urlUtils";
|
||||||
import { useRouter } from "next/navigation";
|
import { useRouter } from "next/navigation";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
|
|||||||
@@ -14,9 +14,8 @@ export async function generateMetadata({
|
|||||||
}): Promise<Metadata> {
|
}): Promise<Metadata> {
|
||||||
const { courseName, quizName } = await params;
|
const { courseName, quizName } = await params;
|
||||||
const decodedQuizName = decodeURIComponent(quizName);
|
const decodedQuizName = decodeURIComponent(quizName);
|
||||||
const decodedCourseName = decodeURIComponent(courseName);
|
|
||||||
return {
|
return {
|
||||||
title: getTitle(`${decodedQuizName}, ${decodedCourseName}`),
|
title: getTitle(`${decodedQuizName}, ${courseName}`),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -4,13 +4,14 @@ import { CourseNavigation } from "./CourseNavigation";
|
|||||||
import { DragStyleContextProvider } from "./context/drag/dragStyleContext";
|
import { DragStyleContextProvider } from "./context/drag/dragStyleContext";
|
||||||
import CollapsableSidebar from "./CollapsableSidebar";
|
import CollapsableSidebar from "./CollapsableSidebar";
|
||||||
|
|
||||||
|
|
||||||
export default async function CoursePage() {
|
export default async function CoursePage() {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className="h-full flex flex-col">
|
<div className="h-full flex flex-col">
|
||||||
<DragStyleContextProvider>
|
<DragStyleContextProvider>
|
||||||
<DraggingContextProvider>
|
<DraggingContextProvider>
|
||||||
<div className="flex sm:flex-row h-full flex-col max-w-[2400px] w-full mx-auto">
|
<div className="flex sm:flex-row h-full flex-col max-w-[2400px] mx-auto">
|
||||||
<div className="flex-1 h-full flex flex-col">
|
<div className="flex-1 h-full flex flex-col">
|
||||||
<CourseNavigation />
|
<CourseNavigation />
|
||||||
<CourseCalendar />
|
<CourseCalendar />
|
||||||
|
|||||||
@@ -3,22 +3,20 @@
|
|||||||
import {
|
import {
|
||||||
useLocalCourseSettingsQuery,
|
useLocalCourseSettingsQuery,
|
||||||
useUpdateLocalCourseSettingsMutation,
|
useUpdateLocalCourseSettingsMutation,
|
||||||
} from "@/features/local/course/localCoursesHooks";
|
} from "@/hooks/localCourse/localCoursesHooks";
|
||||||
import { LocalAssignmentGroup } from "@/features/local/assignments/models/localAssignmentGroup";
|
import { LocalAssignmentGroup } from "@/models/local/assignment/localAssignmentGroup";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import TextInput from "../../../../components/form/TextInput";
|
import TextInput from "../../../../components/form/TextInput";
|
||||||
|
import { useSetAssignmentGroupsMutation } from "@/hooks/canvas/canvasCourseHooks";
|
||||||
import { settingsBox } from "./sharedSettings";
|
import { settingsBox } from "./sharedSettings";
|
||||||
import { Spinner } from "@/components/Spinner";
|
import { Spinner } from "@/components/Spinner";
|
||||||
|
import { baseCanvasUrl } from "@/services/canvas/canvasServiceUtils";
|
||||||
import MeatballIcon from "./MeatballIcon";
|
import MeatballIcon from "./MeatballIcon";
|
||||||
import { useSetAssignmentGroupsMutation } from "@/features/canvas/hooks/canvasCourseHooks";
|
|
||||||
import { baseCanvasUrl } from "@/features/canvas/services/canvasServiceUtils";
|
|
||||||
import Modal, { useModal } from "@/components/Modal";
|
|
||||||
|
|
||||||
export default function AssignmentGroupManagement() {
|
export default function AssignmentGroupManagement() {
|
||||||
const { data: settings, isPending } = useLocalCourseSettingsQuery();
|
const { data: settings, isPending } = useLocalCourseSettingsQuery();
|
||||||
const updateSettings = useUpdateLocalCourseSettingsMutation();
|
const updateSettings = useUpdateLocalCourseSettingsMutation();
|
||||||
const applyInCanvas = useSetAssignmentGroupsMutation(settings.canvasId);
|
const applyInCanvas = useSetAssignmentGroupsMutation(settings.canvasId);
|
||||||
const modal = useModal();
|
|
||||||
|
|
||||||
const [assignmentGroups, setAssignmentGroups] = useState<
|
const [assignmentGroups, setAssignmentGroups] = useState<
|
||||||
LocalAssignmentGroup[]
|
LocalAssignmentGroup[]
|
||||||
@@ -106,46 +104,17 @@ export default function AssignmentGroupManagement() {
|
|||||||
</div>
|
</div>
|
||||||
<br />
|
<br />
|
||||||
<div className="flex justify-end">
|
<div className="flex justify-end">
|
||||||
<Modal
|
|
||||||
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
|
<button
|
||||||
onClick={async () => {
|
onClick={async () => {
|
||||||
const newSettings = await applyInCanvas.mutateAsync(
|
const newSettings = await applyInCanvas.mutateAsync(settings);
|
||||||
settings
|
|
||||||
);
|
|
||||||
|
|
||||||
// prevent debounce from resetting
|
// prevent debounce from resetting
|
||||||
if (newSettings)
|
if (newSettings) setAssignmentGroups(newSettings.assignmentGroups);
|
||||||
setAssignmentGroups(newSettings.assignmentGroups);
|
|
||||||
}}
|
}}
|
||||||
disabled={applyInCanvas.isPending}
|
disabled={applyInCanvas.isPending}
|
||||||
className="btn-danger"
|
|
||||||
>
|
>
|
||||||
Yes
|
Update Assignment Groups In Canvas
|
||||||
</button>
|
</button>
|
||||||
<button onClick={closeModal} disabled={applyInCanvas.isPending}>
|
|
||||||
No
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
{applyInCanvas.isPending && <Spinner />}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</Modal>
|
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
{applyInCanvas.isPending && <Spinner />}
|
{applyInCanvas.isPending && <Spinner />}
|
||||||
{applyInCanvas.isSuccess && (
|
{applyInCanvas.isSuccess && (
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import { Spinner } from "@/components/Spinner";
|
|||||||
import {
|
import {
|
||||||
useLocalCourseSettingsQuery,
|
useLocalCourseSettingsQuery,
|
||||||
useUpdateLocalCourseSettingsMutation,
|
useUpdateLocalCourseSettingsMutation,
|
||||||
} from "@/features/local/course/localCoursesHooks";
|
} from "@/hooks/localCourse/localCoursesHooks";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
|
|
||||||
export default function DaysOfWeekSettings() {
|
export default function DaysOfWeekSettings() {
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
import {
|
import {
|
||||||
useLocalCourseSettingsQuery,
|
useLocalCourseSettingsQuery,
|
||||||
useUpdateLocalCourseSettingsMutation,
|
useUpdateLocalCourseSettingsMutation,
|
||||||
} from "@/features/local/course/localCoursesHooks";
|
} from "@/hooks/localCourse/localCoursesHooks";
|
||||||
import { TimePicker } from "../../../../components/TimePicker";
|
import { TimePicker } from "../../../../components/TimePicker";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import DefaultLockOffset from "./DefaultLockOffset";
|
import DefaultLockOffset from "./DefaultLockOffset";
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import TextInput from "@/components/form/TextInput";
|
|||||||
import {
|
import {
|
||||||
useLocalCourseSettingsQuery,
|
useLocalCourseSettingsQuery,
|
||||||
useUpdateLocalCourseSettingsMutation,
|
useUpdateLocalCourseSettingsMutation,
|
||||||
} from "@/features/local/course/localCoursesHooks";
|
} from "@/hooks/localCourse/localCoursesHooks";
|
||||||
import { useState, useEffect } from "react";
|
import { useState, useEffect } from "react";
|
||||||
import { settingsBox } from "./sharedSettings";
|
import { settingsBox } from "./sharedSettings";
|
||||||
|
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import TextInput from "@/components/form/TextInput";
|
|||||||
import {
|
import {
|
||||||
useLocalCourseSettingsQuery,
|
useLocalCourseSettingsQuery,
|
||||||
useUpdateLocalCourseSettingsMutation,
|
useUpdateLocalCourseSettingsMutation,
|
||||||
} from "@/features/local/course/localCoursesHooks";
|
} from "@/hooks/localCourse/localCoursesHooks";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
|
|
||||||
export default function DefaultLockOffset() {
|
export default function DefaultLockOffset() {
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
"use client";
|
"use client";
|
||||||
import { useLocalCourseSettingsQuery } from "@/features/local/course/localCoursesHooks";
|
import { useLocalCourseSettingsQuery } from "@/hooks/localCourse/localCoursesHooks";
|
||||||
import { settingsBox } from "./sharedSettings";
|
import { settingsBox } from "./sharedSettings";
|
||||||
|
import { useCourseStudentsQuery } from "@/hooks/canvas/canvasCourseHooks";
|
||||||
import { Spinner } from "@/components/Spinner";
|
import { Spinner } from "@/components/Spinner";
|
||||||
import { useCourseStudentsQuery } from "@/features/canvas/hooks/canvasCourseHooks";
|
|
||||||
|
|
||||||
export default function GithubClassroomList() {
|
export default function GithubClassroomList() {
|
||||||
const { data: settings } = useLocalCourseSettingsQuery();
|
const { data: settings } = useLocalCourseSettingsQuery();
|
||||||
|
|||||||
@@ -5,13 +5,13 @@ import { SuspenseAndErrorHandling } from "@/components/SuspenseAndErrorHandling"
|
|||||||
import {
|
import {
|
||||||
useLocalCourseSettingsQuery,
|
useLocalCourseSettingsQuery,
|
||||||
useUpdateLocalCourseSettingsMutation,
|
useUpdateLocalCourseSettingsMutation,
|
||||||
} from "@/features/local/course/localCoursesHooks";
|
} from "@/hooks/localCourse/localCoursesHooks";
|
||||||
import { getDateFromString } from "@/features/local/utils/timeUtils";
|
import { getDateFromString } from "@/models/local/utils/timeUtils";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import {
|
import {
|
||||||
holidaysToString,
|
holidaysToString,
|
||||||
parseHolidays,
|
parseHolidays,
|
||||||
} from "../../../../features/local/utils/settingsUtils";
|
} from "../../../../models/local/utils/settingsUtils";
|
||||||
import { settingsBox } from "./sharedSettings";
|
import { settingsBox } from "./sharedSettings";
|
||||||
|
|
||||||
const exampleString = `springBreak:
|
const exampleString = `springBreak:
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
"use client";
|
"use client";
|
||||||
import { useLocalCourseSettingsQuery } from "@/features/local/course/localCoursesHooks";
|
import { useLocalCourseSettingsQuery } from "@/hooks/localCourse/localCoursesHooks";
|
||||||
import { getCourseUrl } from "@/services/urlUtils";
|
import { getCourseUrl } from "@/services/urlUtils";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
"use client";
|
"use client";
|
||||||
import { useLocalCourseSettingsQuery } from "@/features/local/course/localCoursesHooks";
|
import { useLocalCourseSettingsQuery } from "@/hooks/localCourse/localCoursesHooks";
|
||||||
import { getDateOnlyMarkdownString } from "@/features/local/utils/timeUtils";
|
import { getDateOnlyMarkdownString } from "@/models/local/utils/timeUtils";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import { settingsBox } from "./sharedSettings";
|
import { settingsBox } from "./sharedSettings";
|
||||||
|
|
||||||
|
|||||||
@@ -3,11 +3,11 @@ import SelectInput from "@/components/form/SelectInput";
|
|||||||
import {
|
import {
|
||||||
useLocalCourseSettingsQuery,
|
useLocalCourseSettingsQuery,
|
||||||
useUpdateLocalCourseSettingsMutation,
|
useUpdateLocalCourseSettingsMutation,
|
||||||
} from "@/features/local/course/localCoursesHooks";
|
} from "@/hooks/localCourse/localCoursesHooks";
|
||||||
import {
|
import {
|
||||||
AssignmentSubmissionType,
|
AssignmentSubmissionType,
|
||||||
AssignmentSubmissionTypeList,
|
AssignmentSubmissionTypeList,
|
||||||
} from "@/features/local/assignments/models/assignmentSubmissionType";
|
} from "@/models/local/assignment/assignmentSubmissionType";
|
||||||
import React, { useEffect, useState } from "react";
|
import React, { useEffect, useState } from "react";
|
||||||
import { settingsBox } from "./sharedSettings";
|
import { settingsBox } from "./sharedSettings";
|
||||||
|
|
||||||
|
|||||||
@@ -1,10 +1,8 @@
|
|||||||
import React, { useState } from "react";
|
import React, { useState } from "react";
|
||||||
|
import { useCanvasTabsQuery } from "@/hooks/canvas/canvasNavigationHooks";
|
||||||
|
import { useUpdateCanvasTabMutation } from "@/hooks/canvas/canvasNavigationHooks";
|
||||||
import { Spinner } from "@/components/Spinner";
|
import { Spinner } from "@/components/Spinner";
|
||||||
import { NavTabListItem } from "./NavTabListItem";
|
import { NavTabListItem } from "./NavTabListItem";
|
||||||
import {
|
|
||||||
useCanvasTabsQuery,
|
|
||||||
useUpdateCanvasTabMutation,
|
|
||||||
} from "@/features/canvas/hooks/canvasNavigationHooks";
|
|
||||||
|
|
||||||
export const CanvasNavigationManagement = () => {
|
export const CanvasNavigationManagement = () => {
|
||||||
const { data: tabs, isLoading, isError } = useCanvasTabsQuery();
|
const { data: tabs, isLoading, isError } = useCanvasTabsQuery();
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { Spinner } from "@/components/Spinner";
|
import { Spinner } from "@/components/Spinner";
|
||||||
import { useUpdateCanvasTabMutation } from "@/features/canvas/hooks/canvasNavigationHooks";
|
import { useUpdateCanvasTabMutation } from "@/hooks/canvas/canvasNavigationHooks";
|
||||||
import { CanvasCourseTab } from "@/features/canvas/services/canvasNavigationService";
|
import { CanvasCourseTab } from "@/services/canvas/canvasNavigationService";
|
||||||
import React, { FC } from "react";
|
import React, { FC } from "react";
|
||||||
|
|
||||||
export const NavTabListItem: FC<{
|
export const NavTabListItem: FC<{
|
||||||
|
|||||||
@@ -8,9 +8,8 @@ export async function generateMetadata({
|
|||||||
params: Promise<{ courseName: string }>;
|
params: Promise<{ courseName: string }>;
|
||||||
}): Promise<Metadata> {
|
}): Promise<Metadata> {
|
||||||
const { courseName } = await params;
|
const { courseName } = await params;
|
||||||
const decodedCourseName = decodeURIComponent(courseName);
|
|
||||||
return {
|
return {
|
||||||
title: getTitle(decodedCourseName) + " Settings",
|
title: getTitle(courseName) + " Settings",
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -76,6 +76,83 @@ code {
|
|||||||
@apply text-wrap;
|
@apply text-wrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Syntax highlighting styles for code blocks */
|
||||||
|
pre code {
|
||||||
|
@apply block bg-transparent p-0 m-0 rounded-none;
|
||||||
|
}
|
||||||
|
|
||||||
|
pre {
|
||||||
|
@apply bg-gray-900 p-4 rounded-lg overflow-x-auto font-mono text-sm leading-relaxed;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Prism.js syntax highlighting styles */
|
||||||
|
.token.comment,
|
||||||
|
.token.prolog,
|
||||||
|
.token.doctype,
|
||||||
|
.token.cdata {
|
||||||
|
@apply text-gray-500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.token.punctuation {
|
||||||
|
@apply text-gray-400;
|
||||||
|
}
|
||||||
|
|
||||||
|
.token.property,
|
||||||
|
.token.tag,
|
||||||
|
.token.constant,
|
||||||
|
.token.symbol,
|
||||||
|
.token.deleted {
|
||||||
|
@apply text-red-400;
|
||||||
|
}
|
||||||
|
|
||||||
|
.token.boolean,
|
||||||
|
.token.number {
|
||||||
|
@apply text-orange-400;
|
||||||
|
}
|
||||||
|
|
||||||
|
.token.selector,
|
||||||
|
.token.attr-name,
|
||||||
|
.token.string,
|
||||||
|
.token.char,
|
||||||
|
.token.builtin,
|
||||||
|
.token.inserted {
|
||||||
|
@apply text-green-400;
|
||||||
|
}
|
||||||
|
|
||||||
|
.token.operator,
|
||||||
|
.token.entity,
|
||||||
|
.token.url,
|
||||||
|
.language-css .token.string,
|
||||||
|
.style .token.string,
|
||||||
|
.token.variable {
|
||||||
|
@apply text-yellow-400;
|
||||||
|
}
|
||||||
|
|
||||||
|
.token.atrule,
|
||||||
|
.token.attr-value,
|
||||||
|
.token.function,
|
||||||
|
.token.class-name {
|
||||||
|
@apply text-blue-400;
|
||||||
|
}
|
||||||
|
|
||||||
|
.token.keyword {
|
||||||
|
@apply text-purple-400;
|
||||||
|
}
|
||||||
|
|
||||||
|
.token.regex,
|
||||||
|
.token.important {
|
||||||
|
@apply text-orange-400;
|
||||||
|
}
|
||||||
|
|
||||||
|
.token.important,
|
||||||
|
.token.bold {
|
||||||
|
@apply font-bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.token.italic {
|
||||||
|
@apply italic;
|
||||||
|
}
|
||||||
|
|
||||||
p {
|
p {
|
||||||
@apply mb-3;
|
@apply mb-3;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,10 +2,15 @@ import type { Metadata } from "next";
|
|||||||
import "./globals.css";
|
import "./globals.css";
|
||||||
import Providers from "./providers";
|
import Providers from "./providers";
|
||||||
import { Suspense } from "react";
|
import { Suspense } from "react";
|
||||||
|
import { dehydrate, HydrationBoundary } from "@tanstack/react-query";
|
||||||
import { MyToaster } from "./MyToaster";
|
import { MyToaster } from "./MyToaster";
|
||||||
|
import { createServerSideHelpers } from "@trpc/react-query/server";
|
||||||
|
import { trpcAppRouter } from "@/services/serverFunctions/router/app";
|
||||||
|
import { createTrpcContext } from "@/services/serverFunctions/context";
|
||||||
|
import superjson from "superjson";
|
||||||
|
import { fileStorageService } from "@/services/fileStorage/fileStorageService";
|
||||||
import { ClientCacheInvalidation } from "../components/realtime/ClientCacheInvalidation";
|
import { ClientCacheInvalidation } from "../components/realtime/ClientCacheInvalidation";
|
||||||
import { getTitle } from "@/services/titleUtils";
|
import { getTitle } from "@/services/titleUtils";
|
||||||
import DataHydration from "./DataHydration";
|
|
||||||
export const dynamic = "force-dynamic";
|
export const dynamic = "force-dynamic";
|
||||||
|
|
||||||
export const metadata: Metadata = {
|
export const metadata: Metadata = {
|
||||||
@@ -36,3 +41,77 @@ export default async function RootLayout({
|
|||||||
</html>
|
</html>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function DataHydration({
|
||||||
|
children,
|
||||||
|
}: Readonly<{
|
||||||
|
children: React.ReactNode;
|
||||||
|
}>) {
|
||||||
|
console.log("starting hydration");
|
||||||
|
const trpcHelper = createServerSideHelpers({
|
||||||
|
router: trpcAppRouter,
|
||||||
|
ctx: createTrpcContext(),
|
||||||
|
transformer: superjson,
|
||||||
|
queryClientConfig: {
|
||||||
|
defaultOptions: {
|
||||||
|
queries: {
|
||||||
|
staleTime: Infinity,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const allSettings = await fileStorageService.settings.getAllCoursesSettings();
|
||||||
|
|
||||||
|
await Promise.all(
|
||||||
|
allSettings.map(async (settings) => {
|
||||||
|
const courseName = settings.name;
|
||||||
|
const moduleNames = await trpcHelper.module.getModuleNames.fetch({
|
||||||
|
courseName,
|
||||||
|
});
|
||||||
|
await Promise.all([
|
||||||
|
// assignments
|
||||||
|
...moduleNames.map(
|
||||||
|
async (moduleName) =>
|
||||||
|
await trpcHelper.assignment.getAllAssignments.prefetch({
|
||||||
|
courseName,
|
||||||
|
moduleName,
|
||||||
|
})
|
||||||
|
),
|
||||||
|
// quizzes
|
||||||
|
...moduleNames.map(
|
||||||
|
async (moduleName) =>
|
||||||
|
await trpcHelper.quiz.getAllQuizzes.prefetch({
|
||||||
|
courseName,
|
||||||
|
moduleName,
|
||||||
|
})
|
||||||
|
),
|
||||||
|
// pages
|
||||||
|
...moduleNames.map(
|
||||||
|
async (moduleName) =>
|
||||||
|
await trpcHelper.page.getAllPages.prefetch({
|
||||||
|
courseName,
|
||||||
|
moduleName,
|
||||||
|
})
|
||||||
|
),
|
||||||
|
]);
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
// lectures
|
||||||
|
await Promise.all(
|
||||||
|
allSettings.map(
|
||||||
|
async (settings) =>
|
||||||
|
await trpcHelper.lectures.getLectures.fetch({
|
||||||
|
courseName: settings.name,
|
||||||
|
})
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
const dehydratedState = dehydrate(trpcHelper.queryClient);
|
||||||
|
console.log("ran hydration");
|
||||||
|
|
||||||
|
return (
|
||||||
|
<HydrationBoundary state={dehydratedState}>{children}</HydrationBoundary>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,16 +1,16 @@
|
|||||||
"use client";
|
"use client";
|
||||||
import React, { useState } from "react";
|
import React, { useState } from "react";
|
||||||
import { SuspenseAndErrorHandling } from "@/components/SuspenseAndErrorHandling";
|
import { SuspenseAndErrorHandling } from "@/components/SuspenseAndErrorHandling";
|
||||||
import AddNewCourseToGlobalSettingsForm from "./AddCourseToGlobalSettingsForm";
|
import NewCourseForm from "./NewCourseForm";
|
||||||
import ClientOnly from "@/components/ClientOnly";
|
import ClientOnly from "@/components/ClientOnly";
|
||||||
|
|
||||||
export default function AddCourseToGlobalSettings() {
|
export default function AddNewCourse() {
|
||||||
const [showForm, setShowForm] = useState(false);
|
const [showForm, setShowForm] = useState(false);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<div className="flex justify-center">
|
<div className="flex justify-center">
|
||||||
<button className="" onClick={() => setShowForm((i) => !i)}>
|
<button className="" onClick={() => setShowForm(true)}>
|
||||||
Add New Course
|
Add New Course
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
@@ -18,9 +18,7 @@ export default function AddCourseToGlobalSettings() {
|
|||||||
<div className={" collapsible " + (showForm && "expand")}>
|
<div className={" collapsible " + (showForm && "expand")}>
|
||||||
<div className="border rounded-md p-3 m-3">
|
<div className="border rounded-md p-3 m-3">
|
||||||
<SuspenseAndErrorHandling>
|
<SuspenseAndErrorHandling>
|
||||||
<ClientOnly>
|
<ClientOnly>{showForm && <NewCourseForm />}</ClientOnly>
|
||||||
{showForm && <AddNewCourseToGlobalSettingsForm />}
|
|
||||||
</ClientOnly>
|
|
||||||
</SuspenseAndErrorHandling>
|
</SuspenseAndErrorHandling>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -1,27 +1,25 @@
|
|||||||
"use client";
|
"use client";
|
||||||
import ButtonSelect from "@/components/ButtonSelect";
|
|
||||||
import { DayOfWeekInput } from "@/components/form/DayOfWeekInput";
|
import { DayOfWeekInput } from "@/components/form/DayOfWeekInput";
|
||||||
import SelectInput from "@/components/form/SelectInput";
|
import SelectInput from "@/components/form/SelectInput";
|
||||||
import { StoragePathSelector } from "@/components/form/StoragePathSelector";
|
|
||||||
import TextInput from "@/components/form/TextInput";
|
|
||||||
import { Spinner } from "@/components/Spinner";
|
import { Spinner } from "@/components/Spinner";
|
||||||
import { SuspenseAndErrorHandling } from "@/components/SuspenseAndErrorHandling";
|
import { SuspenseAndErrorHandling } from "@/components/SuspenseAndErrorHandling";
|
||||||
|
import { useCourseListInTermQuery } from "@/hooks/canvas/canvasCourseHooks";
|
||||||
|
import { useCanvasTermsQuery } from "@/hooks/canvas/canvasHooks";
|
||||||
import {
|
import {
|
||||||
useCreateLocalCourseMutation,
|
useCreateLocalCourseMutation,
|
||||||
useLocalCoursesSettingsQuery,
|
useLocalCoursesSettingsQuery,
|
||||||
} from "@/features/local/course/localCoursesHooks";
|
} from "@/hooks/localCourse/localCoursesHooks";
|
||||||
import { CanvasCourseModel } from "@/features/canvas/models/courses/canvasCourseModel";
|
import { useEmptyDirectoriesQuery } from "@/hooks/localCourse/storageDirectoryHooks";
|
||||||
import { CanvasEnrollmentTermModel } from "@/features/canvas/models/enrollmentTerms/canvasEnrollmentTermModel";
|
import { CanvasCourseModel } from "@/models/canvas/courses/canvasCourseModel";
|
||||||
import { AssignmentSubmissionType } from "@/features/local/assignments/models/assignmentSubmissionType";
|
import { CanvasEnrollmentTermModel } from "@/models/canvas/enrollmentTerms/canvasEnrollmentTermModel";
|
||||||
import { getCourseUrl } from "@/services/urlUtils";
|
import { AssignmentSubmissionType } from "@/models/local/assignment/assignmentSubmissionType";
|
||||||
import { useRouter } from "next/navigation";
|
|
||||||
import React, { Dispatch, SetStateAction, useMemo, useState } from "react";
|
|
||||||
import {
|
import {
|
||||||
DayOfWeek,
|
DayOfWeek,
|
||||||
LocalCourseSettings,
|
LocalCourseSettings,
|
||||||
} from "@/features/local/course/localCourseSettings";
|
} from "@/models/local/localCourseSettings";
|
||||||
import { useCourseListInTermQuery } from "@/features/canvas/hooks/canvasCourseHooks";
|
import { getCourseUrl } from "@/services/urlUtils";
|
||||||
import { useCanvasTermsQuery } from "@/features/canvas/hooks/canvasHooks";
|
import { useRouter } from "next/navigation";
|
||||||
|
import React, { useMemo, useState } from "react";
|
||||||
|
|
||||||
const sampleCompose = `services:
|
const sampleCompose = `services:
|
||||||
canvas_manager:
|
canvas_manager:
|
||||||
@@ -39,7 +37,7 @@ const sampleCompose = `services:
|
|||||||
- ~/projects/faculty/4850_AdvancedFE/2024-fall-alex/modules:/app/storage/advanced_frontend
|
- ~/projects/faculty/4850_AdvancedFE/2024-fall-alex/modules:/app/storage/advanced_frontend
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export default function AddNewCourseToGlobalSettingsForm() {
|
export default function NewCourseForm() {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const today = useMemo(() => new Date(), []);
|
const today = useMemo(() => new Date(), []);
|
||||||
const { data: canvasTerms } = useCanvasTermsQuery(today);
|
const { data: canvasTerms } = useCanvasTermsQuery(today);
|
||||||
@@ -56,7 +54,6 @@ export default function AddNewCourseToGlobalSettingsForm() {
|
|||||||
const [courseToImport, setCourseToImport] = useState<
|
const [courseToImport, setCourseToImport] = useState<
|
||||||
LocalCourseSettings | undefined
|
LocalCourseSettings | undefined
|
||||||
>();
|
>();
|
||||||
const [name, setName] = useState("");
|
|
||||||
const createCourse = useCreateLocalCourseMutation();
|
const createCourse = useCreateLocalCourseMutation();
|
||||||
|
|
||||||
const formIsComplete =
|
const formIsComplete =
|
||||||
@@ -64,13 +61,12 @@ export default function AddNewCourseToGlobalSettingsForm() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<ButtonSelect
|
<SelectInput
|
||||||
options={canvasTerms}
|
|
||||||
getOptionName={(t) => t?.name ?? ""}
|
|
||||||
setValue={setSelectedTerm}
|
|
||||||
value={selectedTerm}
|
value={selectedTerm}
|
||||||
|
setValue={setSelectedTerm}
|
||||||
label={"Canvas Term"}
|
label={"Canvas Term"}
|
||||||
center={true}
|
options={canvasTerms}
|
||||||
|
getOptionName={(t) => t.name}
|
||||||
/>
|
/>
|
||||||
<SuspenseAndErrorHandling>
|
<SuspenseAndErrorHandling>
|
||||||
{selectedTerm && (
|
{selectedTerm && (
|
||||||
@@ -84,8 +80,6 @@ export default function AddNewCourseToGlobalSettingsForm() {
|
|||||||
setSelectedDaysOfWeek={setSelectedDaysOfWeek}
|
setSelectedDaysOfWeek={setSelectedDaysOfWeek}
|
||||||
courseToImport={courseToImport}
|
courseToImport={courseToImport}
|
||||||
setCourseToImport={setCourseToImport}
|
setCourseToImport={setCourseToImport}
|
||||||
name={name}
|
|
||||||
setName={setName}
|
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</SuspenseAndErrorHandling>
|
</SuspenseAndErrorHandling>
|
||||||
@@ -94,7 +88,6 @@ export default function AddNewCourseToGlobalSettingsForm() {
|
|||||||
disabled={!formIsComplete || createCourse.isPending}
|
disabled={!formIsComplete || createCourse.isPending}
|
||||||
onClick={async () => {
|
onClick={async () => {
|
||||||
if (formIsComplete) {
|
if (formIsComplete) {
|
||||||
console.log("Creating course with settings:", selectedDirectory);
|
|
||||||
const newSettings: LocalCourseSettings = courseToImport
|
const newSettings: LocalCourseSettings = courseToImport
|
||||||
? {
|
? {
|
||||||
...courseToImport,
|
...courseToImport,
|
||||||
@@ -133,10 +126,8 @@ export default function AddNewCourseToGlobalSettingsForm() {
|
|||||||
await createCourse.mutateAsync({
|
await createCourse.mutateAsync({
|
||||||
settings: newSettings,
|
settings: newSettings,
|
||||||
settingsFromCourseToImport: courseToImport,
|
settingsFromCourseToImport: courseToImport,
|
||||||
name,
|
|
||||||
directory: selectedDirectory,
|
|
||||||
});
|
});
|
||||||
router.push(getCourseUrl(name));
|
router.push(getCourseUrl(selectedDirectory));
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
@@ -157,35 +148,32 @@ function OtherSettings({
|
|||||||
selectedTerm,
|
selectedTerm,
|
||||||
selectedCanvasCourse,
|
selectedCanvasCourse,
|
||||||
setSelectedCanvasCourse,
|
setSelectedCanvasCourse,
|
||||||
selectedDirectory: _,
|
selectedDirectory,
|
||||||
setSelectedDirectory,
|
setSelectedDirectory,
|
||||||
selectedDaysOfWeek,
|
selectedDaysOfWeek,
|
||||||
setSelectedDaysOfWeek,
|
setSelectedDaysOfWeek,
|
||||||
courseToImport,
|
courseToImport,
|
||||||
setCourseToImport,
|
setCourseToImport,
|
||||||
name,
|
|
||||||
setName,
|
|
||||||
}: {
|
}: {
|
||||||
selectedTerm: CanvasEnrollmentTermModel;
|
selectedTerm: CanvasEnrollmentTermModel;
|
||||||
selectedCanvasCourse: CanvasCourseModel | undefined;
|
selectedCanvasCourse: CanvasCourseModel | undefined;
|
||||||
setSelectedCanvasCourse: Dispatch<
|
setSelectedCanvasCourse: React.Dispatch<
|
||||||
SetStateAction<CanvasCourseModel | undefined>
|
React.SetStateAction<CanvasCourseModel | undefined>
|
||||||
>;
|
>;
|
||||||
selectedDirectory: string | undefined;
|
selectedDirectory: string | undefined;
|
||||||
setSelectedDirectory: Dispatch<SetStateAction<string | undefined>>;
|
setSelectedDirectory: React.Dispatch<
|
||||||
|
React.SetStateAction<string | undefined>
|
||||||
|
>;
|
||||||
selectedDaysOfWeek: DayOfWeek[];
|
selectedDaysOfWeek: DayOfWeek[];
|
||||||
setSelectedDaysOfWeek: Dispatch<SetStateAction<DayOfWeek[]>>;
|
setSelectedDaysOfWeek: React.Dispatch<React.SetStateAction<DayOfWeek[]>>;
|
||||||
courseToImport: LocalCourseSettings | undefined;
|
courseToImport: LocalCourseSettings | undefined;
|
||||||
setCourseToImport: Dispatch<SetStateAction<LocalCourseSettings | undefined>>;
|
setCourseToImport: React.Dispatch<
|
||||||
name: string;
|
React.SetStateAction<LocalCourseSettings | undefined>
|
||||||
setName: Dispatch<SetStateAction<string>>;
|
>;
|
||||||
}) {
|
}) {
|
||||||
const { data: canvasCourses } = useCourseListInTermQuery(selectedTerm.id);
|
const { data: canvasCourses } = useCourseListInTermQuery(selectedTerm.id);
|
||||||
const { data: allSettings } = useLocalCoursesSettingsQuery();
|
const { data: allSettings } = useLocalCoursesSettingsQuery();
|
||||||
const [directory, setDirectory] = useState("./");
|
const { data: emptyDirectories } = useEmptyDirectoriesQuery();
|
||||||
// const directoryIsCourseQuery = useDirectoryIsCourseQuery(
|
|
||||||
// selectedDirectory ?? "./"
|
|
||||||
// );
|
|
||||||
|
|
||||||
const populatedCanvasCourseIds = allSettings?.map((s) => s.canvasId) ?? [];
|
const populatedCanvasCourseIds = allSettings?.map((s) => s.canvasId) ?? [];
|
||||||
const availableCourses =
|
const availableCourses =
|
||||||
@@ -196,21 +184,25 @@ function OtherSettings({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<ButtonSelect
|
<SelectInput
|
||||||
value={selectedCanvasCourse}
|
value={selectedCanvasCourse}
|
||||||
setValue={setSelectedCanvasCourse}
|
setValue={setSelectedCanvasCourse}
|
||||||
label={"Course"}
|
label={"Course"}
|
||||||
options={availableCourses}
|
options={availableCourses}
|
||||||
getOptionName={(c) => c?.name ?? ""}
|
getOptionName={(c) => c.name}
|
||||||
center={true}
|
|
||||||
/>
|
/>
|
||||||
|
<SelectInput
|
||||||
<StoragePathSelector
|
value={selectedDirectory}
|
||||||
value={directory}
|
setValue={setSelectedDirectory}
|
||||||
setValue={setDirectory}
|
|
||||||
setLastTypedValue={setSelectedDirectory}
|
|
||||||
label={"Storage Folder"}
|
label={"Storage Folder"}
|
||||||
|
options={emptyDirectories ?? []}
|
||||||
|
getOptionName={(d) => d}
|
||||||
|
emptyOptionText="--- add a new folder to your docker compose to add more folders ---"
|
||||||
/>
|
/>
|
||||||
|
<div className="px-5">
|
||||||
|
New folders will not be created automatically, you are expected to mount
|
||||||
|
a docker volume for each courses.
|
||||||
|
</div>
|
||||||
<br />
|
<br />
|
||||||
<div className="flex justify-center">
|
<div className="flex justify-center">
|
||||||
<DayOfWeekInput
|
<DayOfWeekInput
|
||||||
@@ -233,7 +225,6 @@ function OtherSettings({
|
|||||||
options={allSettings}
|
options={allSettings}
|
||||||
getOptionName={(c) => c.name}
|
getOptionName={(c) => c.name}
|
||||||
/>
|
/>
|
||||||
<TextInput value={name} setValue={setName} label={"Display Name"} />
|
|
||||||
<div className="px-5">
|
<div className="px-5">
|
||||||
Assignments, Quizzes, Pages, and Lectures will have their due dates
|
Assignments, Quizzes, Pages, and Lectures will have their due dates
|
||||||
moved based on how far they are from the start of the semester.
|
moved based on how far they are from the start of the semester.
|
||||||
@@ -1,6 +1,5 @@
|
|||||||
import CourseList from "./CourseList";
|
import CourseList from "./CourseList";
|
||||||
import { AddExistingCourseToGlobalSettings } from "./addCourse/AddExistingCourseToGlobalSettings";
|
import AddNewCourse from "./newCourse/AddNewCourse";
|
||||||
import AddCourseToGlobalSettings from "./addCourse/AddNewCourse";
|
|
||||||
import TodaysLectures from "./todaysLectures/TodaysLectures";
|
import TodaysLectures from "./todaysLectures/TodaysLectures";
|
||||||
|
|
||||||
export default async function Home() {
|
export default async function Home() {
|
||||||
@@ -19,36 +18,7 @@ export default async function Home() {
|
|||||||
<TodaysLectures />
|
<TodaysLectures />
|
||||||
<br />
|
<br />
|
||||||
<br />
|
<br />
|
||||||
<AddCourseToGlobalSettings />
|
<AddNewCourse />
|
||||||
<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 />
|
|
||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -3,13 +3,13 @@
|
|||||||
import { getLecturePreviewUrl } from "@/services/urlUtils";
|
import { getLecturePreviewUrl } from "@/services/urlUtils";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { useCourseContext } from "../course/[courseName]/context/courseContext";
|
import { useCourseContext } from "../course/[courseName]/context/courseContext";
|
||||||
import { useLecturesSuspenseQuery as useLecturesQuery } from "@/features/local/lectures/lectureHooks";
|
import { useLecturesSuspenseQuery as useLecturesQuery } from "@/hooks/localCourse/lectureHooks";
|
||||||
import { getLectureForDay } from "@/features/local/utils/lectureUtils";
|
import { getLectureForDay } from "@/models/local/utils/lectureUtils";
|
||||||
import { getDateOnlyMarkdownString } from "@/features/local/utils/timeUtils";
|
import { getDateOnlyMarkdownString } from "@/models/local/utils/timeUtils";
|
||||||
|
|
||||||
export default function OneCourseLectures() {
|
export default function OneCourseLectures() {
|
||||||
const { courseName } = useCourseContext();
|
const { courseName } = useCourseContext();
|
||||||
const { data: weeks } = useLecturesQuery();
|
const {data: weeks} = useLecturesQuery();
|
||||||
|
|
||||||
const dayAsDate = new Date();
|
const dayAsDate = new Date();
|
||||||
const dayAsString = getDateOnlyMarkdownString(dayAsDate);
|
const dayAsString = getDateOnlyMarkdownString(dayAsDate);
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { useLocalCoursesSettingsQuery } from "@/features/local/course/localCoursesHooks";
|
import { useLocalCoursesSettingsQuery } from "@/hooks/localCourse/localCoursesHooks";
|
||||||
import OneCourseLectures from "./OneCourseLectures";
|
import OneCourseLectures from "./OneCourseLectures";
|
||||||
import { SuspenseAndErrorHandling } from "@/components/SuspenseAndErrorHandling";
|
import { SuspenseAndErrorHandling } from "@/components/SuspenseAndErrorHandling";
|
||||||
import CourseContextProvider from "../course/[courseName]/context/CourseContextProvider";
|
import CourseContextProvider from "../course/[courseName]/context/CourseContextProvider";
|
||||||
|
|||||||
@@ -2,39 +2,32 @@ import React from "react";
|
|||||||
|
|
||||||
export default function ButtonSelect<T>({
|
export default function ButtonSelect<T>({
|
||||||
options,
|
options,
|
||||||
getOptionName,
|
getName,
|
||||||
setValue,
|
setSelectedOption,
|
||||||
value,
|
selectedOption,
|
||||||
label,
|
label
|
||||||
center = false,
|
|
||||||
}: {
|
}: {
|
||||||
options: T[];
|
options: T[];
|
||||||
getOptionName: (value: T | undefined) => string;
|
getName: (value: T | undefined) => string;
|
||||||
setValue: (value: T | undefined) => void;
|
setSelectedOption: (value: T | undefined) => void;
|
||||||
value: T | undefined;
|
selectedOption: T | undefined;
|
||||||
label: string;
|
label: string;
|
||||||
center?: boolean;
|
|
||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<div className={center ? "text-center" : ""}>
|
<div>
|
||||||
<label>{label}</label>
|
<label>{label}</label>
|
||||||
<div
|
<div className="flex flex-row gap-3 flex-wrap">
|
||||||
className={
|
|
||||||
"flex flex-row gap-3 flex-wrap " + (center ? "justify-center" : "")
|
|
||||||
}
|
|
||||||
>
|
|
||||||
{options.map((o) => (
|
{options.map((o) => (
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
key={getOptionName(o)}
|
key={getName(o)}
|
||||||
className={
|
className={
|
||||||
getOptionName(o) === getOptionName(value)
|
getName(o) === getName(selectedOption) ? " " : "unstyled btn-outline"
|
||||||
? " "
|
|
||||||
: "unstyled btn-outline"
|
|
||||||
}
|
}
|
||||||
onClick={() => setValue(o)}
|
onClick={() => setSelectedOption(o)}
|
||||||
>
|
>
|
||||||
{getOptionName(o)}
|
{getName(o)}
|
||||||
</button>
|
</button>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,19 +1,14 @@
|
|||||||
import { useLocalCourseSettingsQuery } from "@/features/local/course/localCoursesHooks";
|
import { useLocalCourseSettingsQuery } from "@/hooks/localCourse/localCoursesHooks";
|
||||||
import { SuspenseAndErrorHandling } from "./SuspenseAndErrorHandling";
|
import { SuspenseAndErrorHandling } from "./SuspenseAndErrorHandling";
|
||||||
import { markdownToHTMLSafe } from "@/services/htmlMarkdownUtils";
|
import { markdownToHTMLSafe } from "@/services/htmlMarkdownUtils";
|
||||||
import { LocalCourseSettings } from "@/features/local/course/localCourseSettings";
|
import { LocalCourseSettings } from "@/models/local/localCourseSettings";
|
||||||
|
|
||||||
export default function MarkdownDisplay({
|
export default function MarkdownDisplay({
|
||||||
markdown,
|
markdown,
|
||||||
className = "",
|
className = "",
|
||||||
replaceText = [],
|
|
||||||
}: {
|
}: {
|
||||||
markdown: string;
|
markdown: string;
|
||||||
className?: string;
|
className?: string;
|
||||||
replaceText?: {
|
|
||||||
source: string;
|
|
||||||
destination: string;
|
|
||||||
}[];
|
|
||||||
}) {
|
}) {
|
||||||
const { data: settings } = useLocalCourseSettingsQuery();
|
const { data: settings } = useLocalCourseSettingsQuery();
|
||||||
return (
|
return (
|
||||||
@@ -22,7 +17,6 @@ export default function MarkdownDisplay({
|
|||||||
markdown={markdown}
|
markdown={markdown}
|
||||||
settings={settings}
|
settings={settings}
|
||||||
className={className}
|
className={className}
|
||||||
replaceText={replaceText}
|
|
||||||
/>
|
/>
|
||||||
</SuspenseAndErrorHandling>
|
</SuspenseAndErrorHandling>
|
||||||
);
|
);
|
||||||
@@ -32,25 +26,16 @@ function DangerousInnerMarkdown({
|
|||||||
markdown,
|
markdown,
|
||||||
settings,
|
settings,
|
||||||
className,
|
className,
|
||||||
replaceText,
|
|
||||||
}: {
|
}: {
|
||||||
markdown: string;
|
markdown: string;
|
||||||
settings: LocalCourseSettings;
|
settings: LocalCourseSettings;
|
||||||
className: string;
|
className: string;
|
||||||
replaceText: {
|
|
||||||
source: string;
|
|
||||||
destination: string;
|
|
||||||
}[];
|
|
||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={"markdownPreview " + className}
|
className={"markdownPreview " + className}
|
||||||
dangerouslySetInnerHTML={{
|
dangerouslySetInnerHTML={{
|
||||||
__html: markdownToHTMLSafe({
|
__html: markdownToHTMLSafe(markdown, settings),
|
||||||
markdownString: markdown,
|
|
||||||
settings,
|
|
||||||
replaceText,
|
|
||||||
}),
|
|
||||||
}}
|
}}
|
||||||
></div>
|
></div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
"use client"
|
|
||||||
import { getErrorMessage } from "@/services/utils/queryClient";
|
import { getErrorMessage } from "@/services/utils/queryClient";
|
||||||
import { QueryErrorResetBoundary } from "@tanstack/react-query";
|
import { QueryErrorResetBoundary } from "@tanstack/react-query";
|
||||||
import { FC, ReactNode, Suspense } from "react";
|
import { FC, ReactNode, Suspense } from "react";
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
"use client";
|
"use client";
|
||||||
import { SimpleTimeOnly } from "@/features/local/course/localCourseSettings";
|
import { SimpleTimeOnly } from "@/models/local/localCourseSettings";
|
||||||
import { FC } from "react";
|
import { FC } from "react";
|
||||||
|
|
||||||
export const TimePicker: FC<{
|
export const TimePicker: FC<{
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { DayOfWeek } from "@/features/local/course/localCourseSettings";
|
import { DayOfWeek } from "@/models/local/localCourseSettings";
|
||||||
|
|
||||||
export function DayOfWeekInput({
|
export function DayOfWeekInput({
|
||||||
selectedDays,
|
selectedDays,
|
||||||
|
|||||||
@@ -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>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -6,14 +6,12 @@ export default function TextInput({
|
|||||||
label,
|
label,
|
||||||
className,
|
className,
|
||||||
isTextArea = false,
|
isTextArea = false,
|
||||||
inputRef = undefined,
|
|
||||||
}: {
|
}: {
|
||||||
value: string;
|
value: string;
|
||||||
setValue: (newValue: string) => void;
|
setValue: (newValue: string) => void;
|
||||||
label: string;
|
label: string;
|
||||||
className?: string;
|
className?: string;
|
||||||
isTextArea?: boolean;
|
isTextArea?: boolean;
|
||||||
inputRef?: React.RefObject<HTMLInputElement | null>;
|
|
||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<label className={"flex flex-col " + className}>
|
<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"
|
className="bg-slate-800 border border-slate-500 rounded-md w-full px-1"
|
||||||
value={value}
|
value={value}
|
||||||
onChange={(e) => setValue(e.target.value)}
|
onChange={(e) => setValue(e.target.value)}
|
||||||
ref={inputRef}
|
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{isTextArea && (
|
{isTextArea && (
|
||||||
|
|||||||
@@ -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),
|
|
||||||
});
|
|
||||||
},
|
|
||||||
});
|
|
||||||
};
|
|
||||||
@@ -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);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -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;
|
|
||||||
};
|
|
||||||
@@ -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
Reference in New Issue
Block a user