diff --git a/@/components/ui/carousel.tsx b/@/components/ui/carousel.tsx new file mode 100644 index 0000000..9782265 --- /dev/null +++ b/@/components/ui/carousel.tsx @@ -0,0 +1,257 @@ +import * as React from "react" +import useEmblaCarousel, { + type EmblaCarouselType as CarouselApi, + type EmblaOptionsType as CarouselOptions, + type EmblaPluginType as CarouselPlugin, +} from "embla-carousel-react" +import { ArrowLeft, ArrowRight } from "lucide-react" + +import { cn } from "@/lib/utils" +import { Button } from "@/components/ui/button" + +type CarouselProps = { + opts?: CarouselOptions + plugins?: CarouselPlugin[] + orientation?: "horizontal" | "vertical" + setApi?: (api: CarouselApi) => void +} + +type CarouselContextProps = { + carouselRef: ReturnType[0] + api: ReturnType[1] + scrollPrev: () => void + scrollNext: () => void + canScrollPrev: boolean + canScrollNext: boolean +} & CarouselProps + +const CarouselContext = React.createContext(null) + +function useCarousel() { + const context = React.useContext(CarouselContext) + + if (!context) { + throw new Error("useCarousel must be used within a ") + } + + return context +} + +const Carousel = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes & CarouselProps +>( + ( + { + orientation = "horizontal", + opts, + setApi, + plugins, + className, + children, + ...props + }, + ref + ) => { + const [carouselRef, api] = useEmblaCarousel( + { + ...opts, + axis: orientation === "horizontal" ? "x" : "y", + }, + plugins + ) + const [canScrollPrev, setCanScrollPrev] = React.useState(false) + const [canScrollNext, setCanScrollNext] = React.useState(false) + + const onSelect = React.useCallback((api: CarouselApi) => { + if (!api) { + return + } + + setCanScrollPrev(api.canScrollPrev()) + setCanScrollNext(api.canScrollNext()) + }, []) + + const scrollPrev = React.useCallback(() => { + api?.scrollPrev() + }, [api]) + + const scrollNext = React.useCallback(() => { + api?.scrollNext() + }, [api]) + + const handleKeyDown = React.useCallback( + (event: React.KeyboardEvent) => { + if (event.key === "ArrowLeft") { + event.preventDefault() + scrollPrev() + } else if (event.key === "ArrowRight") { + event.preventDefault() + scrollNext() + } + }, + [scrollPrev, scrollNext] + ) + + React.useEffect(() => { + if (!api || !setApi) { + return + } + + setApi(api) + }, [api, setApi]) + + React.useEffect(() => { + if (!api) { + return + } + + onSelect(api) + api.on("reInit", onSelect) + api.on("select", onSelect) + + return () => { + api?.off("select", onSelect) + } + }, [api, onSelect]) + + return ( + +
+ {children} +
+
+ ) + } +) +Carousel.displayName = "Carousel" + +const CarouselContent = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => { + const { carouselRef, orientation } = useCarousel() + + return ( +
+
+
+ ) +}) +CarouselContent.displayName = "CarouselContent" + +const CarouselItem = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => { + const { orientation } = useCarousel() + + return ( +
+ ) +}) +CarouselItem.displayName = "CarouselItem" + +const CarouselPrevious = React.forwardRef< + HTMLButtonElement, + React.ComponentProps +>(({ className, variant = "outline", size = "icon", ...props }, ref) => { + const { orientation, scrollPrev, canScrollPrev } = useCarousel() + + return ( + + ) +}) +CarouselPrevious.displayName = "CarouselPrevious" + +const CarouselNext = React.forwardRef< + HTMLButtonElement, + React.ComponentProps +>(({ className, variant = "outline", size = "icon", ...props }, ref) => { + const { orientation, scrollNext, canScrollNext } = useCarousel() + + return ( + + ) +}) +CarouselNext.displayName = "CarouselNext" + +export { + type CarouselApi, + Carousel, + CarouselContent, + CarouselItem, + CarouselPrevious, + CarouselNext, +} diff --git a/@/components/ui/resizable.tsx b/@/components/ui/resizable.tsx new file mode 100644 index 0000000..cd3cb0e --- /dev/null +++ b/@/components/ui/resizable.tsx @@ -0,0 +1,43 @@ +import { GripVertical } from "lucide-react" +import * as ResizablePrimitive from "react-resizable-panels" + +import { cn } from "@/lib/utils" + +const ResizablePanelGroup = ({ + className, + ...props +}: React.ComponentProps) => ( + +) + +const ResizablePanel = ResizablePrimitive.Panel + +const ResizableHandle = ({ + withHandle, + className, + ...props +}: React.ComponentProps & { + withHandle?: boolean +}) => ( + div]:rotate-90", + className + )} + {...props} + > + {withHandle && ( +
+ +
+ )} +
+) + +export { ResizablePanelGroup, ResizablePanel, ResizableHandle } diff --git a/package-lock.json b/package-lock.json index de435f8..fa35325 100644 --- a/package-lock.json +++ b/package-lock.json @@ -77,6 +77,7 @@ "class-variance-authority": "^0.7.0", "clsx": "^2.0.0", "cmdk": "^0.2.0", + "embla-carousel-react": "^8.0.0-rc19", "lucide-react": "^0.288.0", "prop-types": "^15.8.1", "react": "^18.2.0", @@ -85,6 +86,7 @@ "react-dom": "^18.2.0", "react-hook-form": "^7.47.0", "react-lite-youtube-embed": "^2.3.52", + "react-resizable-panels": "^1.0.7", "react-router-dom": "^6.15.0", "react-sortablejs": "^6.1.4", "react-tweet": "^3.1.1", @@ -4437,6 +4439,31 @@ "integrity": "sha512-FFa8QKjQK/A5QuFr2167myhMesGrhlOBD+3cYNxO9/S4XzHEXesyTD/1/xF644gC8buFPz3ca6G1LOQD0tZrrg==", "dev": true }, + "node_modules/embla-carousel": { + "version": "8.0.0-rc19", + "resolved": "https://registry.npmjs.org/embla-carousel/-/embla-carousel-8.0.0-rc19.tgz", + "integrity": "sha512-PAChVyYoVZo8subkBN8LjZ7+0vk4CmVvMnxH0Y2ux76VUEUBl1wk5xDo8+MUhH5MXU6ZrgkBpMe++bKob1Z+2g==" + }, + "node_modules/embla-carousel-react": { + "version": "8.0.0-rc19", + "resolved": "https://registry.npmjs.org/embla-carousel-react/-/embla-carousel-react-8.0.0-rc19.tgz", + "integrity": "sha512-4BBj1HvlUqhWXFyDJOL/JbQ74jtekfdH646B1wQzM9QmWn6CEcbD/SmovKqc6B5jYTKaaGEGEEw7bpUJRajA8w==", + "dependencies": { + "embla-carousel": "8.0.0-rc19", + "embla-carousel-reactive-utils": "8.0.0-rc19" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.1 || ^18.0.0" + } + }, + "node_modules/embla-carousel-reactive-utils": { + "version": "8.0.0-rc19", + "resolved": "https://registry.npmjs.org/embla-carousel-reactive-utils/-/embla-carousel-reactive-utils-8.0.0-rc19.tgz", + "integrity": "sha512-ed9NppY0OxTtrSIwTCYNcMLlQfSNcNy8Zsw8uIG0te3qrhvQ2ePPsbcElK2SRAV8VMU6G7JQweQIb6amzYMDXA==", + "peerDependencies": { + "embla-carousel": "8.0.0-rc19" + } + }, "node_modules/entities": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/entities/-/entities-2.2.0.tgz", @@ -6174,6 +6201,15 @@ } } }, + "node_modules/react-resizable-panels": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/react-resizable-panels/-/react-resizable-panels-1.0.7.tgz", + "integrity": "sha512-CluJkHQheeNqIJly2FYDfri3ME+2h2nCXpf0Y+hTO1K1eVtNxXFA5hVp5cUD6NS70iiufswOmnku9QZiLr1hYg==", + "peerDependencies": { + "react": "^16.14.0 || ^17.0.0 || ^18.0.0", + "react-dom": "^16.14.0 || ^17.0.0 || ^18.0.0" + } + }, "node_modules/react-router": { "version": "6.15.0", "resolved": "https://registry.npmjs.org/react-router/-/react-router-6.15.0.tgz", diff --git a/package.json b/package.json index e2badf0..8e3026e 100644 --- a/package.json +++ b/package.json @@ -4,7 +4,7 @@ "version": "0.0.0", "type": "module", "scripts": { - "dev": "vite --host", + "dev": "vite", "build": "tsc && vite build", "lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0", "preview": "vite preview" @@ -79,6 +79,7 @@ "class-variance-authority": "^0.7.0", "clsx": "^2.0.0", "cmdk": "^0.2.0", + "embla-carousel-react": "^8.0.0-rc19", "lucide-react": "^0.288.0", "prop-types": "^15.8.1", "react": "^18.2.0", @@ -87,6 +88,7 @@ "react-dom": "^18.2.0", "react-hook-form": "^7.47.0", "react-lite-youtube-embed": "^2.3.52", + "react-resizable-panels": "^1.0.7", "react-router-dom": "^6.15.0", "react-sortablejs": "^6.1.4", "react-tweet": "^3.1.1", diff --git a/src/App.tsx b/src/App.tsx index 1d534b0..ffe6542 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -21,7 +21,7 @@ function App() { useEffect(() => { const token = localStorage.getItem("token"); - const account_id = Number(localStorage.getItem("account_id")); + const account_id = String(localStorage.getItem("account_id")); if (!token || !account_id) { return; @@ -31,6 +31,12 @@ function App() { if (response.data.result) { setIsLogin(true); } + else { + localStorage.removeItem("token"); + localStorage.removeItem("account_id"); + localStorage.removeItem("username"); + + } }); }, []); diff --git a/src/components/AddProblemDialog.tsx b/src/components/AddProblemDialog.tsx deleted file mode 100644 index adfea69..0000000 --- a/src/components/AddProblemDialog.tsx +++ /dev/null @@ -1,39 +0,0 @@ -import React, { useEffect, useState } from "react"; -import { Dialog, DialogTrigger, DialogContent } from "./shadcn/Dialog"; -import { Input } from "./shadcn/Input"; -import MyProblemCard from "./MyProblemCard"; -import { - ProblemModel, - ProblemSecureModel, -} from "../types/models/Problem.model"; -import { ProblemService } from "../services/Problem.service"; - -const AddProblemDialog = ({ children }: { children: React.ReactNode }) => { - const [problems, setProblems] = useState< - ProblemSecureModel[] | ProblemModel[] - >([]); - - useEffect(() => { - ProblemService.getAllByAccount(4).then((response) => { - setProblems(response.data.problems); - }); - }); - - return ( - - {children} - -

Add Problem

- - -
- {problems.map((problem) => ( - - ))} -
-
-
- ); -}; - -export default AddProblemDialog; diff --git a/src/components/AccountCheckboxCard.tsx b/src/components/Cards/AccountCards/AccountCheckboxCard.tsx similarity index 80% rename from src/components/AccountCheckboxCard.tsx rename to src/components/Cards/AccountCards/AccountCheckboxCard.tsx index 4fb7237..b3a6ecd 100644 --- a/src/components/AccountCheckboxCard.tsx +++ b/src/components/Cards/AccountCards/AccountCheckboxCard.tsx @@ -1,8 +1,8 @@ import React from "react"; -import { Card } from "./shadcn/Card"; -import { Checkbox } from "./shadcn/Checkbox"; +import { Card } from "../../shadcn/Card"; +import { Checkbox } from "../../shadcn/Checkbox"; import { useState } from "react"; -import { AccountSecureModel } from "../types/models/Account.model"; +import { AccountSecureModel } from "../../../types/models/Account.model"; export type AccountCheckboxCardOnClickCallback = { checked: boolean; diff --git a/src/components/AccountMiniCard.tsx b/src/components/Cards/AccountCards/AccountMiniCard.tsx similarity index 78% rename from src/components/AccountMiniCard.tsx rename to src/components/Cards/AccountCards/AccountMiniCard.tsx index 8288edf..5e7b727 100644 --- a/src/components/AccountMiniCard.tsx +++ b/src/components/Cards/AccountCards/AccountMiniCard.tsx @@ -1,6 +1,6 @@ import React, { useEffect, useState } from "react"; -import { Card, CardContent, CardTitle } from "./shadcn/Card"; -import { Button } from "./shadcn/Button"; +import { Card, CardContent, CardTitle } from "../../shadcn/Card"; +import { Button } from "../../shadcn/Button"; import { Check, CheckCircle2, @@ -19,19 +19,19 @@ import { ProblemPopulateTestcases, ProblemSecureModel, TestcaseModel, -} from "../types/models/Problem.model"; -import { readableDateFormat } from "../utilities/ReadableDateFormat"; +} from "../../../types/models/Problem.model"; +import { readableDateFormat } from "../../../utilities/ReadableDateFormat"; import { ContextMenu, ContextMenuTrigger, ContextMenuContent, ContextMenuItem, -} from "./shadcn/ContextMenu"; -import DeleteProblemConfirmationDialog from "./DeleteProblemConfirmationDialog"; -import Checkmark from "./Checkmark"; -import { Tooltip, TooltipContent, TooltipTrigger } from "./shadcn/Tooltip"; -import { CollectionModel, CollectionPopulateProblemSecureModel } from "../types/models/Collection.model"; -import { AccountModel, AccountSecureModel } from "../types/models/Account.model"; +} from "../../shadcn/ContextMenu"; +import DeleteProblemConfirmationDialog from "../../Dialogs/DeleteProblemConfirmationDialog"; +import Checkmark from "../../Checkmark"; +import { Tooltip, TooltipContent, TooltipTrigger } from "../../shadcn/Tooltip"; +import { CollectionModel, CollectionPopulateProblemSecureModel } from "../../../types/models/Collection.model"; +import { AccountModel, AccountSecureModel } from "../../../types/models/Account.model"; const AccountMiniCard = ({ // problem, diff --git a/src/components/Cards/AccountCards/AccountMiniCard2.tsx b/src/components/Cards/AccountCards/AccountMiniCard2.tsx new file mode 100644 index 0000000..9ee38e8 --- /dev/null +++ b/src/components/Cards/AccountCards/AccountMiniCard2.tsx @@ -0,0 +1,103 @@ +import React, { useEffect, useState } from "react"; +import { Card, CardContent, CardTitle } from "../../shadcn/Card"; +import { Button } from "../../shadcn/Button"; +import { + Check, + CheckCircle2, + FileSpreadsheet, + Folder, + Pencil, + PencilIcon, + Trash, + User, + Users, + X, +} from "lucide-react"; +import { useNavigate } from "react-router-dom"; +import { + ProblemModel, + ProblemPopulateTestcases, + ProblemSecureModel, + TestcaseModel, +} from "../../../types/models/Problem.model"; +import { readableDateFormat } from "../../../utilities/ReadableDateFormat"; +import { + ContextMenu, + ContextMenuTrigger, + ContextMenuContent, + ContextMenuItem, +} from "../../shadcn/ContextMenu"; +import DeleteProblemConfirmationDialog from "../../Dialogs/DeleteProblemConfirmationDialog"; +import Checkmark from "../../Checkmark"; +import { Tooltip, TooltipContent, TooltipTrigger } from "../../shadcn/Tooltip"; +import { CollectionModel, CollectionPopulateProblemSecureModel } from "../../../types/models/Collection.model"; +import { AccountModel, AccountSecureModel } from "../../../types/models/Account.model"; + +const AccountMiniCard2 = ({ + // problem, + account, + disabled=false, + disabledHighlight=false, + onClick=()=>{} +}: { + // problem: ProblemPopulateTestcases | ProblemSecureModel | ProblemModel; + account: AccountModel | AccountSecureModel; + disabled?: boolean; + disabledHighlight?: boolean; + onClick?: () => void; +}) => { + const navigate = useNavigate(); + + const [highlightTitle, setHighlightTitle] = useState(false); + const [toolVisible, setToolVisible] = useState(true); + + const handleMouseOver = () => { + setHighlightTitle(true); + setToolVisible(true); + }; + const handleMouseOut = () => { + setHighlightTitle(false); + setToolVisible(false); + }; + + const customCardCSS = ():string => { + let className = "p-2 cursor-pointer "; + + if (disabled) { + className += "opacity-50 "; + } + else{ + if (highlightTitle && !disabledHighlight) { + className += "border-green-500 bg-green-100 "; + } + } + return className; + } + + return ( + account && ( + // + onClick()} + onMouseOver={handleMouseOver} + onMouseOut={handleMouseOut} + className={customCardCSS()} + + // className={`pt-6 px-5 ${disabled ? "opacity-50" : }`}`} + > +
+
+ +

{account.username}

+
+ {/*
+ {collection.problems.length} +
*/} +
+
+ //
+ ) + ); +}; + +export default AccountMiniCard2; diff --git a/src/components/MyCollectionCard.tsx b/src/components/Cards/CollectionCards/MyCollectionCard.tsx similarity index 72% rename from src/components/MyCollectionCard.tsx rename to src/components/Cards/CollectionCards/MyCollectionCard.tsx index c5ecc06..97d59b9 100644 --- a/src/components/MyCollectionCard.tsx +++ b/src/components/Cards/CollectionCards/MyCollectionCard.tsx @@ -1,17 +1,17 @@ -import React, { useState } from "react"; -import { Card, CardContent, CardTitle } from "./shadcn/Card"; -import { Button } from "./shadcn/Button"; -import { Check, CheckCircle2, FileSpreadsheet, Folder, X } from "lucide-react"; +import { FileSpreadsheet, Folder } from "lucide-react"; +import { useState } from "react"; import { useNavigate } from "react-router-dom"; -import { ProblemModel } from "../types/models/Problem.model"; -import { readableDateFormat } from "../utilities/ReadableDateFormat"; -import Checkmark from "./Checkmark"; -import { CollectionProblemModel, CollectionProblemPopulateProblemSecureModel, GetCollectionByAccountResponse } from "../types/models/Collection.model"; +import { CollectionPopulateCollectionProblemPopulateProblemModel, CollectionProblemModel } from "../../../types/models/Collection.model"; +import { readableDateFormat } from "../../../utilities/ReadableDateFormat"; +import Checkmark from "../../Checkmark"; +import { Card, CardContent } from "../../shadcn/Card"; +import { onMiddleClickOpenInNewTab } from "../../../utilities/OnMiddleClickOpenInNewTab"; +import MyCollectionContextMenu from "../../ContextMenus/MyCollectionContextMenu"; const MyCollectionCard = ({ collection }:{ - collection: CollectionProblemModel + collection: CollectionPopulateCollectionProblemPopulateProblemModel }) => { const navigate = useNavigate(); @@ -29,8 +29,10 @@ const MyCollectionCard = ({ }; return ( - navigate(`/my/collections/${collection.collection_id}`)} + + onMiddleClickOpenInNewTab(e,`/my/collections/${collection.collection_id}/edit`)} + onClick={() => navigate(`/my/collections/${collection.collection_id}/edit`)} onMouseOver={handleMouseOver} onMouseOut={handleMouseOut} className={`pt-6 px-5 cursor-pointer ${ @@ -88,6 +90,7 @@ const MyCollectionCard = ({
+ ); }; diff --git a/src/components/MyCollectionMiniCard.tsx b/src/components/Cards/CollectionCards/MyCollectionMiniCard.tsx similarity index 87% rename from src/components/MyCollectionMiniCard.tsx rename to src/components/Cards/CollectionCards/MyCollectionMiniCard.tsx index 77a29e3..928a21b 100644 --- a/src/components/MyCollectionMiniCard.tsx +++ b/src/components/Cards/CollectionCards/MyCollectionMiniCard.tsx @@ -1,6 +1,6 @@ import React, { useEffect, useState } from "react"; -import { Card, CardContent, CardTitle } from "./shadcn/Card"; -import { Button } from "./shadcn/Button"; +import { Card, CardContent, CardTitle } from "../../shadcn/Card"; +import { Button } from "../../shadcn/Button"; import { Check, CheckCircle2, @@ -17,18 +17,18 @@ import { ProblemPopulateTestcases, ProblemSecureModel, TestcaseModel, -} from "../types/models/Problem.model"; -import { readableDateFormat } from "../utilities/ReadableDateFormat"; +} from "../../../types/models/Problem.model"; +import { readableDateFormat } from "../../../utilities/ReadableDateFormat"; import { ContextMenu, ContextMenuTrigger, ContextMenuContent, ContextMenuItem, -} from "./shadcn/ContextMenu"; -import DeleteProblemConfirmationDialog from "./DeleteProblemConfirmationDialog"; -import Checkmark from "./Checkmark"; -import { Tooltip, TooltipContent, TooltipTrigger } from "./shadcn/Tooltip"; -import { CollectionModel, CollectionPopulateProblemSecureModel } from "../types/models/Collection.model"; +} from "../../shadcn/ContextMenu"; +import DeleteProblemConfirmationDialog from "../../Dialogs/DeleteProblemConfirmationDialog"; +import Checkmark from "../../Checkmark"; +import { Tooltip, TooltipContent, TooltipTrigger } from "../../shadcn/Tooltip"; +import { CollectionModel, CollectionPopulateProblemSecureModel } from "../../../types/models/Collection.model"; const checkRuntimeStatus = (testcases: TestcaseModel[]) => { for (const testcase of testcases) { diff --git a/src/components/Cards/CollectionCards/MyCollectionMiniCard2.tsx b/src/components/Cards/CollectionCards/MyCollectionMiniCard2.tsx new file mode 100644 index 0000000..f732a3c --- /dev/null +++ b/src/components/Cards/CollectionCards/MyCollectionMiniCard2.tsx @@ -0,0 +1,150 @@ +import React, { useEffect, useState } from "react"; +import { Card, CardContent, CardTitle } from "../../shadcn/Card"; +import { Button } from "../../shadcn/Button"; +import { + Check, + CheckCircle2, + FileSpreadsheet, + Folder, + Pencil, + PencilIcon, + Trash, + X, +} from "lucide-react"; +import { useNavigate } from "react-router-dom"; +import { + ProblemModel, + ProblemPopulateTestcases, + ProblemSecureModel, + TestcaseModel, +} from "../../../types/models/Problem.model"; +import { readableDateFormat } from "../../../utilities/ReadableDateFormat"; +import { + ContextMenu, + ContextMenuTrigger, + ContextMenuContent, + ContextMenuItem, +} from "../../shadcn/ContextMenu"; +import DeleteProblemConfirmationDialog from "../../Dialogs/DeleteProblemConfirmationDialog"; +import Checkmark from "../../Checkmark"; +import { Tooltip, TooltipContent, TooltipTrigger } from "../../shadcn/Tooltip"; +import { + CollectionModel, + CollectionPopulateCollectionProblemPopulateProblemModel, + CollectionPopulateProblemSecureModel, + CollectionProblemPopulateProblemModel, +} from "../../../types/models/Collection.model"; +import { onMiddleClickOpenInNewTab } from "../../../utilities/OnMiddleClickOpenInNewTab"; +import MyCollectionContextMenu from "../../ContextMenus/MyCollectionContextMenu"; + +const checkRuntimeStatus = (testcases: TestcaseModel[]) => { + for (const testcase of testcases) { + if (testcase.runtime_status !== "OK") { + return false; + } + } + return true; +}; + +// const MyCollectionContextMenu = ({ +// children, +// collection, +// }: { +// children: React.ReactNode; +// collection: CollectionModel; +// }) => { + +// const navigate = useNavigate(); +// const [openDeleteDialog, setOpenDeleteDialog] = useState(false); + +// return ( +// +// {/* window.location.reload()} +// /> */} +// {children} +// +// navigate(`/my/collections/${collection.collection_id}`)}> +// +// Edit +// +// {/* setOpenDeleteDialog(true)}> +// +// Delete +// */} +// +// +// ); +// }; + +const MyCollectionMiniCard2 = ({ + // problem, + collection, + disabled = false, + disabledHighlight = false, + onClick = () => {}, +}: { + // problem: ProblemPopulateTestcases | ProblemSecureModel | ProblemModel; + collection: CollectionPopulateProblemSecureModel | CollectionPopulateCollectionProblemPopulateProblemModel; + disabled?: boolean; + disabledHighlight?: boolean; + onClick?: () => void; +}) => { + const navigate = useNavigate(); + + const [highlightTitle, setHighlightTitle] = useState(false); + const [toolVisible, setToolVisible] = useState(true); + + const handleMouseOver = () => { + setHighlightTitle(true); + setToolVisible(true); + }; + const handleMouseOut = () => { + setHighlightTitle(false); + setToolVisible(false); + }; + + const customCardCSS = (): string => { + let className = "p-2 cursor-pointer "; + + if (disabled) { + className += "opacity-50 "; + } else { + if (highlightTitle && !disabledHighlight) { + className += "border-green-500 bg-green-100 "; + } + } + return className; + }; + + return ( + collection && ( + + onMiddleClickOpenInNewTab(e,`/my/collections/${collection.collection_id}`)} + onClick={() => onClick()} + onMouseOver={handleMouseOver} + onMouseOut={handleMouseOut} + className={customCardCSS()} + + // className={`pt-6 px-5 ${disabled ? "opacity-50" : }`}`} + > +
+
+ +

{collection.name}

+
+
+ {collection.problems.length} +
+
+
+
+ ) + ); +}; + +export default MyCollectionMiniCard2; diff --git a/src/components/TopicCollectionAccordionCard.tsx b/src/components/Cards/CollectionCards/TopicCollectionAccordionCard.tsx similarity index 83% rename from src/components/TopicCollectionAccordionCard.tsx rename to src/components/Cards/CollectionCards/TopicCollectionAccordionCard.tsx index 0b77310..a2a35bc 100644 --- a/src/components/TopicCollectionAccordionCard.tsx +++ b/src/components/Cards/CollectionCards/TopicCollectionAccordionCard.tsx @@ -1,19 +1,19 @@ import React from "react"; -import { Card } from "./shadcn/Card"; +import { Card } from "../../shadcn/Card"; import { Accordion, AccordionContent, AccordionItem, AccordionTrigger, -} from "./shadcn/Accordion"; +} from "../../shadcn/Accordion"; import { Check, FileCheck, FileSpreadsheet, Folder } from "lucide-react"; -import PublicProblemCard from "./PublicProblemCard"; -import { CollectionPopulateCollectionProblemPopulateProblemPopulateAccountAndSubmissionPopulateSubmissionTestcasesSecureModel } from "../types/models/Collection.model"; -import CardContainer from "./CardContainer"; -import { ScrollArea } from "./shadcn/ScrollArea"; -import PublicProblemMiniCard from "./PublicProblemMiniCard"; -import ReadOnlyPlate from "./ReadOnlyPlate"; -import { Badge } from "./shadcn/Badge"; +import PublicProblemCard from "../ProblemCards/PublicProblemCard"; +import { CollectionPopulateCollectionProblemPopulateProblemPopulateAccountAndSubmissionPopulateSubmissionTestcasesSecureModel } from "../../../types/models/Collection.model"; +import CardContainer from "../../CardContainer"; +import { ScrollArea } from "../../shadcn/ScrollArea"; +import PublicProblemMiniCard from "../ProblemCards/PublicProblemMiniCard"; +import ReadOnlyPlate from "../../ReadOnlyPlate"; +import { Badge } from "../../shadcn/Badge"; const isPassed = (collection: CollectionPopulateCollectionProblemPopulateProblemPopulateAccountAndSubmissionPopulateSubmissionTestcasesSecureModel):boolean => { return collection.problems.filter( diff --git a/src/components/Cards/CourseCards/MyCourseCard.tsx b/src/components/Cards/CourseCards/MyCourseCard.tsx new file mode 100644 index 0000000..bc1e52d --- /dev/null +++ b/src/components/Cards/CourseCards/MyCourseCard.tsx @@ -0,0 +1,84 @@ +import React, { useState } from "react"; +import { Card, CardContent } from "../../shadcn/Card"; +import { Folder, LibraryBig } from "lucide-react"; +import { TopicPopulateTopicCollectionPopulateCollectionModel } from "../../../types/models/Topic.model"; +import { readableDateFormat } from "../../../utilities/ReadableDateFormat"; +import { BASE_URL } from "../../../constants/BackendBaseURL"; +import { useNavigate } from "react-router-dom"; +import { onMiddleClickOpenInNewTab } from "../../../utilities/OnMiddleClickOpenInNewTab"; +import MyCourseContextMenu from "../../ContextMenus/MyCourseContextMenu"; + +const MyCourseCard = ({ + course, +}: { + course: TopicPopulateTopicCollectionPopulateCollectionModel; +}) => { + const navigate = useNavigate(); + const [mouseOver, setMouseOver] = useState(false); + + return ( + + + onMiddleClickOpenInNewTab( + e, + `/my/courses/${course.topic_id}` + ) + } + onClick={() => navigate(`/my/courses/${course.topic_id}/edit`)} + onMouseOver={() => setMouseOver(true)} + onMouseOut={() => setMouseOver(false)} + className={`pt-6 px-5 cursor-pointer ${ + mouseOver ? "border-green-500 bg-green-100" : "" + }`} + > + {/*
+ */} + + +
+ + {mouseOver ? ( +

{course.name}

+ ) : ( + course.name + )} +
+
+
+
+

Lasted Updated

+

+ {readableDateFormat(course.updated_date)} +

+
+
+

Created Date

+

+ {readableDateFormat(course.created_date)} +

+
+
+ +
+
+

Visibility

+

Public

+
+
+ +
+

+ + Collections ({course.collections.length}) +

+
+
+
+ {/*
*/} +
+
+ ); +}; + +export default MyCourseCard; diff --git a/src/components/Cards/CourseCards/PublicCourseCard.tsx b/src/components/Cards/CourseCards/PublicCourseCard.tsx new file mode 100644 index 0000000..8d88fc1 --- /dev/null +++ b/src/components/Cards/CourseCards/PublicCourseCard.tsx @@ -0,0 +1,29 @@ +import React from "react"; +import { Card } from "../../shadcn/Card"; +import { LibraryBig, StepForward } from "lucide-react"; +import { Button } from "../../shadcn/Button"; +import { TopicModel } from "../../../types/models/Topic.model"; +import { useNavigate } from "react-router-dom"; + +const PublicCourseCard = ({ course }: { course: TopicModel }) => { + + const navigate = useNavigate(); + + return ( + +
+ +

{course?.name}

+
+ +
+ +
+
+ ); +}; + +export default PublicCourseCard; diff --git a/src/components/MyGroupCard.tsx b/src/components/Cards/MyGroupCard.tsx similarity index 84% rename from src/components/MyGroupCard.tsx rename to src/components/Cards/MyGroupCard.tsx index 3ce4f49..44fcb54 100644 --- a/src/components/MyGroupCard.tsx +++ b/src/components/Cards/MyGroupCard.tsx @@ -1,9 +1,10 @@ import React, { useState } from "react"; -import { Card, CardContent } from "./shadcn/Card"; +import { Card, CardContent } from "../shadcn/Card"; import { useNavigate } from "react-router-dom"; import { User, Users } from "lucide-react"; -import { readableDateFormat } from "../utilities/ReadableDateFormat"; -import { GroupPopulateGroupMemberPopulateAccountSecureModel } from "../types/models/Group.model"; +import { readableDateFormat } from "../../utilities/ReadableDateFormat"; +import { GroupPopulateGroupMemberPopulateAccountSecureModel } from "../../types/models/Group.model"; +import { onMiddleClickOpenInNewTab } from "../../utilities/OnMiddleClickOpenInNewTab"; const MyGroupCard = ({ group @@ -19,6 +20,7 @@ const MyGroupCard = ({ return ( onMiddleClickOpenInNewTab(e,`/my/groups/${group?.group_id}`)} onMouseOver={() => setMouseOver(true)} onMouseOut={() => setMouseOver(false)} className={`pt-6 px-5 cursor-pointer ${ diff --git a/src/components/MyProblemCard.tsx b/src/components/Cards/ProblemCards/MyProblemCard.tsx similarity index 65% rename from src/components/MyProblemCard.tsx rename to src/components/Cards/ProblemCards/MyProblemCard.tsx index b4492e0..ae73822 100644 --- a/src/components/MyProblemCard.tsx +++ b/src/components/Cards/ProblemCards/MyProblemCard.tsx @@ -1,31 +1,26 @@ -import React, { useState } from "react"; -import { Card, CardContent, CardTitle } from "./shadcn/Card"; -import { Button } from "./shadcn/Button"; import { - Check, - CheckCircle2, FileSpreadsheet, - Pencil, PencilIcon, - Trash, - X, + Trash } from "lucide-react"; +import React, { useState } from "react"; import { useNavigate } from "react-router-dom"; import { - ProblemModel, ProblemPopulateTestcases, - ProblemSecureModel, - TestcaseModel, -} from "../types/models/Problem.model"; -import { readableDateFormat } from "../utilities/ReadableDateFormat"; + TestcaseModel +} from "../../../types/models/Problem.model"; +import { readableDateFormat } from "../../../utilities/ReadableDateFormat"; +import { onMiddleClickOpenInNewTab } from "../../../utilities/OnMiddleClickOpenInNewTab"; +import Checkmark from "../../Checkmark"; +import DeleteProblemConfirmationDialog from "../../Dialogs/DeleteProblemConfirmationDialog"; +import { Card, CardContent } from "../../shadcn/Card"; import { ContextMenu, - ContextMenuTrigger, ContextMenuContent, ContextMenuItem, -} from "./shadcn/ContextMenu"; -import DeleteProblemConfirmationDialog from "./DeleteProblemConfirmationDialog"; -import Checkmark from "./Checkmark"; + ContextMenuTrigger, +} from "../../shadcn/ContextMenu"; +import MyProblemContextMenu from "../../ContextMenus/MyProblemContextMenu"; const checkRuntimeStatus = (testcases: TestcaseModel[]) => { for (const testcase of testcases) { @@ -36,35 +31,35 @@ const checkRuntimeStatus = (testcases: TestcaseModel[]) => { return true; }; -const MyProblemContextMenu = ({ children,problem }: { - children: React.ReactNode - problem: ProblemPopulateTestcases -}) => { +// const MyProblemContextMenu = ({ children,problem }: { +// children: React.ReactNode +// problem: ProblemPopulateTestcases +// }) => { - const [openDeleteDialog, setOpenDeleteDialog] = useState(false); +// const [openDeleteDialog, setOpenDeleteDialog] = useState(false); - return ( - - window.location.reload()} - /> - {children} - - - - Edit - - setOpenDeleteDialog(true)}> - - Delete - - - - ); -}; +// return ( +// +// window.location.reload()} +// /> +// {children} +// +// +// +// Edit +// +// setOpenDeleteDialog(true)}> +// +// Delete +// +// +// +// ); +// }; const MyProblemCard = ({ problem }: { problem: ProblemPopulateTestcases }) => { const navigate = useNavigate(); @@ -86,7 +81,8 @@ const MyProblemCard = ({ problem }: { problem: ProblemPopulateTestcases }) => { navigate(`/my/problems/${problem.problem_id}`)} + onMouseDown={(e) => onMiddleClickOpenInNewTab(e,`/my/problems/${problem.problem_id}/edit`)} + onClick={() => navigate(`/my/problems/${problem.problem_id}/edit`)} onMouseOver={handleMouseOver} onMouseOut={handleMouseOut} className={`pt-6 px-5 cursor-pointer ${ diff --git a/src/components/MyProblemMiniCard.tsx b/src/components/Cards/ProblemCards/MyProblemMiniCard.tsx similarity index 88% rename from src/components/MyProblemMiniCard.tsx rename to src/components/Cards/ProblemCards/MyProblemMiniCard.tsx index dae3ef9..496c41e 100644 --- a/src/components/MyProblemMiniCard.tsx +++ b/src/components/Cards/ProblemCards/MyProblemMiniCard.tsx @@ -1,6 +1,6 @@ import React, { useEffect, useState } from "react"; -import { Card, CardContent, CardTitle } from "./shadcn/Card"; -import { Button } from "./shadcn/Button"; +import { Card, CardContent, CardTitle } from "../../shadcn/Card"; +import { Button } from "../../shadcn/Button"; import { Check, CheckCircle2, @@ -16,17 +16,17 @@ import { ProblemPopulateTestcases, ProblemSecureModel, TestcaseModel, -} from "../types/models/Problem.model"; -import { readableDateFormat } from "../utilities/ReadableDateFormat"; +} from "../../../types/models/Problem.model"; +import { readableDateFormat } from "../../../utilities/ReadableDateFormat"; import { ContextMenu, ContextMenuTrigger, ContextMenuContent, ContextMenuItem, -} from "./shadcn/ContextMenu"; -import DeleteProblemConfirmationDialog from "./DeleteProblemConfirmationDialog"; -import Checkmark from "./Checkmark"; -import { Tooltip, TooltipContent, TooltipTrigger } from "./shadcn/Tooltip"; +} from "../../shadcn/ContextMenu"; +import DeleteProblemConfirmationDialog from "../../Dialogs/DeleteProblemConfirmationDialog"; +import Checkmark from "../../Checkmark"; +import { Tooltip, TooltipContent, TooltipTrigger } from "../../shadcn/Tooltip"; const checkRuntimeStatus = (testcases: TestcaseModel[]) => { for (const testcase of testcases) { @@ -119,7 +119,7 @@ const MyProblemMiniCard = ({ // className={`pt-6 px-5 ${disabled ? "opacity-50" : }`}`} > - + {/* */}
@@ -149,7 +149,7 @@ const MyProblemMiniCard = ({
-
+ {/*
*/}
) diff --git a/src/components/Cards/ProblemCards/MyProblemMiniCard2.tsx b/src/components/Cards/ProblemCards/MyProblemMiniCard2.tsx new file mode 100644 index 0000000..d90a39e --- /dev/null +++ b/src/components/Cards/ProblemCards/MyProblemMiniCard2.tsx @@ -0,0 +1,143 @@ +import React, { useEffect, useState } from "react"; +import { Card, CardContent, CardTitle } from "../../shadcn/Card"; +import { Button } from "../../shadcn/Button"; +import { + Check, + CheckCircle2, + FileSpreadsheet, + Pencil, + PencilIcon, + Trash, + X, +} from "lucide-react"; +import { useNavigate } from "react-router-dom"; +import { + ProblemModel, + ProblemPopulateTestcases, + ProblemSecureModel, + TestcaseModel, +} from "../../../types/models/Problem.model"; +import { readableDateFormat } from "../../../utilities/ReadableDateFormat"; +import { + ContextMenu, + ContextMenuTrigger, + ContextMenuContent, + ContextMenuItem, +} from "../../shadcn/ContextMenu"; +import DeleteProblemConfirmationDialog from "../../Dialogs/DeleteProblemConfirmationDialog"; +import Checkmark from "../../Checkmark"; +import { Tooltip, TooltipContent, TooltipTrigger } from "../../shadcn/Tooltip"; +import { onMiddleClickOpenInNewTab } from "../../../utilities/OnMiddleClickOpenInNewTab"; +import MyProblemContextMenu from "../../ContextMenus/MyProblemContextMenu"; + +const checkRuntimeStatus = (testcases: TestcaseModel[]) => { + for (const testcase of testcases) { + if (testcase.runtime_status !== "OK") { + return false; + } + } + return true; +}; + +// const MyProblemContextMenu = ({ +// children, +// problem, +// }: { +// children: React.ReactNode; +// problem: ProblemPopulateTestcases | ProblemSecureModel | ProblemModel; +// }) => { +// const [openDeleteDialog, setOpenDeleteDialog] = useState(false); +// const navigate = useNavigate(); + +// return ( +// +// window.location.reload()} +// /> +// {children} +// +// navigate(`/my/problems/${problem.problem_id}`)}> +// +// Edit +// +// setOpenDeleteDialog(true)}> +// +// Delete +// +// +// +// ); +// }; + +const MyProblemMiniCard2 = ({ + problem, + disabled = false, + disabledHighlight = false, + onClick = () => {}, +}: { + problem: ProblemPopulateTestcases | ProblemSecureModel | ProblemModel; + disabled?: boolean; + disabledHighlight?: boolean; + onClick?: () => void; +}) => { + const navigate = useNavigate(); + + const [highlightTitle, setHighlightTitle] = useState(false); + const [toolVisible, setToolVisible] = useState(true); + + const handleMouseOver = () => { + setHighlightTitle(true); + setToolVisible(true); + }; + const handleMouseOut = () => { + setHighlightTitle(false); + setToolVisible(false); + }; + + const customCardCSS = (): string => { + let className = "p-2 cursor-pointer "; + + if (disabled) { + className += "opacity-50 "; + } else { + if (highlightTitle && !disabledHighlight) { + className += "border-green-500 bg-green-100 "; + } + } + return className; + }; + + return ( + problem && ( + + onMiddleClickOpenInNewTab(e,`/my/problems/${problem.problem_id}`)} + onClick={() => onClick()} + onMouseOver={handleMouseOver} + onMouseOut={handleMouseOut} + className={customCardCSS()} + + // className={`pt-6 px-5 ${disabled ? "opacity-50" : }`}`} + > +
+
+ +

{problem.title}

+
+ {/*
+ {collection.problems.length} +
*/} +
+
+
+ ) + ); +}; + +export default MyProblemMiniCard2; diff --git a/src/components/PublicProblemCard.tsx b/src/components/Cards/ProblemCards/PublicProblemCard.tsx similarity index 80% rename from src/components/PublicProblemCard.tsx rename to src/components/Cards/ProblemCards/PublicProblemCard.tsx index bb3e807..099e2b3 100644 --- a/src/components/PublicProblemCard.tsx +++ b/src/components/Cards/ProblemCards/PublicProblemCard.tsx @@ -1,12 +1,13 @@ import React, { useState } from "react"; -import { Card, CardContent, CardTitle } from "./shadcn/Card"; -import TestcasesGradingIndicator from "./TestcasesGradingIndicator"; -import { ProblemPopulateAccountAndSubmissionPopulateSubmissionTestcasesSecureModel } from "../types/models/Problem.model"; -import { readableDateFormat } from "../utilities/ReadableDateFormat"; -import { Button } from "./shadcn/Button"; -import { Label } from "./shadcn/Label"; +import { Card, CardContent, CardTitle } from "../../shadcn/Card"; +import TestcasesGradingIndicator from "../../TestcasesGradingIndicator"; +import { ProblemPopulateAccountAndSubmissionPopulateSubmissionTestcasesSecureModel } from "../../../types/models/Problem.model"; +import { readableDateFormat } from "../../../utilities/ReadableDateFormat"; +import { Button } from "../../shadcn/Button"; +import { Label } from "../../shadcn/Label"; import { useNavigate } from "react-router-dom"; import { FileSpreadsheet, Puzzle } from "lucide-react"; +import { onMiddleClickOpenInNewTab } from "../../../utilities/OnMiddleClickOpenInNewTab"; const PublicProblemCard = ({ problem, @@ -72,6 +73,7 @@ const PublicProblemCard = ({
+ +
+ + + ); +}; + +export default DeleteCollectionConfirmationDialog; diff --git a/src/components/Dialogs/DeleteCourseConfirmationDialog.tsx b/src/components/Dialogs/DeleteCourseConfirmationDialog.tsx new file mode 100644 index 0000000..e6eba75 --- /dev/null +++ b/src/components/Dialogs/DeleteCourseConfirmationDialog.tsx @@ -0,0 +1,61 @@ +import React, { useState } from "react"; +import { Dialog, DialogContent, DialogTitle } from "../shadcn/Dialog"; +import { Button } from "../shadcn/Button"; +import { ProblemService } from "../../services/Problem.service"; +import { + ProblemModel, + ProblemPopulateTestcases, + ProblemSecureModel, +} from "../../types/models/Problem.model"; +import { CollectionModel } from "../../types/models/Collection.model"; +import { CollectionService } from "../../services/Collection.service"; +import { TopicModel } from "../../types/models/Topic.model"; +import { TopicService } from "../../services/Topic.service"; + +const DeleteCourseConfirmationDialog = ({ + open, + setOpen, + course, + afterDelete = () => {}, +}: { + course: TopicModel; + open: boolean; + setOpen: React.Dispatch>; + afterDelete?: () => void; +}) => { + const accountId = String(localStorage.getItem("account_id")); + + const handleDelete = () => { + setOpen(false); + TopicService.delete(course.topic_id,accountId).then(() => + afterDelete() + ); + }; + + return ( + setOpen(false)}> + +
+
Are you sure you want to delete this course?
+
{course.name}
+
+
+ + +
+
+
+ ); +}; + +export default DeleteCourseConfirmationDialog; diff --git a/src/components/DeleteProblemConfirmationDialog.tsx b/src/components/Dialogs/DeleteProblemConfirmationDialog.tsx similarity index 83% rename from src/components/DeleteProblemConfirmationDialog.tsx rename to src/components/Dialogs/DeleteProblemConfirmationDialog.tsx index 6761b6b..deae215 100644 --- a/src/components/DeleteProblemConfirmationDialog.tsx +++ b/src/components/Dialogs/DeleteProblemConfirmationDialog.tsx @@ -1,8 +1,8 @@ import React, { useState } from "react"; -import { Dialog, DialogContent, DialogTitle } from "./shadcn/Dialog"; -import { Button } from "./shadcn/Button"; -import { ProblemService } from "../services/Problem.service"; -import { ProblemModel, ProblemPopulateTestcases, ProblemSecureModel } from "../types/models/Problem.model"; +import { Dialog, DialogContent, DialogTitle } from "../shadcn/Dialog"; +import { Button } from "../shadcn/Button"; +import { ProblemService } from "../../services/Problem.service"; +import { ProblemModel, ProblemPopulateTestcases, ProblemSecureModel } from "../../types/models/Problem.model"; const DeleteProblemConfirmationDialog = ({ open, diff --git a/src/components/Forms/CreateCollectionForm/ManageGroups.tsx b/src/components/Forms/CreateCollectionForm/ManageGroups.tsx new file mode 100644 index 0000000..702c96e --- /dev/null +++ b/src/components/Forms/CreateCollectionForm/ManageGroups.tsx @@ -0,0 +1,371 @@ +import React, { useEffect, useState } from "react"; +import { + CollectionGroupPermissionRequestForm, + CreateCollectionRequestForm, +} from "../../../types/forms/CreateCollectionRequestForm"; +import GroupAndPermissionManager, { + GroupAndPermissionManagerOnAddGroupsCallback, + GroupAndPermissionManagerOnRemoveGroupCallback, +} from "../GroupAndPermissionManager"; +import CollectionPermissionSwitchGroup from "../PermissionSwitchGroups/CollectionPermissionSwitchGroup"; +import { GroupModel } from "../../../types/models/Group.model"; +import { GroupService } from "../../../services/Group.service"; +import { Tabs, TabsList, TabsTrigger } from "../../shadcn/Tabs"; +import PermissionSwitchScrollArea from "../../Permissions/PermissionSwitchScrollArea"; +import MyProblemMiniCard2 from "../../Cards/ProblemCards/MyProblemMiniCard2"; +import { Switch } from "../../shadcn/Switch"; + +const ManageGroups = ({ + createRequest, + setCreateRequest, +}: { + createRequest: CreateCollectionRequestForm; + setCreateRequest: React.Dispatch< + React.SetStateAction + >; +}) => { + const accountId = String(localStorage.getItem("account_id")); + + const [groupPermission, setGroupPermission] = + useState(); + + const [selectedIndex, setSelectedIndex] = useState(-1); + const [currentGroupId, setCurrentGroupId] = useState(""); + + const handleAddGroups = ({ + addingGroups, + }: GroupAndPermissionManagerOnAddGroupsCallback) => { + const newGroupPermissions = addingGroups.map((group) => ({ + group_id: group.group_id, + group, + manageCollections: group.permission_manage_collections, + viewCollections: group.permission_view_collections, + })); + + setCreateRequest({ + ...createRequest, + groupPermissions: [ + ...createRequest.groupPermissions, + ...newGroupPermissions, + ], + }); + }; + + const handleRemoveGroup = ({ + index, + }: GroupAndPermissionManagerOnRemoveGroupCallback) => { + setCreateRequest({ + ...createRequest, + groupPermissions: createRequest.groupPermissions.filter( + (v, i) => i !== index + ), + }); + }; + + useEffect(() => { + if ( + selectedIndex >= 0 && + selectedIndex < createRequest.groupPermissions.length + ) { + setGroupPermission(createRequest.groupPermissions[selectedIndex]); + setCurrentGroupId( + createRequest.groupPermissions[selectedIndex].group_id + ); + } + }, [selectedIndex]); + + useEffect(() => { + if (groupPermission) { + setCreateRequest({ + ...createRequest, + groupPermissions: [ + ...createRequest.groupPermissions.slice(0, selectedIndex), + groupPermission, + ...createRequest.groupPermissions.slice(selectedIndex + 1), + ], + }); + } + }, [groupPermission]); + + const [tabValue, setTabValue] = useState("collection"); + const [allGroups, setAllGroups] = useState([]); + + useEffect(() => { + GroupService.getAllAsCreator(accountId).then((response) => { + setAllGroups(response.data.groups); + }); + }, [accountId]); + + return ( + <> +
+ setTabValue(e)}> + + + Collection Permissions + + + Problem Permissions + + + +
+ handleAddGroups(e)} + onRemoveGroup={(e) => handleRemoveGroup(e)} + selectedIndex={selectedIndex} + setSelectedIndex={setSelectedIndex} + > + <> + {tabValue === "collection" && ( + + {groupPermission && selectedIndex >= 0 && ( + { + setGroupPermission({ + ...groupPermission, + manageCollections: + !groupPermission.manageCollections, + }); + }} + onClickViewCollections={() => { + setGroupPermission({ + ...groupPermission, + viewCollections: + !groupPermission.viewCollections, + }); + }} + /> + )} + + )} + + {tabValue === "problems" && + groupPermission && + selectedIndex >= 0 && ( +
+ {createRequest.problemsInterface.map( + (collectionProblem) => ( +
+
+ +
+ +
+ View Problem + + gp.group + .group_id === + currentGroupId + )?.viewProblems || + false + } + onClick={() => { + const findGroup = + collectionProblem.groupPermissions?.find( + (gp) => + gp.group + .group_id === + currentGroupId + ); + + const newGroupPermissions = + collectionProblem.groupPermissions.map( + (gp) => { + if ( + gp.group + .group_id === + currentGroupId + ) { + return { + ...gp, + viewProblems: + !gp.viewProblems, + }; + } else { + return gp; + } + } + ); + + if ( + !findGroup || + newGroupPermissions.length === + 0 + ) { + newGroupPermissions.push( + { + groupId: + currentGroupId, + group: allGroups.find( + (g) => + g.group_id === + currentGroupId + )!, + viewProblems: + true, + manageProblems: + false, + } + ); + } + + if ( + !createRequest.collection + ) { + return; + } + + setCreateRequest({ + ...createRequest, + problemsInterface: + createRequest.problemsInterface.map( + (cp) => { + if ( + cp + .problem + .problem_id === + collectionProblem + .problem + .problem_id + ) { + return { + ...cp, + groupPermissions: + newGroupPermissions, + }; + } else { + return cp; + } + } + ), + }); + }} + className="ml-2" + /> +
+
+ Manage Problem + + gp.group + .group_id === + currentGroupId + )?.manageProblems || + false + } + onClick={() => { + const findGroup = + collectionProblem.groupPermissions?.find( + (gp) => + gp.group + .group_id === + currentGroupId + ); + + const newGroupPermissions = + collectionProblem.groupPermissions.map( + (gp) => { + if ( + gp.group + .group_id === + currentGroupId + ) { + return { + ...gp, + manageProblems: + !gp.manageProblems, + }; + } else { + return gp; + } + } + ); + + if ( + !findGroup || + newGroupPermissions.length === + 0 + ) { + newGroupPermissions.push( + { + groupId: + currentGroupId, + group: allGroups.find( + (g) => + g.group_id === + currentGroupId + )!, + viewProblems: + false, + manageProblems: + true, + } + ); + } + + if ( + !createRequest.collection + ) { + return; + } + + setCreateRequest({ + ...createRequest, + problemsInterface: + createRequest.problemsInterface.map( + (cp) => { + if ( + cp + .problem + .problem_id === + collectionProblem + .problem + .problem_id + ) { + return { + ...cp, + groupPermissions: + newGroupPermissions, + }; + } else { + return cp; + } + } + ), + }); + }} + className="ml-2" + /> +
+
+ ) + )} +
+ )} + +
+ + ); +}; + +export default ManageGroups; diff --git a/src/components/Forms/CreateCollectionForm/ManageProblems.tsx b/src/components/Forms/CreateCollectionForm/ManageProblems.tsx index 58a521a..7db43bd 100644 --- a/src/components/Forms/CreateCollectionForm/ManageProblems.tsx +++ b/src/components/Forms/CreateCollectionForm/ManageProblems.tsx @@ -1,24 +1,18 @@ import React, { useEffect, useState } from "react"; -import { CreateCollectionRequestForm } from "../../../types/forms/CreateCollectionRequestForm"; import { ReactSortable } from "react-sortablejs"; -import { Button } from "../../shadcn/Button"; -import AddProblemDialog from "../../AddProblemDialog"; -import { Separator } from "../../shadcn/Seperator"; -import { Input } from "../../shadcn/Input"; import { ProblemService } from "../../../services/Problem.service"; +import { transformProblemModel2ProblemHashedTable } from "../../../types/adapters/Problem.adapter"; import { - ProblemHashedTable, - ProblemModel, - ProblemSecureModel, -} from "../../../types/models/Problem.model"; -import { ItemInterface } from "./../../../../node_modules/react-sortablejs/dist/index.d"; -import MyProblemCard from "../../MyProblemCard"; -import CardContainer from "../../CardContainer"; -import SortableCardContainer from "../../SortableCardContainer"; -import MyProblemMiniCard from "../../MyProblemMiniCard"; + CreateCollectionRequestForm, + ProblemItemInterface, +} from "../../../types/forms/CreateCollectionRequestForm"; +import { ProblemHashedTable } from "../../../types/models/Problem.model"; +import MyProblemMiniCard2 from "../../Cards/ProblemCards/MyProblemMiniCard2"; +import { Button } from "../../shadcn/Button"; +import { Input } from "../../shadcn/Input"; import { ScrollArea } from "../../shadcn/ScrollArea"; -import { Item } from "@radix-ui/react-context-menu"; -import { transformProblemModel2ProblemHashedTable } from "../../../types/adapters/Problem.adapter"; +import { Separator } from "../../shadcn/Seperator"; +import { ItemInterface } from "./../../../../node_modules/react-sortablejs/dist/index.d"; const ManageProblems = ({ createRequest, @@ -29,48 +23,47 @@ const ManageProblems = ({ React.SetStateAction >; }) => { - - const accountId = Number(localStorage.getItem("account_id")); + const accountId = String(localStorage.getItem("account_id")); const [allProblemsSortable, setAllProblemsSortable] = useState< - ItemInterface[] + ProblemItemInterface[] >([]); const [selectedProblemsSortable, setSelectedProblemsSortable] = useState< - ItemInterface[] + ProblemItemInterface[] >([]); - const [allProblems, setAllProblems] = useState< - ProblemHashedTable - >({}); + const [allProblems, setAllProblems] = useState({}); const [initial, setInitial] = useState(true); - const [selectedProblemSortableIds, setSelectedProblemSortableIds] = useState([]); + const [selectedProblemSortableIds, setSelectedProblemSortableIds] = + useState([]); useEffect(() => { - setSelectedProblemSortableIds(selectedProblemsSortable.map((item) => item.id as number)); - },[selectedProblemsSortable]) - - const handleRemoveSelectedProblem = (id: number) => { - setSelectedProblemsSortable( - [...selectedProblemsSortable.filter((item) => item.id !== id)] + setSelectedProblemSortableIds( + selectedProblemsSortable.map((item) => item.id as string) ); - } + }, [selectedProblemsSortable]); + + const handleRemoveSelectedProblem = (id: string) => { + setSelectedProblemsSortable([ + ...selectedProblemsSortable.filter((item) => item.id !== id), + ]); + }; - const handleQuickToggleSelectedProblem = (item: ItemInterface) => { + const handleQuickToggleSelectedProblem = (item: ProblemItemInterface) => { // if (selectedProblemsSortable.find((item1) => item1.id === item.id)) { // console.log("Remove"); - // handleRemoveSelectedProblem(item.id as number); + // handleRemoveSelectedProblem(item.id as string); // } else { // console.log("Add"); // setSelectedProblemsSortable([...selectedProblemsSortable, item]); // } - if (selectedProblemSortableIds.includes(item.id as number)) { - handleRemoveSelectedProblem(item.id as number); - } - else { + if (selectedProblemSortableIds.includes(item.id as string)) { + handleRemoveSelectedProblem(item.id as string); + } else { setSelectedProblemsSortable([...selectedProblemsSortable, item]); } - } + }; useEffect(() => { setCreateRequest({ @@ -80,54 +73,79 @@ const ManageProblems = ({ }, [selectedProblemsSortable]); useEffect(() => { - ProblemService.getAllByAccount(accountId).then((response) => { - setAllProblems(transformProblemModel2ProblemHashedTable(response.data.problems)); + ProblemService.getAllAsCreator(accountId).then((response) => { + setAllProblems( + transformProblemModel2ProblemHashedTable(response.data.problems) + ); setAllProblemsSortable( response.data.problems.map((problem) => ({ - id: problem?.problem_id, - name: problem?.title + id: problem.problem_id, + name: problem.title, + problem: problem, + groupPermissions: [], })) ); }); }, [accountId]); + useEffect(() => { + if (createRequest.collection) { + setAllProblems({ + ...allProblems, + ...transformProblemModel2ProblemHashedTable( + createRequest.collection.problems.map((cp) => cp.problem) + ), + }); + } + }, [createRequest.collection, allProblems]); + useEffect(() => { if (initial) { - setSelectedProblemsSortable(createRequest.problemsInterface) + console.log("AAA",createRequest) + // setSelectedProblemsSortable(createRequest.problemsInterface); + setSelectedProblemsSortable( + createRequest.collection?.problems.map((cp) => ({ + id: cp.problem.problem_id, + name: cp.problem.title, + problem: cp.problem, + groupPermissions: cp.problem.group_permissions.map((gc) => ({ + groupId: gc.group.group_id, + group: gc.group, + manageProblems: gc.permission_manage_problems, + viewProblems: gc.permission_view_problems, + })), + })) ?? ([] as ProblemItemInterface[]) + ); setInitial(false); } - console.log("Create Request", createRequest); - },[createRequest]) + // console.log("Create Request", createRequest); + }, [createRequest]); return (
-
-

Manage Problems

- - -
-
- + {selectedProblemsSortable?.map((item) => ( - handleRemoveSelectedProblem(item.id as number)} - key={item.id} - problem={ - allProblems[item.id as number] + onClick={() => + handleRemoveSelectedProblem( + item.id as string + ) } + key={item.id} + problem={item.problem} /> ))} @@ -142,7 +160,7 @@ const ManageProblems = ({
- + {allProblemsSortable?.map((item) => ( -
- handleQuickToggleSelectedProblem(item)} - disabled={selectedProblemSortableIds.includes(item.id as number)} - key={item.id} - problem={ - allProblems[item.id as number] +
+ + handleQuickToggleSelectedProblem( + item + ) } + disabled={selectedProblemSortableIds.includes( + item.id as string + )} + key={item.id} + problem={item.problem} />
))} diff --git a/src/components/Forms/CreateCollectionForm/index.tsx b/src/components/Forms/CreateCollectionForm/index.tsx index acdb164..c598d7c 100644 --- a/src/components/Forms/CreateCollectionForm/index.tsx +++ b/src/components/Forms/CreateCollectionForm/index.tsx @@ -1,6 +1,6 @@ import { ELEMENT_PARAGRAPH } from "@udecode/plate-paragraph"; import React, { useEffect, useState } from "react"; -import { useNavigate } from "react-router-dom"; +import { useNavigate, useSearchParams } from "react-router-dom"; import { PlateEditorValueType } from "../../../types/PlateEditorValueType"; import { ProblemService } from "../../../services/Problem.service"; import { toast } from "../../shadcn/UseToast"; @@ -16,6 +16,7 @@ import { CreateCollectionRequestForm } from "../../../types/forms/CreateCollecti import ManageProblem from "./ManageProblems"; import ManageProblems from "./ManageProblems"; import FormSaveButton from "../FormSaveButton"; +import ManageGroups from "./ManageGroups"; const TabList = [ { @@ -26,48 +27,52 @@ const TabList = [ value: "problems", label: "Manage Problems", }, -] + { + value: "groups", + label: "Manage Groups & Permissions", + }, +]; export type OnCollectionSavedCallback = { - setLoading?: React.Dispatch> - collectionId?: number - setCollectionId?: React.Dispatch> - createRequest?: CreateCollectionRequestForm -} + setLoading?: React.Dispatch>; + createRequest?: CreateCollectionRequestForm; +}; const CreateCollectionForm = ({ createRequestInitialValue, onCollectionSave, }: { - createRequestInitialValue: CreateCollectionRequestForm - onCollectionSave: (callback: OnCollectionSavedCallback) => void + createRequestInitialValue: CreateCollectionRequestForm; + onCollectionSave: (callback: OnCollectionSavedCallback) => void; }) => { + const navigate = useNavigate(); + const [currentForm, setCurrentForm] = useSearchParams(); - const navigate = useNavigate(); - const [currentForm, setCurrentForm] = useState("general"); - const [loading, setLoading] = useState(false); + useEffect(() => { + if (!currentForm.get("section")) { + setCurrentForm({ section: "general" }); + } + }, [currentForm]) - const [collectionId, setCollectionId] = useState(-1); - - const [createRequest, setCreateRequest] = useState(createRequestInitialValue) + const [loading, setLoading] = useState(false); + const [createRequest, setCreateRequest] = + useState(createRequestInitialValue); const handleSave = () => { onCollectionSave({ setLoading, createRequest, - collectionId, - setCollectionId, - }) - } + }); + }; - return ( -
+ return ( +
-

+

navigate("/my/collections")} + onClick={() => navigate(-1)} /> {createRequest.title === "" ? "Create Problem" @@ -75,14 +80,14 @@ const CreateCollectionForm = ({

- + {TabList.map((tab, index) => ( - setCurrentForm(tab.value) + setCurrentForm({section: tab.value}) } > {tab.label} @@ -99,21 +104,28 @@ const CreateCollectionForm = ({
- {currentForm === "general" && ( + {currentForm.get("section") === "general" && ( )} - {currentForm === "problems" && ( + {currentForm.get("section") === "problems" && ( )} + + {currentForm.get("section") === "groups" && ( + + )}
- ) -} + ); +}; -export default CreateCollectionForm \ No newline at end of file +export default CreateCollectionForm; diff --git a/src/components/Forms/CreateCourseForm/ManageCollections.tsx b/src/components/Forms/CreateCourseForm/ManageCollections.tsx index c046a93..8a58294 100644 --- a/src/components/Forms/CreateCourseForm/ManageCollections.tsx +++ b/src/components/Forms/CreateCourseForm/ManageCollections.tsx @@ -1,29 +1,19 @@ import React, { useEffect, useState } from "react"; -import { CreateCollectionRequestForm } from "../../../types/forms/CreateCollectionRequestForm"; import { ReactSortable } from "react-sortablejs"; -import { Button } from "../../shadcn/Button"; -import AddProblemDialog from "../../AddProblemDialog"; -import { Separator } from "../../shadcn/Seperator"; -import { Input } from "../../shadcn/Input"; -import { ProblemService } from "../../../services/Problem.service"; +import { CollectionService } from "../../../services/Collection.service"; +import { transformCollectionPopulateProblemSecureModel2CollectionHashedTable } from "../../../types/adapters/Collection.adapter"; +import { + CollectionItemInterface, + CreateCourseRequestForm, +} from "../../../types/forms/CreateCourseRequestForm"; import { - ProblemHashedTable, - ProblemModel, - ProblemSecureModel, -} from "../../../types/models/Problem.model"; -import { ItemInterface } from "./../../../../node_modules/react-sortablejs/dist/index.d"; -import MyProblemCard from "../../MyProblemCard"; -import CardContainer from "../../CardContainer"; -import SortableCardContainer from "../../SortableCardContainer"; -import MyProblemMiniCard from "../../MyProblemMiniCard"; + CollectionHashedTable, + CollectionPopulateCollectionProblemPopulateProblemModel, +} from "../../../types/models/Collection.model"; +import MyCollectionMiniCard2 from "../../Cards/CollectionCards/MyCollectionMiniCard2"; +import { Input } from "../../shadcn/Input"; import { ScrollArea } from "../../shadcn/ScrollArea"; -import { Item } from "@radix-ui/react-context-menu"; -import { transformProblemModel2ProblemHashedTable } from "../../../types/adapters/Problem.adapter"; -import { CreateCourseRequestForm } from "../../../types/forms/CreateCourseRequestForm"; -import MyCollectionMiniCard from "../../MyCollectionMiniCard"; -import { CollectionService } from "../../../services/Collection.service"; -import { transformCollectionModel2CollectionHashedTable } from "../../../types/adapters/Collection.adapter"; -import { CollectionHashedTable, CollectionPopulateProblemSecureModel } from "../../../types/models/Collection.model"; +import { Separator } from "../../shadcn/Seperator"; const ManageCollections = ({ createRequest, @@ -34,48 +24,55 @@ const ManageCollections = ({ React.SetStateAction >; }) => { - - const accountId = Number(localStorage.getItem("account_id")); + const accountId = String(localStorage.getItem("account_id")); const [allCollectionsSortable, setAllCollectionsSortable] = useState< - ItemInterface[] + CollectionItemInterface[] >([]); - const [selectedCollectionsSortable, setSelectedCollectionsSortable] = useState< - ItemInterface[] - >([]); - const [allCollections, setAllCollections] = useState< - CollectionHashedTable - >({}); + const [selectedCollectionsSortable, setSelectedCollectionsSortable] = + useState([]); + + const [allCollections, setAllCollections] = useState( + {} + ); const [initial, setInitial] = useState(true); - const [selectedCollectionsSortableIds, setSelectedCollectionsSortableIds] = useState([]); + const [tabValue, setTabValue] = useState("add"); + const [selectedCollectionsSortableIds, setSelectedCollectionsSortableIds] = + useState([]); useEffect(() => { - setSelectedCollectionsSortableIds(selectedCollectionsSortable.map((item) => item.id as number)); - },[selectedCollectionsSortable]) - - const handleRemoveSelectedCollection = (id: number) => { - setSelectedCollectionsSortable( - [...selectedCollectionsSortable.filter((item) => item.id !== id)] + setSelectedCollectionsSortableIds( + selectedCollectionsSortable.map((item) => item.id as string) ); - } + }, [selectedCollectionsSortable]); + + const handleRemoveSelectedCollection = (id: string) => { + setSelectedCollectionsSortable([ + ...selectedCollectionsSortable.filter((item) => item.id !== id), + ]); + }; - const handleQuickToggleSelectedCollection = (item: ItemInterface) => { + const handleQuickToggleSelectedCollection = ( + item: CollectionItemInterface + ) => { // if (selectedCollectionsSortable.find((item1) => item1.id === item.id)) { // console.log("Remove"); - // handleRemoveSelectedCollection(item.id as number); + // handleRemoveSelectedCollection(item.id as string); // } else { // console.log("Add"); // setSelectedCollectionsSortable([...selectedCollectionsSortable, item]); // } - if (selectedCollectionsSortableIds.includes(item.id as number)) { - handleRemoveSelectedCollection(item.id as number); + if (selectedCollectionsSortableIds.includes(item.id as string)) { + handleRemoveSelectedCollection(item.id as string); + } else { + setSelectedCollectionsSortable([ + ...selectedCollectionsSortable, + item, + ]); } - else { - setSelectedCollectionsSortable([...selectedCollectionsSortable, item]); - } - } + }; useEffect(() => { setCreateRequest({ @@ -85,64 +82,89 @@ const ManageCollections = ({ }, [selectedCollectionsSortable]); useEffect(() => { - // ProblemService.getAllByAccount(accountId).then((response) => { - // setAllCollections(transformProblemModel2ProblemHashedTable(response.data.problems)); - // setAllCollectionsSortable( - // response.data.problems.map((problem) => ({ - // id: problem?.problem_id, - // name: problem?.title - // })) - // ); - // }); - - CollectionService.getAllByAccount(accountId).then((response) => { - setAllCollections(transformCollectionModel2CollectionHashedTable(response.data.collections)); + CollectionService.getAllAsCreator(accountId).then((response) => { + setAllCollections( + transformCollectionPopulateProblemSecureModel2CollectionHashedTable( + response.data.collections + ) + ); + setAllCollectionsSortable( response.data.collections.map((collection) => ({ id: collection.collection_id, - name: collection.name + name: collection.name, + collection: collection, + groupPermissions: [], })) ); - }) + }); }, [accountId]); + useEffect(() => { + if (createRequest.course) { + setAllCollections({ + ...allCollections, + ...transformCollectionPopulateProblemSecureModel2CollectionHashedTable( + createRequest.course.collections.map( + (cc) => + cc.collection as CollectionPopulateCollectionProblemPopulateProblemModel + ) + ), + }); + } + }, [createRequest.course, allCollections]); + useEffect(() => { if (initial) { - setSelectedCollectionsSortable(createRequest.collectionsInterface) + setSelectedCollectionsSortable( + createRequest.course?.collections.map((cc) => ({ + id: cc.collection.collection_id, + name: cc.collection.name, + collection: cc.collection, + groupPermissions: cc.collection.group_permissions.map( + (gc) => ({ + group_id: gc.group.group_id, + group: gc.group, + manageCollections: gc.permission_manage_collections, + viewCollections: gc.permission_view_collections, + }) + ), + })) ?? ([] as CollectionItemInterface[]) + ); setInitial(false); } console.log("Create Request", createRequest); - },[createRequest]) + }, [createRequest]); return (
-
-

Manage Collections

- - -
-
- + - {selectedCollectionsSortable?.map((item) => ( - handleRemoveSelectedCollection(item.id as number)} - key={item.id} - collection={allCollections[item.id as number] as CollectionPopulateProblemSecureModel} - /> - ))} + {selectedCollectionsSortable?.map( + (item) => ( + + handleRemoveSelectedCollection( + item.id as string + ) + } + key={item.id} + collection={item.collection} + /> + ) + )}
@@ -155,7 +177,7 @@ const ManageCollections = ({
- + {allCollectionsSortable?.map((item) => ( -
- handleQuickToggleSelectedCollection(item)} - disabled={selectedCollectionsSortableIds.includes(item.id as number)} +
+ + handleQuickToggleSelectedCollection( + item + ) + } + disabled={selectedCollectionsSortableIds.includes( + item.id as string + )} key={item.id} - collection={allCollections[item.id as number] as CollectionPopulateProblemSecureModel} + collection={item.collection} />
))} diff --git a/src/components/Forms/CreateCourseForm/ManageGroups.tsx b/src/components/Forms/CreateCourseForm/ManageGroups.tsx new file mode 100644 index 0000000..6b4c621 --- /dev/null +++ b/src/components/Forms/CreateCourseForm/ManageGroups.tsx @@ -0,0 +1,384 @@ +import React, { useEffect, useState } from "react"; +import { GroupService } from "../../../services/Group.service"; +import { + CourseGroupPermissionRequestForm, + CreateCourseRequestForm, +} from "../../../types/forms/CreateCourseRequestForm"; +import { CollectionPopulateCollectionProblemPopulateProblemModel } from "../../../types/models/Collection.model"; +import { GroupModel } from "../../../types/models/Group.model"; +import MyCollectionMiniCard2 from "../../Cards/CollectionCards/MyCollectionMiniCard2"; +import PermissionSwitchScrollArea from "../../Permissions/PermissionSwitchScrollArea"; +import { Switch } from "../../shadcn/Switch"; +import { Tabs, TabsList, TabsTrigger } from "../../shadcn/Tabs"; +import GroupAndPermissionManager, { + GroupAndPermissionManagerOnAddGroupsCallback, + GroupAndPermissionManagerOnRemoveGroupCallback, +} from "../GroupAndPermissionManager"; +import CoursePermissionSwitchGroup from "../PermissionSwitchGroups/CoursePermissionSwitchGroup"; + +const ManageGroups = ({ + createRequest, + setCreateRequest, +}: { + createRequest: CreateCourseRequestForm; + setCreateRequest: React.Dispatch< + React.SetStateAction + >; +}) => { + const accountId = String(localStorage.getItem("account_id")); + + const [groupPermission, setGroupPermission] = + useState(); + + const [selectedIndex, setselectedIndex] = useState(-1); + const [currentGroupId, setCurrentGroupId] = useState(""); + + const handleAddGroups = ({ + addingGroups, + }: GroupAndPermissionManagerOnAddGroupsCallback) => { + const newGroupPermissions = addingGroups.map((group) => ({ + group_id: group.group_id, + group, + manageCourses: group.permission_manage_topics, + viewCourseLogs: group.permission_view_topics_log, + viewCourses: group.permission_view_topics, + })); + + setCreateRequest({ + ...createRequest, + groupPermissions: [ + ...createRequest.groupPermissions, + ...newGroupPermissions, + ], + }); + }; + + const handleRemoveGroup = ({ + index, + }: GroupAndPermissionManagerOnRemoveGroupCallback) => { + if (index === selectedIndex) { + setselectedIndex(-1); + } + setCreateRequest({ + ...createRequest, + groupPermissions: createRequest.groupPermissions.filter( + (v, i) => i !== index + ), + }); + }; + + useEffect(() => { + if ( + selectedIndex >= 0 && + selectedIndex < createRequest.groupPermissions.length + ) { + setGroupPermission(createRequest.groupPermissions[selectedIndex]); + setCurrentGroupId( + createRequest.groupPermissions[selectedIndex].group_id + ); + } + }, [selectedIndex]); + + useEffect(() => { + if (groupPermission) { + setCreateRequest({ + ...createRequest, + groupPermissions: [ + ...createRequest.groupPermissions.slice(0, selectedIndex), + groupPermission, + ...createRequest.groupPermissions.slice(selectedIndex + 1), + ], + }); + } + }, [groupPermission]); + + const [tabValue, setTabValue] = useState("course"); + const [allGroups, setAllGroups] = useState([]); + + useEffect(() => { + GroupService.getAllAsCreator(accountId).then((response) => { + setAllGroups(response.data.groups); + }); + }, [accountId]); + + return ( + <> +
+ setTabValue(e)}> + + + Course Permissions + + + Collection Permissions + + + +
+ handleAddGroups(e)} + onRemoveGroup={(e) => handleRemoveGroup(e)} + selectedIndex={selectedIndex} + setSelectedIndex={setselectedIndex} + > + <> + {tabValue === "course" && ( + + {groupPermission && selectedIndex >= 0 && ( + + setGroupPermission({ + ...groupPermission, + manageCourses: + !groupPermission.manageCourses, + }) + } + onClickViewCourseLogs={() => + setGroupPermission({ + ...groupPermission, + viewCourseLogs: + !groupPermission.viewCourseLogs, + }) + } + onClickViewCourses={() => + setGroupPermission({ + ...groupPermission, + viewCourses: + !groupPermission.viewCourses, + }) + } + /> + )} + + )} + + {tabValue === "collections" && + groupPermission && + selectedIndex >= 0 && ( +
+ {createRequest.collectionsInterface?.map( + (courseCollection) => ( +
+
+ +
+ +
+ View Collection + + gp.group + .group_id === + currentGroupId + )?.viewCollections || false + } + onClick={() => { + const findGroup = + courseCollection.groupPermissions?.find( + (gp) => + gp.group + .group_id === + currentGroupId + ); + + const newGroupPermissions = + courseCollection.groupPermissions.map( + (gp) => { + if ( + gp.group + .group_id === + currentGroupId + ) { + return { + ...gp, + viewCollections: + !gp.viewCollections, + }; + } else { + return gp; + } + } + ); + + if ( + !findGroup || + newGroupPermissions.length === + 0 + ) { + newGroupPermissions.push( + { + group_id: + currentGroupId, + group: allGroups.find( + (g) => + g.group_id === + currentGroupId + )!, + viewCollections: + true, + manageCollections: + false, + } + ); + } + + if ( + !createRequest.course + ) { + return; + } + + setCreateRequest({ + ...createRequest, + collectionsInterface: + createRequest.collectionsInterface.map( + (cc) => { + if ( + cc + .collection + .collection_id === + courseCollection + .collection + .collection_id + ) { + return { + ...cc, + groupPermissions: + newGroupPermissions, + }; + } else { + return cc; + } + } + ), + }); + }} + className="ml-2" + /> +
+
+ Manage Collection + + gp.group + .group_id === + currentGroupId + )?.manageCollections || false + } + onClick={() => { + const findGroup = + courseCollection.groupPermissions?.find( + (gp) => + gp.group + .group_id === + currentGroupId + ); + + const newGroupPermissions = + courseCollection.groupPermissions.map( + (gp) => { + if ( + gp.group + .group_id === + currentGroupId + ) { + return { + ...gp, + manageCollections: + !gp.manageCollections, + }; + } else { + return gp; + } + } + ); + + if ( + !findGroup || + newGroupPermissions.length === + 0 + ) { + newGroupPermissions.push( + { + group_id: + currentGroupId, + group: allGroups.find( + (g) => + g.group_id === + currentGroupId + )!, + viewCollections: + false, + manageCollections: + true, + } + ); + } + + if ( + !createRequest.course + ) { + return; + } + + setCreateRequest({ + ...createRequest, + collectionsInterface: + createRequest.collectionsInterface.map( + (cc) => { + if ( + cc + .collection + .collection_id === + courseCollection + .collection + .collection_id + ) { + return { + ...cc, + groupPermissions: + newGroupPermissions, + }; + } else { + return cc; + } + } + ), + }); + }} + className="ml-2" + /> +
+
+ ) + )} +
+ )} + +
+ + ); +}; + +export default ManageGroups; diff --git a/src/components/Forms/CreateCourseForm/index.tsx b/src/components/Forms/CreateCourseForm/index.tsx index 634bc78..e59e159 100644 --- a/src/components/Forms/CreateCourseForm/index.tsx +++ b/src/components/Forms/CreateCourseForm/index.tsx @@ -1,21 +1,12 @@ -import React, { useState } from "react"; -import { useNavigate } from "react-router-dom"; +import { ArrowLeft } from "lucide-react"; +import React, { useEffect, useState } from "react"; +import { useNavigate, useSearchParams } from "react-router-dom"; import { CreateCourseRequestForm } from "../../../types/forms/CreateCourseRequestForm"; -import { ELEMENT_PARAGRAPH } from "@udecode/plate-paragraph"; -import { PlateEditorValueType } from "../../../types/PlateEditorValueType"; -import { ProblemService } from "../../../services/Problem.service"; -import { toast } from "../../shadcn/UseToast"; -import NavbarSidebarLayout from "../../../layout/NavbarSidebarLayout"; -import { ArrowLeft, ChevronLeftIcon, Loader2 } from "lucide-react"; import { Tabs, TabsList, TabsTrigger } from "../../shadcn/Tabs"; -import { CreateProblemRequest } from "../../../types/apis/Problem.api"; -import { Button } from "../../shadcn/Button"; -import { CreateProblemRequestForm } from "../../../types/forms/CreateProblemRequestForm"; -import { TestcaseModel } from "../../../types/models/Problem.model"; -import { CreateCollectionRequestForm } from "../../../types/forms/CreateCollectionRequestForm"; +import FormSaveButton from "../FormSaveButton"; import GeneralDetail from "./GeneralDetail"; import ManageCollections from "./ManageCollections"; -import FormSaveButton from "../FormSaveButton"; +import ManageGroups from "./ManageGroups"; const TabList = [ { @@ -26,14 +17,18 @@ const TabList = [ value: "collections", label: "Manage Collections", }, + { + value: "groups", + label: "Manage Groups & Permissions", + }, ]; export type OnCourseSavedCallback = { setLoading: React.Dispatch>; - courseId: number; - setCourseId: React.Dispatch>; + // courseId: string; + // setCourseId: React.Dispatch>; createRequest: CreateCourseRequestForm; -} +}; const CreateCourseForm = ({ createRequestInitialValue, @@ -44,8 +39,18 @@ const CreateCourseForm = ({ onCourseSave: (callback: OnCourseSavedCallback) => void; // onCollectionSave: (callback: OnCollectionSavedCallback) => void; }) => { + const accountId = String(localStorage.getItem("account_id")); + + const [currentForm, setCurrentForm] = useSearchParams(); + + useEffect(() => { + if (!currentForm.get("section")) { + setCurrentForm({ section: "general" }); + } + }, [currentForm]) + const navigate = useNavigate(); - const [currentForm, setCurrentForm] = useState("general"); + // const [currentForm, setCurrentForm] = useState(searchParams.get("section")); const [loading, setLoading] = useState(false); const [courseId, setCourseId] = useState(-1); @@ -58,8 +63,8 @@ const CreateCourseForm = ({ onCourseSave({ setLoading, createRequest, - courseId, - setCourseId, + // courseId, + // setCourseId, }); }; @@ -70,7 +75,7 @@ const CreateCourseForm = ({ navigate("/my/courses")} + onClick={() => navigate(-1)} /> {createRequest.title === "" ? "Create Course" @@ -78,14 +83,19 @@ const CreateCourseForm = ({
- + {TabList.map((tab, index) => ( - setCurrentForm(tab.value) + setCurrentForm({section: tab.value}) } > {tab.label} @@ -102,18 +112,25 @@ const CreateCourseForm = ({
- {currentForm === "general" && ( + {currentForm.get("section") === "general" && ( )} - {currentForm === "collections" && ( + {currentForm.get("section") === "collections" && ( )} + + {currentForm.get("section") === "groups" && ( + + )}
); diff --git a/src/components/Forms/CreateGroupForm/ManageMembers.tsx b/src/components/Forms/CreateGroupForm/ManageMembers.tsx index bdaae0e..9e39d48 100644 --- a/src/components/Forms/CreateGroupForm/ManageMembers.tsx +++ b/src/components/Forms/CreateGroupForm/ManageMembers.tsx @@ -2,7 +2,6 @@ import React, { useEffect, useState } from "react"; import { CreateCollectionRequestForm } from "../../../types/forms/CreateCollectionRequestForm"; import { ReactSortable } from "react-sortablejs"; import { Button } from "../../shadcn/Button"; -import AddProblemDialog from "../../AddProblemDialog"; import { Separator } from "../../shadcn/Seperator"; import { Input } from "../../shadcn/Input"; import { ProblemService } from "../../../services/Problem.service"; @@ -12,28 +11,29 @@ import { ProblemSecureModel, } from "../../../types/models/Problem.model"; import { ItemInterface } from "./../../../../node_modules/react-sortablejs/dist/index.d"; -import MyProblemCard from "../../MyProblemCard"; +import MyProblemCard from "../../Cards/ProblemCards/MyProblemCard"; import CardContainer from "../../CardContainer"; import SortableCardContainer from "../../SortableCardContainer"; -import MyProblemMiniCard from "../../MyProblemMiniCard"; +import MyProblemMiniCard from "../../Cards/ProblemCards/MyProblemMiniCard"; import { ScrollArea } from "../../shadcn/ScrollArea"; import { Item } from "@radix-ui/react-context-menu"; import { transformProblemModel2ProblemHashedTable } from "../../../types/adapters/Problem.adapter"; import { CreateGroupRequestForm } from "../../../types/forms/CreateGroupRequestForm"; -import MyCollectionMiniCard from "../../MyCollectionMiniCard"; +import MyCollectionMiniCard from "../../Cards/CollectionCards/MyCollectionMiniCard"; import { CollectionService } from "../../../services/Collection.service"; -import { transformCollectionModel2CollectionHashedTable } from "../../../types/adapters/Collection.adapter"; +import { transformCollectionPopulateProblemSecureModel2CollectionHashedTable } from "../../../types/adapters/Collection.adapter"; import { CollectionHashedTable, CollectionPopulateProblemSecureModel, } from "../../../types/models/Collection.model"; -import AccountCheckboxCard from "../../AccountCheckboxCard"; +import AccountCheckboxCard from "../../Cards/AccountCards/AccountCheckboxCard"; import { AccountHashedTable, AccountSecureModel } from "../../../types/models/Account.model"; import { GroupHashedTable } from "../../../types/models/Group.model"; import { GroupService } from "../../../services/Group.service"; import { AccountService } from "../../../services/Account.service"; import { transformAccountModels2AccountHashedTable } from "../../../types/adapters/Account.adapter"; -import AccountMiniCard from "../../AccountMiniCard"; +import AccountMiniCard from "../../Cards/AccountCards/AccountMiniCard"; +import AccountMiniCard2 from "../../Cards/AccountCards/AccountMiniCard2"; const ManageMembers = ({ createRequest, @@ -44,7 +44,7 @@ const ManageMembers = ({ React.SetStateAction >; }) => { - const accountId = Number(localStorage.getItem("account_id")); + const accountId = String(localStorage.getItem("account_id")); const [allAccountsSortable, setAllAccountsSortable] = useState< ItemInterface[] @@ -56,23 +56,23 @@ const ManageMembers = ({ const [initial, setInitial] = useState(true); const [selectedAccountsSortableIds, setSelectedAccountsSortableIds] = - useState([]); + useState([]); useEffect(() => { setSelectedAccountsSortableIds( - selectedAccountsSortable?.map((item) => item.id as number) + selectedAccountsSortable?.map((item) => item.id as string) ); }, [selectedAccountsSortable]); - const handleRemoveSelectedCollection = (id: number) => { + const handleRemoveSelectedCollection = (id: string) => { setSelectedAccountsSortable([ ...selectedAccountsSortable.filter((item) => item.id !== id), ]); }; const handleQuickToggleSelectedCollection = (item: ItemInterface) => { - if (selectedAccountsSortableIds.includes(item.id as number)) { - handleRemoveSelectedCollection(item.id as number); + if (selectedAccountsSortableIds.includes(item.id as string)) { + handleRemoveSelectedCollection(item.id as string); } else { setSelectedAccountsSortable([...selectedAccountsSortable, item]); } @@ -119,21 +119,21 @@ const ManageMembers = ({
- + {selectedAccountsSortable?.map((item) => ( - handleRemoveSelectedCollection(item.id as number)} + onClick={() => handleRemoveSelectedCollection(item.id as string)} key={item.id} - account={allAccounts[item.id as number] as AccountSecureModel} + account={allAccounts[item.id as string] as AccountSecureModel} /> ))} @@ -148,7 +148,7 @@ const ManageMembers = ({
- + {allAccountsSortable?.map((item) => (
- handleQuickToggleSelectedCollection(item)} key={item.id} - account={allAccounts[item.id as number] as AccountSecureModel} + account={allAccounts[item.id as string] as AccountSecureModel} />
))} diff --git a/src/components/Forms/CreateGroupForm/ManagePermissions.tsx b/src/components/Forms/CreateGroupForm/ManagePermissions.tsx new file mode 100644 index 0000000..6d45d55 --- /dev/null +++ b/src/components/Forms/CreateGroupForm/ManagePermissions.tsx @@ -0,0 +1,84 @@ +import React from "react"; +import { CreateGroupRequestForm } from "../../../types/forms/CreateGroupRequestForm"; +import { Separator } from "../../shadcn/Seperator"; +import { ScrollArea } from "../../shadcn/ScrollArea"; +import { Switch } from "../../shadcn/Switch"; +import PermissionSwitch from "../../Permissions/PermissionSwitch"; +import PermissionSwitchScrollArea from "../../Permissions/PermissionSwitchScrollArea"; +import CoursePermissionSwitchGroup from "../PermissionSwitchGroups/CoursePermissionSwitchGroup"; +import CollectionPermissionSwitchGroup from "../PermissionSwitchGroups/CollectionPermissionSwitchGroup"; +import ProblemPermissionSwitchGroup from "../PermissionSwitchGroups/ProblemPermissionSwitchGroup"; + +const ManagePermissions = ({ + createRequest, + setCreateRequest, +}: { + createRequest: CreateGroupRequestForm; + setCreateRequest: React.Dispatch< + React.SetStateAction + >; +}) => { + return ( +
+

Manage Permissions

+ +
+ +

+ Course Permission +

+ + setCreateRequest({ + ...createRequest, + manageCourses: !createRequest.manageCourses, + }) + } + onClickViewCourseLogs={() => + setCreateRequest({ + ...createRequest, + viewCourseLogs: !createRequest.viewCourseLogs, + }) + } + onClickViewCourses={() => + setCreateRequest({ + ...createRequest, + viewCourses: !createRequest.viewCourses, + }) + } + /> +

+ Collection Permission +

+ setCreateRequest({ + ...createRequest, + manageCollections: !createRequest.manageCollections + })} + onClickViewCollections={() => setCreateRequest({ + ...createRequest, + viewCollections: !createRequest.viewCollections + })} + /> +

+ Problem Permission +

+ +
+
+
+ ); +}; + +export default ManagePermissions; diff --git a/src/components/Forms/CreateGroupForm/index.tsx b/src/components/Forms/CreateGroupForm/index.tsx index e32d892..07fb57b 100644 --- a/src/components/Forms/CreateGroupForm/index.tsx +++ b/src/components/Forms/CreateGroupForm/index.tsx @@ -1,5 +1,5 @@ -import React, { useState } from "react"; -import { useNavigate } from "react-router-dom"; +import React, { useEffect, useState } from "react"; +import { useNavigate, useSearchParams } from "react-router-dom"; import { CreateGroupRequestForm } from "../../../types/forms/CreateGroupRequestForm"; import { ELEMENT_PARAGRAPH } from "@udecode/plate-paragraph"; import { PlateEditorValueType } from "../../../types/PlateEditorValueType"; @@ -18,6 +18,7 @@ import { CreateCollectionRequestForm } from "../../../types/forms/CreateCollecti import FormSaveButton from "../FormSaveButton"; import GeneralDetail from "./GeneralDetail"; import ManageMembers from "./ManageMembers"; +import ManagePermissions from "./ManagePermissions"; const TabList = [ { @@ -28,12 +29,16 @@ const TabList = [ value: "members", label: "Manage Members", }, + { + value: "permissions", + label: "Permissions", + }, ]; export type OnGroupSavedCallback = { setLoading: React.Dispatch>; - groupId: number; - setGroupId: React.Dispatch>; + // groupId: string; + // setGroupId: React.Dispatch>; createRequest: CreateGroupRequestForm; } @@ -47,7 +52,7 @@ const CreateGroupForm = ({ // onCollectionSave: (callback: OnCollectionSavedCallback) => void; }) => { const navigate = useNavigate(); - const [currentForm, setCurrentForm] = useState("general"); + const [currentForm, setCurrentForm] = useSearchParams(); const [loading, setLoading] = useState(false); const [groupId, setGroupId] = useState(-1); @@ -56,12 +61,19 @@ const CreateGroupForm = ({ createRequestInitialValue ); + useEffect(() => { + if (!currentForm.get("section")) { + setCurrentForm({ section: "general" }); + } + }, [currentForm]) + const handleSave = () => { + console.log(createRequest) onCourseSave({ setLoading, createRequest, - groupId, - setGroupId, + // groupId, + // setGroupId, }); }; @@ -72,7 +84,7 @@ const CreateGroupForm = ({ navigate("/my/groups")} + onClick={() => navigate(-1)} /> {createRequest.name === "" ? "Create Group" @@ -80,14 +92,14 @@ const CreateGroupForm = ({
- + {TabList.map((tab, index) => ( - setCurrentForm(tab.value) + setCurrentForm({section: tab.value}) } > {tab.label} @@ -104,18 +116,26 @@ const CreateGroupForm = ({
- {currentForm === "general" && ( + {currentForm.get("section") === "general" && ( )} - {currentForm === "members" && ( + {currentForm.get("section") === "members" && ( )} + {currentForm.get("section") === "permissions" && ( + + )} + +
); diff --git a/src/components/Forms/CreateProblemForm/ManageGroups.tsx b/src/components/Forms/CreateProblemForm/ManageGroups.tsx new file mode 100644 index 0000000..02861bf --- /dev/null +++ b/src/components/Forms/CreateProblemForm/ManageGroups.tsx @@ -0,0 +1,135 @@ +import React, { useEffect, useState } from "react"; +import { + CreateProblemRequestForm, + ProblemGroupPermissionRequestForm, +} from "../../../types/forms/CreateProblemRequestForm"; +import GroupAndPermissionManager, { + GroupAndPermissionManagerOnAddGroupsCallback, + GroupAndPermissionManagerOnRemoveGroupCallback, +} from "../GroupAndPermissionManager"; +import { GroupModel } from "../../../types/models/Group.model"; +import { GroupService } from "../../../services/Group.service"; +import PermissionSwitchScrollArea from "../../Permissions/PermissionSwitchScrollArea"; +import ProblemPermissionSwitchGroup from "../PermissionSwitchGroups/ProblemPermissionSwitchGroup"; + +const ManageGroups = ({ + createRequest, + setCreateRequest, +}: { + createRequest: CreateProblemRequestForm; + setCreateRequest: React.Dispatch< + React.SetStateAction + >; +}) => { + createRequest; + setCreateRequest; + + const accountId = String(localStorage.getItem("account_id")); + + const [groupPermission, setGroupPermission] = + useState(); + + const [selectedIndex, setselectedIndex] = useState(-1); + const [currentGroupId, setCurrentGroupId] = useState(""); + + const handleAddGroups = ({ + addingGroups, + }: GroupAndPermissionManagerOnAddGroupsCallback) => { + const newGroupPermissions = addingGroups.map((group) => ({ + groupId: group.group_id, + group, + manageProblems: group.permission_manage_problems, + viewProblems: group.permission_view_problems, + })); + + setCreateRequest({ + ...createRequest, + groupPermissions: [ + ...createRequest.groupPermissions, + ...newGroupPermissions, + ], + }); + }; + + const handleRemoveGroup = ({ + index, + }: GroupAndPermissionManagerOnRemoveGroupCallback) => { + if (index === selectedIndex) { + setselectedIndex(-1); + } + setCreateRequest({ + ...createRequest, + groupPermissions: createRequest.groupPermissions.filter( + (v, i) => i !== index + ), + }); + }; + + useEffect(() => { + if ( + selectedIndex >= 0 && + selectedIndex < createRequest.groupPermissions.length + ) { + setGroupPermission(createRequest.groupPermissions[selectedIndex]); + setCurrentGroupId( + createRequest.groupPermissions[selectedIndex].groupId + ); + } + }, [selectedIndex]); + + useEffect(() => { + if (groupPermission) { + setCreateRequest({ + ...createRequest, + groupPermissions: [ + ...createRequest.groupPermissions.slice(0, selectedIndex), + groupPermission, + ...createRequest.groupPermissions.slice(selectedIndex + 1), + ], + }); + } + }, [groupPermission]); + + const [allGroups, setAllGroups] = useState([]); + + useEffect(() => { + GroupService.getAllAsCreator(accountId).then((response) => { + setAllGroups(response.data.groups); + }); + }, [accountId]); + + return ( + handleAddGroups(e)} + onRemoveGroup={(e) => handleRemoveGroup(e)} + selectedIndex={selectedIndex} + setSelectedIndex={setselectedIndex} + > + + {groupPermission && selectedIndex >= 0 && ( + { + setGroupPermission({ + ...groupPermission, + manageProblems: !groupPermission.manageProblems, + }); + }} + onClickViewProblems={() => { + setGroupPermission({ + ...groupPermission, + viewProblems: !groupPermission.viewProblems, + }); + }} + /> + )} + + + ); +}; + +export default ManageGroups; diff --git a/src/components/Forms/CreateProblemForm/Requirement.tsx b/src/components/Forms/CreateProblemForm/Requirement.tsx index 4a3e6dd..ac22fc4 100644 --- a/src/components/Forms/CreateProblemForm/Requirement.tsx +++ b/src/components/Forms/CreateProblemForm/Requirement.tsx @@ -1,15 +1,79 @@ -import React from 'react' -import { CreateProblemRequestForm } from '../../../types/forms/CreateProblemRequestForm'; -import { Label } from '../../shadcn/Label'; -import { Input } from '../../shadcn/Input'; +import React, { ReactNode, useEffect, useState } from "react"; +import { CreateProblemRequestForm } from "../../../types/forms/CreateProblemRequestForm"; +import { Label } from "../../shadcn/Label"; +import { Input } from "../../shadcn/Input"; +import { Checkbox } from "../../shadcn/Checkbox"; +import { ProgrammingLanguageOptions } from "../../../constants/ProgrammingLanguage"; + +const AllowedLanguageCheckbox = ({ + children, + checked = false, + onClick = () => {}, +}: { + children: ReactNode; + checked?: boolean; + onClick?: () => void; +}) => { + return ( +
+ onClick()} /> +
{children}
+
+ ); +}; const Requirement = ({ createRequest, setCreateRequest, }: { createRequest: CreateProblemRequestForm; - setCreateRequest: React.Dispatch>; + setCreateRequest: React.Dispatch< + React.SetStateAction + >; }) => { + const hasSelectedAllLanguage = () => { + return ( + createRequest.allowedLanguage.filter((lang) => lang !== "") + .length === ProgrammingLanguageOptions.length + ); + }; + + const handleAllOption = () => { + if (hasSelectedAllLanguage()) { + setCreateRequest({ + ...createRequest, + allowedLanguage: [], + }); + } else { + setCreateRequest({ + ...createRequest, + allowedLanguage: ProgrammingLanguageOptions.map( + (proLang) => proLang.value + ), + }); + } + }; + + const handleOnClick = (language: string) => { + if (createRequest.allowedLanguage.includes(language)) { + setCreateRequest({ + ...createRequest, + allowedLanguage: createRequest.allowedLanguage.filter( + (lang) => lang !== language + ), + }); + } else { + setCreateRequest({ + ...createRequest, + allowedLanguage: [...createRequest.allowedLanguage, language], + }); + } + }; + + useEffect(() => { + console.log(createRequest); + }, [createRequest]); + return (
@@ -23,8 +87,31 @@ const Requirement = ({ }) } /> + + +
+ + All + +
+ +
+ {ProgrammingLanguageOptions.map((proLang) => ( + handleOnClick(proLang.value)} + > + {proLang.label} + + ))} +
); }; -export default Requirement \ No newline at end of file +export default Requirement; diff --git a/src/components/Forms/CreateProblemForm/index.tsx b/src/components/Forms/CreateProblemForm/index.tsx index 7ebbfd8..a5ef3e5 100644 --- a/src/components/Forms/CreateProblemForm/index.tsx +++ b/src/components/Forms/CreateProblemForm/index.tsx @@ -1,6 +1,6 @@ import { ELEMENT_PARAGRAPH } from "@udecode/plate-paragraph"; import React, { useEffect, useState } from "react"; -import { useNavigate } from "react-router-dom"; +import { useNavigate, useSearchParams } from "react-router-dom"; import { PlateEditorValueType } from "../../../types/PlateEditorValueType"; import { ProblemService } from "../../../services/Problem.service"; import { toast } from "../../shadcn/UseToast"; @@ -16,6 +16,7 @@ import Requirement from "./Requirement"; import Privacy from "./Privacy"; import { TestcaseModel } from "../../../types/models/Problem.model"; import FormSaveButton from "../FormSaveButton"; +import ManageGroups from "./ManageGroups"; const TabList = [ { @@ -34,32 +35,21 @@ const TabList = [ value: "privacy", label: "Privacy", }, + { + value: "groups", + label: "Manage Groups & Permissions", + }, ]; const testcaseFormat = (testcases: string, delimeter: string) => { return testcases.replace(/\r\n/g, "\n").split(delimeter + "\n"); }; -const transformCreateProblemRequestForm2CreateProblemRequest = ( - createRequest: CreateProblemRequestForm -): CreateProblemRequest => { - return { - title: createRequest.title, - language: createRequest.language, - description: JSON.stringify(createRequest.description), - solution: createRequest.solution, - testcases: testcaseFormat( - createRequest.testcases, - createRequest.testcase_delimeter - ), - time_limit: createRequest.time_limit, - }; -}; export type OnProblemSaveCallback = ( setLoading: React.Dispatch>, - problemId: number, - setProblemId: React.Dispatch>, + // problemid: string, + // setProblemId: React.Dispatch>, createRequest: CreateProblemRequestForm ) => void; @@ -72,21 +62,27 @@ const CreateProblemForm = ({ onProblemSave: OnProblemSaveCallback; validatedTestcases?: TestcaseModel[]; }) => { - const accountId = Number(localStorage.getItem("account_id")); + const accountId = String(localStorage.getItem("account_id")); const navigate = useNavigate(); const [loading, setLoading] = useState(false); - const [currentForm, setCurrentForm] = React.useState("general"); + const [currentForm, setCurrentForm] = useSearchParams(); const [createRequest, setCreateRequest] = useState(createRequestInitialValue); const [problemId, setProblemId] = useState(-1); const handleSave = () => { - onProblemSave(setLoading, problemId, setProblemId, createRequest); + onProblemSave(setLoading, createRequest); }; + useEffect(() => { + if (!currentForm.get("section")) { + setCurrentForm({ section: "general" }); + } + }, [currentForm]) + useEffect(() => { console.log(problemId) },[problemId]) @@ -107,7 +103,7 @@ const CreateProblemForm = ({ navigate("/my/problems")} + onClick={() => navigate(-1)} /> {createRequest.title === "" ? "Create Problem" @@ -115,14 +111,14 @@ const CreateProblemForm = ({
- + {TabList.map((tab, index) => ( - setCurrentForm(tab.value) + setCurrentForm({section: tab.value}) } > {tab.label} @@ -139,30 +135,36 @@ const CreateProblemForm = ({
- {currentForm === "general" && ( + {currentForm.get("section") === "general" && ( )} - {currentForm === "scoring" && ( + {currentForm.get("section") === "scoring" && ( )} - {currentForm === "requirement" && ( + {currentForm.get("section") === "requirement" && ( )} - {currentForm === "privacy" && ( + {currentForm.get("section") === "privacy" && ( )} + {currentForm.get("section") === "groups" && ( + + )}
); diff --git a/src/components/Forms/GroupAndPermissionManager.tsx b/src/components/Forms/GroupAndPermissionManager.tsx new file mode 100644 index 0000000..156e21a --- /dev/null +++ b/src/components/Forms/GroupAndPermissionManager.tsx @@ -0,0 +1,243 @@ +import React, { useEffect, useState } from "react"; +import { Separator } from "../shadcn/Seperator"; +import { ScrollArea } from "../shadcn/ScrollArea"; +import PermissionSwitchScrollArea from "../Permissions/PermissionSwitchScrollArea"; +import CoursePermissionSwitchGroup from "./PermissionSwitchGroups/CoursePermissionSwitchGroup"; +import { + CourseGroupPermissionRequestForm, + CreateCourseRequestForm, +} from "../../types/forms/CreateCourseRequestForm"; +import { Eye, PlusCircle, Users, X } from "lucide-react"; +import { Tooltip, TooltipContent, TooltipTrigger } from "../shadcn/Tooltip"; +import { Dialog, DialogContent } from "../shadcn/Dialog"; +import { Input } from "../shadcn/Input"; +import GroupCheckbox from "../GroupCheckbox"; +import { Button } from "../shadcn/Button"; +import { GroupService } from "../../services/Group.service"; +import { GroupModel } from "../../types/models/Group.model"; +import { Command } from "../shadcn/Command"; +import { + ContextMenu, + ContextMenuContent, + ContextMenuItem, + ContextMenuTrigger, +} from "../shadcn/ContextMenu"; +import { CreateCollectionRequestForm } from "../../types/forms/CreateCollectionRequestForm"; +import { set } from 'react-hook-form'; +import { useNavigate } from "react-router-dom"; +import { CreateProblemRequestForm } from "../../types/forms/CreateProblemRequestForm"; + +export type GroupAndPermissionManagerOnAddGroupsCallback = { + addingGroups: GroupModel[]; +} + +export type GroupAndPermissionManagerOnRemoveGroupCallback = { + index: number; +} + +const GroupListItem = ({ + hexColor = "#000000", + name = "Group Name", + hightlighted = false, +}: { + hexColor?: string; + name?: string; + hightlighted?: boolean; +}) => { + const customStyle = () => { + let style = + "font-bold text-base flex items-center cursor-pointer py-1 px-3 rounded-md "; + if (hightlighted) { + style += " bg-green-100 text-white"; + } + + return style; + }; + + return ( +
+
+
{name}
+
+ ); +}; + +const GroupListItemContextMenu = ({ + children, + onClickRemove = () => {}, + onClickViewGroup = () => {}, +}: { + children: React.ReactNode; + onClickRemove?: () => void; + onClickViewGroup?: () => void; +}) => { + return ( + + {children} + + onClickViewGroup()}> + + View Group + + onClickRemove()}> + + Remove + + + + ); +}; + +const GroupAndPermissionManager = ({ + allGroups=[], + createRequest, + setCreateRequest, + onAddGroups=()=>{}, + onRemoveGroup=()=>{}, + selectedIndex=-1, + setSelectedIndex=()=>{}, + children, +}: { + allGroups: GroupModel[]; + createRequest: CreateCourseRequestForm | CreateCollectionRequestForm | CreateProblemRequestForm; + setCreateRequest: + React.Dispatch> | + React.Dispatch> | + React.Dispatch>; + + onAddGroups?: (e: GroupAndPermissionManagerOnAddGroupsCallback) => void; + onRemoveGroup?: (e: GroupAndPermissionManagerOnRemoveGroupCallback) => void; + selectedIndex?: number; + setSelectedIndex?: React.Dispatch>; + children: React.ReactNode; +}) => { + const navigate = useNavigate(); + + const [openAddGroupsDialog, setOpenAddGroupsDialog] = + useState(false); + const [selectedGroupIds, setSelectedGroupIds] = useState([]); + + const handleSelectGroupCheckbox = (groupId: string) => { + if (selectedGroupIds.includes(groupId)) { + setSelectedGroupIds(selectedGroupIds.filter((g) => g !== groupId)); + } else { + setSelectedGroupIds([...selectedGroupIds, groupId]); + } + }; + + const handleRemoveGroupPermission = (index: number) => { + onRemoveGroup({index}) + } + + const getNotInPermissionGroup = () => { + const inPermissiongroupIds = createRequest.groupPermissions.map( + (groupPermission) => groupPermission.group.group_id + ); + return allGroups.filter( + (group) => !inPermissiongroupIds.includes(group.group_id) + ); + }; + + const handleAddGroups = () => { + const addingGroups = allGroups.filter((group) => + selectedGroupIds.includes(group.group_id) + ); + onAddGroups({addingGroups}) + setSelectedGroupIds([]); + setOpenAddGroupsDialog(false); + }; + + return ( +
+
+
+

+ + Groups +

+ + + setOpenAddGroupsDialog(true)} + size={20} + /> + + Add Group + +
+ +
+ {createRequest.groupPermissions.map( + (groupPermission, index) => ( +
setSelectedIndex(index)}> + navigate(`/my/groups/${groupPermission.group.group_id}/edit`)} + onClickRemove={() => handleRemoveGroupPermission(index)} + > + + +
+ ) + )} +
+
+
+
+ +
+ + {children} + + + + +

Add Groups

+ +
+ {getNotInPermissionGroup().map((group) => ( + + handleSelectGroupCheckbox( + group.group_id + ) + } + checked={selectedGroupIds.includes( + group.group_id + )} + group={group} + /> + ))} +
+
+
+ +
+
+
+
+ ); +}; + +export default GroupAndPermissionManager; diff --git a/src/components/Forms/PermissionSwitchGroups/CollectionPermissionSwitchGroup.tsx b/src/components/Forms/PermissionSwitchGroups/CollectionPermissionSwitchGroup.tsx new file mode 100644 index 0000000..e589955 --- /dev/null +++ b/src/components/Forms/PermissionSwitchGroups/CollectionPermissionSwitchGroup.tsx @@ -0,0 +1,63 @@ +import React from "react"; +import PermissionSwitch from "../../Permissions/PermissionSwitch"; +import { CreateGroupRequestForm } from "../../../types/forms/CreateGroupRequestForm"; +import { CreateCourseRequestForm } from "../../../types/forms/CreateCourseRequestForm"; +import { Switch } from "../../shadcn/Switch"; + +const CollectionPermissionSwitchGroup = ({ + manageCollectionsChecked = false, + viewCollectionsChecked = false, + onClickManageCollections = () => {}, + onClickViewCollections = () => {}, + variant = "normal", +}: { + manageCollectionsChecked?: boolean; + viewCollectionsChecked?: boolean; + onClickManageCollections?: () => void | undefined; + onClickViewCollections?: () => void | undefined; + variant?: "normal" | "minimal"; +}) => { + const CollectionPermissions = [ + { + title: "Manage Collections", + description: + "Can edit collections name and description. Can add or remove problems from collection as well.", + checked: manageCollectionsChecked, + onClick: onClickManageCollections, + }, + { + title: "View Collections", + description: + "Can view collection and thier problems. Note that those problems must be accessible as well.", + checked: viewCollectionsChecked, + onClick: onClickViewCollections, + }, + ]; + + return ( + (variant === "normal" && ( + <> + {CollectionPermissions.map((permission) => ( + + ))} + + )) || + (variant === "minimal" && ( + <> + {CollectionPermissions.map((permission) => ( +
+ {permission.title} + +
+ ))} + + )) + ); +}; + +export default CollectionPermissionSwitchGroup; diff --git a/src/components/Forms/PermissionSwitchGroups/CoursePermissionSwitchGroup.tsx b/src/components/Forms/PermissionSwitchGroups/CoursePermissionSwitchGroup.tsx new file mode 100644 index 0000000..2887038 --- /dev/null +++ b/src/components/Forms/PermissionSwitchGroups/CoursePermissionSwitchGroup.tsx @@ -0,0 +1,54 @@ +import React from "react"; +import PermissionSwitch from "../../Permissions/PermissionSwitch"; +import { CreateGroupRequestForm } from "../../../types/forms/CreateGroupRequestForm"; +import { CreateCourseRequestForm } from "../../../types/forms/CreateCourseRequestForm"; +import { set } from 'react-hook-form'; + +const CoursePermissionSwitchGroup = ({ + disabled = false, + manageCoursesChecked=false, + viewCourseLogsChecked=false, + viewCoursesChecked=false, + onClickManageCourses=()=>{}, + onClickViewCourseLogs=()=>{}, + onClickViewCourses=()=>{}, +}: { + disabled?: boolean; + manageCoursesChecked?: boolean; + viewCourseLogsChecked?: boolean; + viewCoursesChecked?: boolean; + onClickManageCourses?: () => void | undefined + onClickViewCourseLogs?: () => void | undefined + onClickViewCourses?: () => void | undefined +}) => { + return ( + <> + onClickManageCourses()} + disabled={disabled} + /> + onClickViewCourseLogs()} + disabled={disabled} + /> + onClickViewCourses()} + disabled={disabled} + /> + + ); +}; + +export default CoursePermissionSwitchGroup; diff --git a/src/components/Forms/PermissionSwitchGroups/ProblemPermissionSwitchGroup.tsx b/src/components/Forms/PermissionSwitchGroups/ProblemPermissionSwitchGroup.tsx new file mode 100644 index 0000000..ec4e61d --- /dev/null +++ b/src/components/Forms/PermissionSwitchGroups/ProblemPermissionSwitchGroup.tsx @@ -0,0 +1,32 @@ +import PermissionSwitch from "../../Permissions/PermissionSwitch"; + +const ProblemPermissionSwitchGroup = ({ + manageProblemsChecked = false, + viewProblemsChecked = false, + onClickManageProblems = () => {}, + onClickViewProblems = () => {}, +}: { + manageProblemsChecked?: boolean; + viewProblemsChecked?: boolean; + onClickManageProblems?: () => void | undefined; + onClickViewProblems?: () => void | undefined; +}) => { + return ( + <> + onClickManageProblems()} + /> + onClickViewProblems()} + /> + + ); +}; + +export default ProblemPermissionSwitchGroup; diff --git a/src/components/GroupCheckbox.tsx b/src/components/GroupCheckbox.tsx new file mode 100644 index 0000000..41c7071 --- /dev/null +++ b/src/components/GroupCheckbox.tsx @@ -0,0 +1,34 @@ +import React from "react"; +import { Checkbox } from "./shadcn/Checkbox"; +import { GroupModel } from "../types/models/Group.model"; + +const GroupCheckbox = ({ + group, + checked=true, + onClick=()=>{} +}:{ + group: GroupModel + checked?:boolean + onClick?:()=>void +}) => { + + const customStyle = () => { + let style = "border rounded-md p-2 flex items-center justify-between cursor-pointer " + if (checked) { + style += "bg-green-100 text-green-800 border-green-600 " + } + return style + } + + return ( +
+
+
+

{group.name}

+
+ +
+ ); +}; + +export default GroupCheckbox; diff --git a/src/components/MyCourseCard.tsx b/src/components/MyCourseCard.tsx deleted file mode 100644 index fe5289d..0000000 --- a/src/components/MyCourseCard.tsx +++ /dev/null @@ -1,75 +0,0 @@ -import React, { useState } from "react"; -import { Card, CardContent } from "./shadcn/Card"; -import { Folder, LibraryBig } from "lucide-react"; -import { TopicPopulateTopicCollectionPopulateCollectionModel } from "../types/models/Topic.model"; -import { readableDateFormat } from "../utilities/ReadableDateFormat"; -import { BASE_URL } from "../constants/BackendBaseURL"; -import { useNavigate } from "react-router-dom"; - -const MyCourseCard = ({ - course, -}: { - course: TopicPopulateTopicCollectionPopulateCollectionModel; -}) => { - - const navigate = useNavigate(); - const [mouseOver, setMouseOver] = useState(false); - - return ( - navigate(`/my/courses/${course.topic_id}`)} - onMouseOver={() => setMouseOver(true)} - onMouseOut={() => setMouseOver(false)} - className={`pt-6 px-5 cursor-pointer ${ - mouseOver ? "border-green-500 bg-green-100" : "" - }`} - > - {/*
- */} - - -
- - {mouseOver ? ( -

{course.name}

- ) : ( - course.name - )} -
-
-
-
-

Lasted Updated

-

- {readableDateFormat(course.updated_date)} -

-
-
-

Created Date

-

- {readableDateFormat(course.created_date)} -

-
-
- -
-
-

Visibility

-

Public

-
-
- -
-

- - Collections ({course.collections.length}) -

-
-
-
- {/*
*/} -
- ); -}; - -export default MyCourseCard; diff --git a/src/components/NavbarCollectionsProblemsAccordion.tsx b/src/components/NavbarCollectionsProblemsAccordion.tsx index 6f5140a..388e18a 100644 --- a/src/components/NavbarCollectionsProblemsAccordion.tsx +++ b/src/components/NavbarCollectionsProblemsAccordion.tsx @@ -31,7 +31,7 @@ const NavbarCollectionProblemCard = ({ result += "hover:bg-blue-100 " } - if (problem.problem_id === Number(problemId)) { + if (problem.problem_id === String(problemId)) { if (problem.best_submission?.is_passed) { result += "bg-green-100 " } @@ -71,16 +71,6 @@ const NavbarCollectionsProblemsAccordion = ({ } = useContext(CourseNavSidebarContext) const handleAccordionTrigger = (index:number) => { - console.log({ - course, - setCourse, - isOpen, - setIsOpen, - section, - setSection, - recentOpenCollection, - setRecentOpenCollection - }) if (recentOpenCollection.length > 0 && recentOpenCollection.includes(String(index))) { setRecentOpenCollection(recentOpenCollection.filter((value) => value !== String(index))) } diff --git a/src/components/NavigationBar/CourseNavSidebar.tsx b/src/components/NavigationBar/CourseNavSidebar.tsx index e5ec43a..fcddf23 100644 --- a/src/components/NavigationBar/CourseNavSidebar.tsx +++ b/src/components/NavigationBar/CourseNavSidebar.tsx @@ -10,7 +10,7 @@ import { ScrollArea } from "../shadcn/ScrollArea"; const CourseNavSidebar = () => { const navigate = useNavigate(); - const accountId = Number(localStorage.getItem("account_id")); + const accountId = String(localStorage.getItem("account_id")); const { courseId } = useParams(); const courseNavSidebarContext = useContext(CourseNavSidebarContext); @@ -19,7 +19,7 @@ const CourseNavSidebar = () => { useEffect(() => { - TopicService.getPublicByAccount(accountId, Number(courseId)).then( + TopicService.getPublicByAccount(accountId, String(courseId)).then( (response) => { console.log(response.data); setCourse(response.data); diff --git a/src/components/NavigationBar/NavSidebar.tsx b/src/components/NavigationBar/NavSidebar.tsx index 3f96154..88439f7 100644 --- a/src/components/NavigationBar/NavSidebar.tsx +++ b/src/components/NavigationBar/NavSidebar.tsx @@ -30,7 +30,7 @@ const NavSidebar = () => { const navigate = useNavigate(); const { isOpen, setIsOpen } = useContext(NavSidebarContext); - + const { section } = useContext(NavSidebarContext); const customIconBehaviour = (selected: boolean) => { diff --git a/src/components/NavigationBar/NavigationBar.tsx b/src/components/NavigationBar/NavigationBar.tsx index dd5a78f..a2a8004 100644 --- a/src/components/NavigationBar/NavigationBar.tsx +++ b/src/components/NavigationBar/NavigationBar.tsx @@ -57,6 +57,12 @@ const NavigationBar = (/* { isLogin = false }: { isLogin?: boolean } */) => { > Courses + + Dashboard + {/* diff --git a/src/components/NavigationBar/ProfileDropdown.tsx b/src/components/NavigationBar/ProfileDropdown.tsx index 246a8c0..956f305 100644 --- a/src/components/NavigationBar/ProfileDropdown.tsx +++ b/src/components/NavigationBar/ProfileDropdown.tsx @@ -42,7 +42,7 @@ const ProfileDropdown = ({ children }: { children: ReactNode }) => { const navigate = useNavigate() const username = localStorage.getItem('username') - const account_id = Number(localStorage.getItem('account_id')) + const account_id = String(localStorage.getItem('account_id')) const token = localStorage.getItem('token') const handleLogout = async () => { @@ -89,6 +89,11 @@ const ProfileDropdown = ({ children }: { children: ReactNode }) => { My Courses {/* ⌘K */} + navigate("/my/groups")}> + + My Groups + {/* ⌘K */} + {/* diff --git a/src/components/Permissions/PermissionSwitch.tsx b/src/components/Permissions/PermissionSwitch.tsx new file mode 100644 index 0000000..6f1e5bc --- /dev/null +++ b/src/components/Permissions/PermissionSwitch.tsx @@ -0,0 +1,32 @@ +import React, { ReactNode } from "react"; +import { Separator } from "../shadcn/Seperator"; +import { Switch } from "../shadcn/Switch"; + +const PermissionSwitch = ({ + title = "Title", + description = "Description", + checked = false, + onClick = () => {}, + disabled = false, +}: { + title?: ReactNode; + description?: ReactNode; + checked?: boolean; + onClick?: () => void; + disabled?: boolean; +}) => { + return ( +
+
+
+

{title}

+ +
+

{description}

+
+ +
+ ); +}; + +export default PermissionSwitch; diff --git a/src/components/Permissions/PermissionSwitchScrollArea.tsx b/src/components/Permissions/PermissionSwitchScrollArea.tsx new file mode 100644 index 0000000..a84f529 --- /dev/null +++ b/src/components/Permissions/PermissionSwitchScrollArea.tsx @@ -0,0 +1,20 @@ +import React from "react"; +import { ScrollArea } from "../shadcn/ScrollArea"; + +const PermissionSwitchScrollArea = ({ + children, + className = "", + childrenClassName = "", +}: { + children: React.ReactNode; + className?: string; + childrenClassName?: string; +}) => { + return ( + +
{children}
+
+ ); +}; + +export default PermissionSwitchScrollArea; diff --git a/src/components/ProblemViewLayout.tsx b/src/components/ProblemViewLayout.tsx index a510655..e72c719 100644 --- a/src/components/ProblemViewLayout.tsx +++ b/src/components/ProblemViewLayout.tsx @@ -2,7 +2,7 @@ import { ArrowLeft, Loader2 } from "lucide-react"; import React, { useEffect, useState } from "react"; import { useNavigate } from "react-router-dom"; import { readableDateFormat } from "../utilities/ReadableDateFormat"; -import { ProblemPoplulateCreatorModel } from "../types/models/Problem.model"; +import { ProblemPoplulateCreatorModel, ProblemPopulateCreatorSecureModel } from "../types/models/Problem.model"; import ReadOnlyPlate from "./ReadOnlyPlate"; import { handleDeprecatedDescription } from "../utilities/HandleDeprecatedDescription"; import { Separator } from "./shadcn/Seperator"; @@ -14,6 +14,7 @@ import styled from "styled-components"; import { Button } from "./shadcn/Button"; import PreviousSubmissionsCombobox from "./PreviousSubmissionsCombobox"; import { Editor as MonacoEditor } from "@monaco-editor/react"; +import { ResizableHandle, ResizablePanel, ResizablePanelGroup } from "./shadcn/Resizable"; export type OnSubmitProblemViewLayoutCallback = { setGrading: React.Dispatch> @@ -28,7 +29,7 @@ const ProblemViewLayout = ({ previousSubmissions }:{ onSubmit: (callback: OnSubmitProblemViewLayoutCallback) => void - problem: ProblemPoplulateCreatorModel + problem: ProblemPoplulateCreatorModel | ProblemPopulateCreatorSecureModel previousSubmissions: GetSubmissionByAccountProblemResponse }) => { @@ -48,7 +49,7 @@ const ProblemViewLayout = ({ onSubmit({setGrading, setLastedSubmission,selectedLanguage,submitCodeValue}) } - const handleSelectPreviousSubmission = (submissionId: number) => { + const handleSelectPreviousSubmission = (submissionId: string) => { let submission = null; if ( submissionId === previousSubmissions?.best_submission?.submission_id @@ -71,18 +72,18 @@ const ProblemViewLayout = ({ }; - useEffect(() => { + // useEffect(() => { - console.log('pb',JSON.parse( - handleDeprecatedDescription( - String(problem?.description) - ) - )) - },[problem]) + // console.log('pb',JSON.parse( + // handleDeprecatedDescription( + // String(problem?.description) + // ) + // )) + // },[problem]) return ( -
-
+ +
)}
-
-
+ + {/*
-
-
+
*/} + +
problem?.allowed_languages.includes(lang.value))} onSelect={(value) => setSelectedLanguage(value)} initialValue={selectedLanguage} value={selectedLanguage} @@ -173,11 +175,11 @@ const ProblemViewLayout = ({ previousSubmissions?.submissions as SubmissionPopulateSubmissionTestcasesSecureModel[] } onSelect={(submissionId) => - handleSelectPreviousSubmission(Number(submissionId)) + handleSelectPreviousSubmission(String(submissionId)) } />
-
-
+
+
); }; diff --git a/src/components/RolesCombobox.tsx b/src/components/RolesCombobox.tsx new file mode 100644 index 0000000..825806e --- /dev/null +++ b/src/components/RolesCombobox.tsx @@ -0,0 +1,91 @@ +"use client" + +import * as React from "react" +import { Check, ChevronsUpDown } from "lucide-react" + +import { cn } from "../lib/utils" +import { Button } from "./shadcn/Button" +import { + Command, + CommandEmpty, + CommandGroup, + CommandInput, + CommandItem, +} from "./shadcn/Command" +import { + Popover, + PopoverContent, + PopoverTrigger, +} from "./shadcn/Popover" + +const frameworks = [ + { + value: "next.js", + label: "Next.js", + }, + { + value: "sveltekit", + label: "SvelteKit", + }, + { + value: "nuxt.js", + label: "Nuxt.js", + }, + { + value: "remix", + label: "Remix", + }, + { + value: "astro", + label: "Astro", + }, +] + +export function RolesCombobox() { + const [open, setOpen] = React.useState(false) + const [value, setValue] = React.useState("") + + return ( + + + + + + + + No framework found. + + {frameworks.map((framework) => ( + { + setValue(currentValue === value ? "" : currentValue) + setOpen(false) + }} + > + + {framework.label} + + ))} + + + + + ) +} diff --git a/src/components/SortableCardContainer.tsx b/src/components/SortableCardContainer.tsx index 1f7da5a..20c9f96 100644 --- a/src/components/SortableCardContainer.tsx +++ b/src/components/SortableCardContainer.tsx @@ -9,7 +9,7 @@ const SortableCardContainer = ({ children: React.ReactNode; }) => { return ( - + {children} diff --git a/src/components/TestcaseValidationAccordion.tsx b/src/components/TestcaseValidationAccordion.tsx index 3dd03f4..a6e2c86 100644 --- a/src/components/TestcaseValidationAccordion.tsx +++ b/src/components/TestcaseValidationAccordion.tsx @@ -29,9 +29,9 @@ const TestcaseValidationInstance = ({ // const [inputValue, setInputValue] = useState("1 2 3"); // const [outputValue, setOutputValue] = useState("Hello World!"); - useEffect(() => { - console.log(inputValue, outputValue, status); - }, [outputValue]); + // useEffect(() => { + // console.log(inputValue, outputValue, status); + // }, [outputValue]); return ( diff --git a/src/components/TestcasesGradingIndicator.tsx b/src/components/TestcasesGradingIndicator.tsx index a3913da..551147b 100644 --- a/src/components/TestcasesGradingIndicator.tsx +++ b/src/components/TestcasesGradingIndicator.tsx @@ -73,9 +73,9 @@ const TestcasesGradingIndicator = ({submissionTestcases,disableHover=false,class sizeX?: number; sizeY?: number; }) => { - useEffect(()=>{ - console.log(submissionTestcases) - },[submissionTestcases]) + // useEffect(()=>{ + // console.log(submissionTestcases) + // },[submissionTestcases]) return (
{ diff --git a/src/components/TopicCollectionsAccordion.tsx b/src/components/TopicCollectionsAccordion.tsx index c2d0478..7b29bee 100644 --- a/src/components/TopicCollectionsAccordion.tsx +++ b/src/components/TopicCollectionsAccordion.tsx @@ -7,11 +7,11 @@ import { AccordionTrigger, } from "./shadcn/Accordion"; import { FileSpreadsheet, Folder } from "lucide-react"; -import PublicProblemCard from "./PublicProblemCard"; +import PublicProblemCard from "./Cards/ProblemCards/PublicProblemCard"; import { CollectionPopulateCollectionProblemPopulateProblemPopulateAccountAndSubmissionPopulateSubmissionTestcasesSecureModel } from "../types/models/Collection.model"; import CardContainer from "./CardContainer"; import { ScrollArea } from "./shadcn/ScrollArea"; -import PublicProblemMiniCard from "./PublicProblemMiniCard"; +import PublicProblemMiniCard from "./Cards/ProblemCards/PublicProblemMiniCard"; import { TopicCollectionPopulateCollectionPopulateCollectionProblemPopulateProblemPopulateAccountAndSubmissionPopulateSubmissionTestcasesSecureModel } from "../types/models/Topic.model"; const TopicCollectionsAccordian = ({ diff --git a/src/components/shadcn/Carousel.tsx b/src/components/shadcn/Carousel.tsx new file mode 100644 index 0000000..e8be0bf --- /dev/null +++ b/src/components/shadcn/Carousel.tsx @@ -0,0 +1,257 @@ +import * as React from "react" +import useEmblaCarousel, { + type EmblaCarouselType as CarouselApi, + type EmblaOptionsType as CarouselOptions, + type EmblaPluginType as CarouselPlugin, +} from "embla-carousel-react" +import { ArrowLeft, ArrowRight } from "lucide-react" + +import { cn } from "../../lib/utils" +import { Button } from "./Button" + +type CarouselProps = { + opts?: CarouselOptions + plugins?: CarouselPlugin[] + orientation?: "horizontal" | "vertical" + setApi?: (api: CarouselApi) => void +} + +type CarouselContextProps = { + carouselRef: ReturnType[0] + api: ReturnType[1] + scrollPrev: () => void + scrollNext: () => void + canScrollPrev: boolean + canScrollNext: boolean +} & CarouselProps + +const CarouselContext = React.createContext(null) + +function useCarousel() { + const context = React.useContext(CarouselContext) + + if (!context) { + throw new Error("useCarousel must be used within a ") + } + + return context +} + +const Carousel = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes & CarouselProps +>( + ( + { + orientation = "horizontal", + opts, + setApi, + plugins, + className, + children, + ...props + }, + ref + ) => { + const [carouselRef, api] = useEmblaCarousel( + { + ...opts, + axis: orientation === "horizontal" ? "x" : "y", + }, + plugins + ) + const [canScrollPrev, setCanScrollPrev] = React.useState(false) + const [canScrollNext, setCanScrollNext] = React.useState(false) + + const onSelect = React.useCallback((api: CarouselApi) => { + if (!api) { + return + } + + setCanScrollPrev(api.canScrollPrev()) + setCanScrollNext(api.canScrollNext()) + }, []) + + const scrollPrev = React.useCallback(() => { + api?.scrollPrev() + }, [api]) + + const scrollNext = React.useCallback(() => { + api?.scrollNext() + }, [api]) + + const handleKeyDown = React.useCallback( + (event: React.KeyboardEvent) => { + if (event.key === "ArrowLeft") { + event.preventDefault() + scrollPrev() + } else if (event.key === "ArrowRight") { + event.preventDefault() + scrollNext() + } + }, + [scrollPrev, scrollNext] + ) + + React.useEffect(() => { + if (!api || !setApi) { + return + } + + setApi(api) + }, [api, setApi]) + + React.useEffect(() => { + if (!api) { + return + } + + onSelect(api) + api.on("reInit", onSelect) + api.on("select", onSelect) + + return () => { + api?.off("select", onSelect) + } + }, [api, onSelect]) + + return ( + +
+ {children} +
+
+ ) + } +) +Carousel.displayName = "Carousel" + +const CarouselContent = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => { + const { carouselRef, orientation } = useCarousel() + + return ( +
+
+
+ ) +}) +CarouselContent.displayName = "CarouselContent" + +const CarouselItem = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => { + const { orientation } = useCarousel() + + return ( +
+ ) +}) +CarouselItem.displayName = "CarouselItem" + +const CarouselPrevious = React.forwardRef< + HTMLButtonElement, + React.ComponentProps +>(({ className, variant = "outline", size = "icon", ...props }, ref) => { + const { orientation, scrollPrev, canScrollPrev } = useCarousel() + + return ( + + ) +}) +CarouselPrevious.displayName = "CarouselPrevious" + +const CarouselNext = React.forwardRef< + HTMLButtonElement, + React.ComponentProps +>(({ className, variant = "outline", size = "icon", ...props }, ref) => { + const { orientation, scrollNext, canScrollNext } = useCarousel() + + return ( + + ) +}) +CarouselNext.displayName = "CarouselNext" + +export { + type CarouselApi, + Carousel, + CarouselContent, + CarouselItem, + CarouselPrevious, + CarouselNext, +} diff --git a/src/components/shadcn/Resizable.tsx b/src/components/shadcn/Resizable.tsx new file mode 100644 index 0000000..5132aeb --- /dev/null +++ b/src/components/shadcn/Resizable.tsx @@ -0,0 +1,43 @@ +import { GripVertical } from "lucide-react" +import * as ResizablePrimitive from "react-resizable-panels" + +import { cn } from "../../lib/utils" + +const ResizablePanelGroup = ({ + className, + ...props +}: React.ComponentProps) => ( + +) + +const ResizablePanel = ResizablePrimitive.Panel + +const ResizableHandle = ({ + withHandle, + className, + ...props +}: React.ComponentProps & { + withHandle?: boolean +}) => ( + div]:rotate-90", + className + )} + {...props} + > + {withHandle && ( +
+ +
+ )} +
+) + +export { ResizablePanelGroup, ResizablePanel, ResizableHandle } diff --git a/src/constants/BackendBaseURL.ts b/src/constants/BackendBaseURL.ts index d6b4f29..cc79b04 100644 --- a/src/constants/BackendBaseURL.ts +++ b/src/constants/BackendBaseURL.ts @@ -1 +1,6 @@ -export const BASE_URL = 'http://192.168.0.11:8000'; \ No newline at end of file +import axios from "axios"; + +// export const BASE_URL = 'http://192.168.0.11:8000'; +export const BASE_URL = 'http://localhost:8000'; +// axios.defaults.headers.common['account_id'] = localStorage.getItem('account_id') || null; +axios.defaults.headers.common['Authorization'] = localStorage.getItem('token') || null; diff --git a/src/layout/NavbarSidebarLayout.tsx b/src/layout/NavbarSidebarLayout.tsx index 107143f..24a5960 100644 --- a/src/layout/NavbarSidebarLayout.tsx +++ b/src/layout/NavbarSidebarLayout.tsx @@ -1,23 +1,30 @@ -import React, { useState } from "react"; +import React, { useContext, useState } from "react"; import { Separator } from "../components/shadcn/Seperator"; import NavbarMenuLayout from "./NavbarMenuLayout"; import { useNavigate } from "react-router-dom"; import NavSidebar from "../components/NavigationBar/NavSidebar"; +import { LoginContext } from "../contexts/LoginContext"; const NavbarSidebarLayout = ({ children }: { children: React.ReactNode }) => { - const navigate = useNavigate(); const [close, setClose] = useState(false); + const { isLogin } = useContext(LoginContext); return ( -
- + { + isLogin ? ( +
+
{children}
+ ) : ( +

No Access

+ ) + } ); }; diff --git a/src/router.tsx b/src/router.tsx index d70f65b..e9a7ff9 100644 --- a/src/router.tsx +++ b/src/router.tsx @@ -36,19 +36,19 @@ const Router = () => { } /> } /> - } /> + } /> } /> - } /> } /> + } /> } /> } /> - } /> + } /> } /> } /> - } /> + } /> } /> } /> diff --git a/src/services/Collection.service.ts b/src/services/Collection.service.ts index 4e6125b..eaafb1b 100644 --- a/src/services/Collection.service.ts +++ b/src/services/Collection.service.ts @@ -1,6 +1,6 @@ import axios from "axios"; -import { CollectionServiceAPI } from "../types/apis/Collection.api"; -import { CollectionModel, CollectionPopulateProblemSecureModel, CollectionProblemModel, GetCollectionByAccountResponse } from "../types/models/Collection.model"; +import { CollectionServiceAPI, GetCollectionByAccountResponse } from "../types/apis/Collection.api"; +import { CollectionModel, CollectionPopulateCollectionProblemsPopulateProblemAndCollectionGroupPermissionsPopulateGroupModel, CollectionPopulateCollectionProblemsPopulateProblemPopulateAccountAndTestcasesAndProblemGroupPermissionsPopulateGroupAndCollectionGroupPermissionsPopulateGroupModel, CollectionPopulateProblemSecureModel, CollectionProblemModel } from "../types/models/Collection.model"; import { BASE_URL } from "../constants/BackendBaseURL"; export const CollectionService: CollectionServiceAPI = { @@ -8,11 +8,11 @@ export const CollectionService: CollectionServiceAPI = { return axios.post(`${BASE_URL}/api/accounts/${accountId}/collections`,request); }, - get: (collectionId) => { - return axios.get(`${BASE_URL}/api/collections/${collectionId}`); + get: (collectionId,accountId) => { + return axios.get(`${BASE_URL}/api/accounts/${accountId}/collections/${collectionId}`); }, - getAllByAccount: (accountId) => { + getAllAsCreator: (accountId) => { return axios.get(`${BASE_URL}/api/accounts/${accountId}/collections`); }, @@ -20,6 +20,10 @@ export const CollectionService: CollectionServiceAPI = { return axios.put(`${BASE_URL}/api/collections/${collectionId}`,request); }, + delete: (collectionId,accountId) => { + return axios.delete(`${BASE_URL}/api/accounts/${accountId}/collections/${collectionId}`); + }, + addProblem: (collectionId,problemIds) => { return axios.put(`${BASE_URL}/api/collections/${collectionId}/problems/add`,{problemIds}); }, @@ -31,5 +35,9 @@ export const CollectionService: CollectionServiceAPI = { updateProblem: (collectionId,problemIds) => { return axios.put(`${BASE_URL}/api/collections/${collectionId}/problems/update`,{problem_ids: problemIds}); }, + + updateGroupPermissions: (collectionId,accountId,groups) => { + return axios.put(`${BASE_URL}/api/accounts/${accountId}/collections/${collectionId}/groups`,{groups}); + } } \ No newline at end of file diff --git a/src/services/Group.service.ts b/src/services/Group.service.ts index 544c4fa..5737e7b 100644 --- a/src/services/Group.service.ts +++ b/src/services/Group.service.ts @@ -3,7 +3,7 @@ import { GroupSerivceAPI } from "../types/apis/Group.api"; import { BASE_URL } from "../constants/BackendBaseURL"; export const GroupService: GroupSerivceAPI = { - get: async (groupId:number,query?:any) => { + get: async (groupId:string,query?:any) => { const response = await axios.get(`${BASE_URL}/api/groups/${groupId}`,{ params: query }) @@ -11,9 +11,9 @@ export const GroupService: GroupSerivceAPI = { return response; }, - getAllByAccount: async (accountId:number,query?:any) => { + getAllAsCreator: async (accountId:string,query?:any) => { const response = await axios.get(`${BASE_URL}/api/accounts/${accountId}/groups`,{ - params: query + params: query, }) return response; diff --git a/src/services/Problem.service.ts b/src/services/Problem.service.ts index 0a86abb..9b66a0f 100644 --- a/src/services/Problem.service.ts +++ b/src/services/Problem.service.ts @@ -1,7 +1,7 @@ import axios from "axios"; import { GetAllProblemsByAccountResponse, GetAllProblemsResponse, ProblemServiceAPI, ValidateProgramResponse } from "../types/apis/Problem.api"; import { BASE_URL } from "../constants/BackendBaseURL"; -import { ProblemModel, ProblemPoplulateCreatorModel } from "../types/models/Problem.model"; +import { ProblemModel, ProblemPoplulateCreatorModel, ProblemPopulateAccountAndTestcasesAndProblemGroupPermissionsPopulateGroupModel, ProblemPopulateCreatorSecureModel, ProblemSecureModel } from "../types/models/Problem.model"; import { ErrorResponse } from "../types/apis/ErrorHandling"; export const ProblemService: ProblemServiceAPI = { @@ -13,12 +13,12 @@ export const ProblemService: ProblemServiceAPI = { return axios.get(`${BASE_URL}/api/problems`); }, - getAllByAccount: async (accountId) => { + getAllAsCreator: async (accountId) => { return axios.get(`${BASE_URL}/api/accounts/${accountId}/problems`); }, - get: async (problemId) => { - return axios.get(`${BASE_URL}/api/problems/${problemId}`); + get: async (accountId,problemId) => { + return axios.get(`${BASE_URL}/api/accounts/${accountId}/problems/${problemId}`); }, update: async (problemId,request) => { @@ -33,7 +33,15 @@ export const ProblemService: ProblemServiceAPI = { // return axios.delete(`${BASE_URL}/api/problems/`, {problem: problemIds}); // }, + updateGroupPermissions: async (problemId, accountId,groups) => { + return axios.put(`${BASE_URL}/api/accounts/${accountId}/problems/${problemId}/groups`, {groups}); + }, + validateProgram: async (request) => { return axios.post(`${BASE_URL}/api/problems/validate`, request); - } + }, + + getPublic: async (problemId) => { + return axios.get(`${BASE_URL}/api/problems/${problemId}`); + }, } \ No newline at end of file diff --git a/src/services/Topic.service.ts b/src/services/Topic.service.ts index 4eea157..2afaa5f 100644 --- a/src/services/Topic.service.ts +++ b/src/services/Topic.service.ts @@ -1,7 +1,7 @@ import axios from "axios"; import { GetAllTopicsByAccountResponse, TopicSerivceAPI } from "../types/apis/Topic.api"; import { BASE_URL } from "../constants/BackendBaseURL"; -import { TopicModel, TopicPopulateTopicCollectionPopulateCollectionModel, TopicPopulateTopicCollectionPopulateCollectionPopulateCollectionProblemPopulateProblemPopulateAccountAndSubmissionPopulateSubmissionTestcasesSecureModel } from "../types/models/Topic.model"; +import { TopicModel, TopicPopulateTopicCollectionPopulateCollectionAndTopicGroupPermissionPopulateGroupModel, TopicPopulateTopicCollectionPopulateCollectionModel, TopicPopulateTopicCollectionPopulateCollectionPopulateCollectionProblemPopulateProblemAndTopicGroupPermissionPopulateGroupModel, TopicPopulateTopicCollectionPopulateCollectionPopulateCollectionProblemPopulateProblemPopulateAccountAndSubmissionPopulateSubmissionTestcasesSecureModel, TopicPopulateTopicCollectionPopulateCollectionPopulateCollectionProblemsPopulateProblemAndCollectionGroupPermissionsPopulateGroupAndTopicGroupPermissionPopulateGroupModel } from "../types/models/Topic.model"; export const TopicService: TopicSerivceAPI = { create: async (accountId, request) => { @@ -10,16 +10,26 @@ export const TopicService: TopicSerivceAPI = { }, get: async (accountId,topicId) => { - const response = await axios.get(`${BASE_URL}/api/accounts/${accountId}/topics/${topicId}`); + const response = await axios.get(`${BASE_URL}/api/accounts/${accountId}/topics/${topicId}`); return response; }, - update: async (topicId, request) => { - const response = await axios.put(`${BASE_URL}/api/topics/${topicId}`, request); + getAllAccessibleByAccount: async (accountId) => { + const response = await axios.get(`${BASE_URL}/api/accounts/${accountId}/access/topics`); return response; }, - getAllByAccount: async (accountId) => { + update: async (topicId,accountId, request) => { + const response = await axios.put(`${BASE_URL}/api/accounts/${accountId}/topics/${topicId}`, request); + return response; + }, + + delete: async (topicId,accountId) => { + const response = await axios.delete(`${BASE_URL}/api/accounts/${accountId}/topics/${topicId}`); + return response; + }, + + getAllAsCreator: async (accountId) => { const response = await axios.get(`${BASE_URL}/api/accounts/${accountId}/topics`); return response; }, @@ -34,5 +44,12 @@ export const TopicService: TopicSerivceAPI = { getPublicByAccount: async (accountId,topicId) => { const response = await axios.get(`${BASE_URL}/api/topics/${topicId}?account_id=${accountId}`); return response; + }, + + updateGroupPermissions: async (topicId,accountId, groups) => { + const response = await axios.put(`${BASE_URL}/api/accounts/${accountId}/topics/${topicId}/groups`, { + groups: groups + }); + return response; } } \ No newline at end of file diff --git a/src/types/adapters/Collection.adapter.ts b/src/types/adapters/Collection.adapter.ts index 5f38ebf..6fde69e 100644 --- a/src/types/adapters/Collection.adapter.ts +++ b/src/types/adapters/Collection.adapter.ts @@ -1,9 +1,36 @@ -import { CollectionHashedTable, CollectionModel, CollectionPopulateProblemSecureModel } from "../models/Collection.model"; +import { CreateCollectionRequestForm } from "../forms/CreateCollectionRequestForm"; +import { CollectionHashedTable, CollectionModel, CollectionPopulateCollectionProblemPopulateProblemModel, CollectionPopulateCollectionProblemsPopulateProblemAndCollectionGroupPermissionsPopulateGroupModel, CollectionPopulateCollectionProblemsPopulateProblemPopulateAccountAndTestcasesAndProblemGroupPermissionsPopulateGroupAndCollectionGroupPermissionsPopulateGroupModel, CollectionPopulateProblemSecureModel, CollectionProblemModel } from "../models/Collection.model"; -export function transformCollectionModel2CollectionHashedTable(collections: CollectionModel[] | CollectionPopulateProblemSecureModel[] ): CollectionHashedTable { - let result = []; +export function transformCollectionPopulateProblemSecureModel2CollectionHashedTable(collections: CollectionPopulateCollectionProblemPopulateProblemModel[] ): CollectionHashedTable { + let result:CollectionHashedTable = {}; for (const collection of collections) { result[collection.collection_id] = collection; } return result; +} + +export function transformCollectionPopulateCollectionProblemsPopulateProblemAndCollectionGroupPermissionsPopulateGroupModel2CreateCollectionRequest(collection: CollectionPopulateCollectionProblemsPopulateProblemPopulateAccountAndTestcasesAndProblemGroupPermissionsPopulateGroupAndCollectionGroupPermissionsPopulateGroupModel): CreateCollectionRequestForm { + + return { + title: collection.name, + description: JSON.parse(String(collection.description)), + problemsInterface: collection.problems.map((cp) => ({ + id: cp.problem.problem_id, + name: cp.problem.title, + problem: cp.problem, + groupPermissions: cp.problem.group_permissions.map((pgp) => ({ + groupId: pgp.group.group_id, + group: pgp.group, + manageProblems: pgp.permission_manage_problems, + viewProblems: pgp.permission_view_problems, + })), + })), + groupPermissions: collection.group_permissions.map((cgp) => ({ + group_id: cgp.group.group_id, + group: cgp.group, + manageCollections: cgp.permission_manage_collections, + viewCollections: cgp.permission_view_collections, + })), + collection: collection, + } } \ No newline at end of file diff --git a/src/types/adapters/CreateCollectionRequestForm.adapter.ts b/src/types/adapters/CreateCollectionRequestForm.adapter.ts index f662efe..9ecea93 100644 --- a/src/types/adapters/CreateCollectionRequestForm.adapter.ts +++ b/src/types/adapters/CreateCollectionRequestForm.adapter.ts @@ -1,10 +1,43 @@ +import { CollectionGroupPermissionCreateRequest } from "../apis/Collection.api"; +import { ProblemGroupPermissionCreateRequest } from "../apis/Problem.api"; import { CreateCollectionRequestForm } from "../forms/CreateCollectionRequestForm"; import { CollectionCreateRequest } from "../models/Collection.model"; -export function transformCreateCollectionRequestForm2CreateCollectionRequestForm(createRequest:CreateCollectionRequestForm): CollectionCreateRequest { - return { - name: createRequest.title, - description: JSON.stringify(createRequest.description), - }; -} +export function transformCreateCollectionRequestForm2CreateCollectionRequestForm( + createRequest: CreateCollectionRequestForm +): { + request: CollectionCreateRequest; + problemIds: string[]; + groups: CollectionGroupPermissionCreateRequest[]; + problemGroupPermissions: { + problem_id: string; + groupPermissions: ProblemGroupPermissionCreateRequest[]; + }[]; +} { + const request = { + name: createRequest.title, + description: JSON.stringify(createRequest.description), + }; + + const problemIds = createRequest.problemsInterface.map( + (problem) => problem.id as string + ); + + const groups = createRequest.groupPermissions.map((groupPermission) => ({ + group_id: groupPermission.group_id, + permission_manage_collections: groupPermission.manageCollections, + permission_view_collections: groupPermission.viewCollections, + })); + const problemGroupPermissions = + createRequest.problemsInterface.map((cp) => ({ + problem_id: cp.problem.problem_id, + groupPermissions: cp.groupPermissions.map((gp) => ({ + group_id: gp.group.group_id, + permission_manage_problems: gp.manageProblems, + permission_view_problems: gp.viewProblems, + })), + })) ?? []; + + return { request, problemIds, groups, problemGroupPermissions }; +} diff --git a/src/types/adapters/CreateCourseRequestForm.adapter.ts b/src/types/adapters/CreateCourseRequestForm.adapter.ts index e3d49e4..aae9023 100644 --- a/src/types/adapters/CreateCourseRequestForm.adapter.ts +++ b/src/types/adapters/CreateCourseRequestForm.adapter.ts @@ -1,11 +1,44 @@ +import { CollectionGroupPermissionCreateRequest } from "../apis/Collection.api"; +import { CourseGroupPermissionCreateRequest } from "../apis/Topic.api"; import { CreateCourseRequestForm } from "../forms/CreateCourseRequestForm"; -export function transformCreateCourseRequestForm2CreateTopicRequestFormData(createRequest: CreateCourseRequestForm): FormData { - const formData = new FormData(); - formData.append("name", createRequest.title); - formData.append("description", JSON.stringify(createRequest.description)); - // formData.append("image_url", createRequest.image); - formData.append("is_private", createRequest.isPrivate ? "true" : "false"); +export function transformCreateCourseRequestForm2CreateTopicRequest( + createRequest: CreateCourseRequestForm +): { + formData: FormData; + collectionIds: string[]; + groups: CourseGroupPermissionCreateRequest[]; + collectionGroupsPermissions: { + collection_id: string; + groupPermissions: CollectionGroupPermissionCreateRequest[]; + }[] +} { + const formData = new FormData(); + formData.append("name", createRequest.title); + formData.append("description", JSON.stringify(createRequest.description)); + // formData.append("image_url", createRequest.image); + formData.append("is_private", createRequest.isPrivate ? "true" : "false"); - return formData; -} \ No newline at end of file + const collectionIds: string[] = createRequest.collectionsInterface.map( + (collection) => collection.id as string + ); + const groups = createRequest.groupPermissions.map((groupPermission) => ({ + group_id: groupPermission.group_id, + permission_manage_topics: groupPermission.manageCourses, + permission_view_topics: groupPermission.viewCourses, + permission_view_topics_log: groupPermission.viewCourseLogs, + })); + + const collectionGroupsPermissions = createRequest.collectionsInterface.map((cc) => ({ + collection_id: cc.collection.collection_id, + groupPermissions: cc.groupPermissions.map((gp) => ({ + group_id: gp.group.group_id, + permission_manage_collections: gp.manageCollections, + permission_view_collections: gp.viewCollections, + })) + })) ?? [] + + console.log("collectionGroupsPermissions", collectionGroupsPermissions) + + return { formData, collectionIds, groups, collectionGroupsPermissions }; +} diff --git a/src/types/adapters/CreateGroupRequestForm.adapter.ts b/src/types/adapters/CreateGroupRequestForm.adapter.ts index c643dc3..9af55b9 100644 --- a/src/types/adapters/CreateGroupRequestForm.adapter.ts +++ b/src/types/adapters/CreateGroupRequestForm.adapter.ts @@ -5,6 +5,13 @@ export function transformCreateGroupRequestForm2CreateGroupRequest(createRequest return { name: createRequest.name, description: createRequest.description, - color: createRequest.color + color: createRequest.color, + permission_manage_topics: createRequest.manageCourses, + permission_view_topics: createRequest.viewCourses, + permission_view_topics_log: createRequest.viewCourseLogs, + permission_manage_collections: createRequest.manageCollections, + permission_view_collections: createRequest.viewCollections, + permission_manage_problems: createRequest.manageProblems, + permission_view_problems: createRequest.viewProblems } } \ No newline at end of file diff --git a/src/types/adapters/CreateProblemRequestForm.adapter.ts b/src/types/adapters/CreateProblemRequestForm.adapter.ts index 0eb2fe4..9b12702 100644 --- a/src/types/adapters/CreateProblemRequestForm.adapter.ts +++ b/src/types/adapters/CreateProblemRequestForm.adapter.ts @@ -1,11 +1,15 @@ import { testcaseParse } from "../../utilities/TestcaseFormat"; -import { CreateProblemRequest } from "../apis/Problem.api"; +import { CreateProblemRequest, ProblemGroupPermissionCreateRequest } from "../apis/Problem.api"; import { CreateProblemRequestForm } from "../forms/CreateProblemRequestForm"; export const transformCreateProblemRequestForm2CreateProblemRequest = ( createRequest: CreateProblemRequestForm -): CreateProblemRequest => { - return { +): { + request: CreateProblemRequest + groups: ProblemGroupPermissionCreateRequest[] +} => { + + const request = { title: createRequest.title, language: createRequest.language, description: JSON.stringify(createRequest.description), @@ -15,5 +19,14 @@ export const transformCreateProblemRequestForm2CreateProblemRequest = ( createRequest.testcase_delimeter ), time_limit: createRequest.time_limit, - }; + allowed_languages: createRequest.allowedLanguage.filter((language) => language !== "").join(","), + } + + const groups = createRequest.groupPermissions.map((groupPermission) => ({ + group_id: groupPermission.groupId, + permission_manage_problems: groupPermission.manageProblems, + permission_view_problems: groupPermission.viewProblems, + })) + + return {request, groups}; }; \ No newline at end of file diff --git a/src/types/adapters/Group.adapter.ts b/src/types/adapters/Group.adapter.ts new file mode 100644 index 0000000..c58647d --- /dev/null +++ b/src/types/adapters/Group.adapter.ts @@ -0,0 +1,26 @@ +import CreateGroupForm from "../../components/Forms/CreateGroupForm"; +import { CreateGroupRequestForm } from "../forms/CreateGroupRequestForm"; +import { GroupModel, GroupPopulateGroupMemberPopulateAccountSecureModel } from "../models/Group.model"; + +export function transformGroupPopulateGroupMemberPopulateAccountSecureModel2CreateGroupRequestForm(group: GroupPopulateGroupMemberPopulateAccountSecureModel): CreateGroupRequestForm { + return { + name: group.name, + description: group.description, + color: group.color, + membersInterface: group.members.map(member => { + return { + id: member.account.account_id, + name: member.account.username, + group: member.group, + created_date: member.created_date + } + }), + manageCourses: group.permission_manage_topics, + viewCourseLogs: group.permission_view_topics_log, + viewCourses: group.permission_view_topics, + manageCollections: group.permission_manage_collections, + viewCollections: group.permission_view_collections, + manageProblems: group.permission_manage_problems, + viewProblems: group.permission_view_problems + } +} \ No newline at end of file diff --git a/src/types/adapters/Problem.adapter.ts b/src/types/adapters/Problem.adapter.ts index 3421dfd..e823493 100644 --- a/src/types/adapters/Problem.adapter.ts +++ b/src/types/adapters/Problem.adapter.ts @@ -1,7 +1,27 @@ -import { ProblemHashedTable, ProblemModel, ProblemPopulateTestcases } from "../models/Problem.model"; +import { CreateProblemRequestForm } from "../forms/CreateProblemRequestForm"; +import { ProblemHashedTable, ProblemModel, ProblemPopulateAccountAndTestcasesAndProblemGroupPermissionsPopulateGroupModel, ProblemPopulateTestcases } from "../models/Problem.model"; + +export function transformProblemPopulateAccountAndTestcasesAndProblemGroupPermissionsPopulateGroupModel2CreateProblemRequestForm(problem: ProblemPopulateAccountAndTestcasesAndProblemGroupPermissionsPopulateGroupModel): CreateProblemRequestForm { + return { + title: problem.title, + description: JSON.parse(String(problem.description)), + language: problem.language, + solution: problem.solution, + testcases: problem.testcases.map(testcase => testcase.input).join(":::\n"), + testcase_delimeter: ":::", + time_limit: problem.time_limit, + groupPermissions: problem.group_permissions.map((permission) => ({ + groupId: permission.group.group_id, + group: permission.group, + manageProblems: permission.permission_manage_problems, + viewProblems: permission.permission_view_problems + })), + allowedLanguage: problem.allowed_languages.split(",") + } +} export function transformProblemModel2ProblemHashedTable(problems: ProblemModel[] | ProblemPopulateTestcases[]): ProblemHashedTable { - let result = [] + let result:ProblemHashedTable = {} for (const problem of problems) { result[problem.problem_id] = problem } diff --git a/src/types/adapters/Topic.adapter.ts b/src/types/adapters/Topic.adapter.ts new file mode 100644 index 0000000..b840286 --- /dev/null +++ b/src/types/adapters/Topic.adapter.ts @@ -0,0 +1,40 @@ +import { CreateCourseRequestForm } from "../forms/CreateCourseRequestForm"; +import { TopicPopulateTopicCollectionPopulateCollectionAndTopicGroupPermissionPopulateGroupModel, TopicPopulateTopicCollectionPopulateCollectionPopulateCollectionProblemsPopulateProblemAndCollectionGroupPermissionsPopulateGroupAndTopicGroupPermissionPopulateGroupModel } from "../models/Topic.model"; + +export function transformTopicPopulateTopicCollectionPopulateCollectionAndTopicGroupPermissionPopulateGroupModel2CreateCourseRequest(topic:TopicPopulateTopicCollectionPopulateCollectionPopulateCollectionProblemsPopulateProblemAndCollectionGroupPermissionsPopulateGroupAndTopicGroupPermissionPopulateGroupModel): CreateCourseRequestForm { + return { + title: topic.name, + description: JSON.parse(String(topic.description)), + image: topic.image_url, + isPrivate: topic.is_private, + collectionsInterface: topic.collections.map((tc) => ({ + id: tc.collection.collection_id, + name: tc.collection.name, + collection: tc.collection, + groupPermissions: tc.collection.group_permissions.map((gp) => ({ + group_id: gp.group.group_id, + group: gp.group, + manageCollections: gp.permission_manage_collections, + viewCollections: gp.permission_view_collections, + })), + })), + groupPermissions: topic.group_permissions.map((gp) => ({ + group_id: gp.group.group_id, + group: gp.group, + manageCourses: gp.permission_manage_topics, + viewCourses: gp.permission_view_topics, + viewCourseLogs: gp.permission_view_topics_log, + })), + course: topic, + // collectionGroupPermissions: topic.collections.map((tc) => ({ + // collection_id: tc.collection.collection_id, + // collection: tc.collection, + // groupPermissions: tc.collection.group_permissions.map((gp) => ({ + // group_id: gp.group.group_id, + // group: gp.group, + // manageCollections: gp.permission_manage_collections, + // viewCollections: gp.permission_view_collections, + // })) + // })) + } +} \ No newline at end of file diff --git a/src/types/apis/Account.api.ts b/src/types/apis/Account.api.ts index 54f8be2..fe9f2cc 100644 --- a/src/types/apis/Account.api.ts +++ b/src/types/apis/Account.api.ts @@ -11,5 +11,5 @@ export type AccountCreateRequest = { export type AccountServiceAPI = { create: (request: AccountCreateRequest) => Promise>; getAll: () => Promise>; - get: (accountId:number) => Promise>; + get: (accountId:string) => Promise>; } \ No newline at end of file diff --git a/src/types/apis/Auth.api.ts b/src/types/apis/Auth.api.ts index 91afc74..eda6c46 100644 --- a/src/types/apis/Auth.api.ts +++ b/src/types/apis/Auth.api.ts @@ -9,12 +9,12 @@ export type LoginRequest = { } export type LogoutRequest = { - account_id: number; + account_id: string; token: string; } export type AuthorizationRequest = { - account_id: number; + account_id: string; token: string; } diff --git a/src/types/apis/Collection.api.ts b/src/types/apis/Collection.api.ts index 8e0c38f..1b7c088 100644 --- a/src/types/apis/Collection.api.ts +++ b/src/types/apis/Collection.api.ts @@ -1,12 +1,26 @@ import { AxiosResponse } from "axios" -import { CollectionCreateRequest, CollectionModel, CollectionPopulateProblemSecureModel, CollectionProblemModel, CollectionUpdateRequest, GetCollectionByAccountResponse } from "../models/Collection.model" +import { CollectionCreateRequest, CollectionModel, CollectionPopulateCollectionProblemPopulateProblemModel, CollectionPopulateCollectionProblemsPopulateProblemAndCollectionGroupPermissionsPopulateGroupModel, CollectionPopulateCollectionProblemsPopulateProblemPopulateAccountAndTestcasesAndProblemGroupPermissionsPopulateGroupAndCollectionGroupPermissionsPopulateGroupModel, CollectionPopulateProblemSecureModel, CollectionProblemModel, CollectionUpdateRequest } from "../models/Collection.model" + +export type GetCollectionByAccountResponse = { + collections: CollectionPopulateCollectionProblemPopulateProblemModel[]; + manageable_collections: CollectionPopulateCollectionProblemPopulateProblemModel[]; + +} + +export type CollectionGroupPermissionCreateRequest = { + group_id: string; + permission_manage_collections?: boolean + permission_view_collections?: boolean +} export type CollectionServiceAPI = { - create: (accountId:number,request:CollectionCreateRequest) => Promise>; - get: (collectionId:number) => Promise>; - getAllByAccount: (accountId:number) => Promise>; - update: (collectionId:number,request:CollectionUpdateRequest) => Promise>; - addProblem: (collectionId:number,problemIds:number[]) => Promise>; - removeProblem: (collectionId:number,problemIds:number[]) => Promise>; - updateProblem: (collectionId:number,problemIds:number[]) => Promise>; + create: (accountId:string,request:CollectionCreateRequest) => Promise>; + get: (collectionId:string,accountId:string) => Promise>; + update: (collectionId:string,accountId:string,request:CollectionUpdateRequest) => Promise>; + delete: (collectionId:string,accountId:string) => Promise>; + getAllAsCreator: (accountId:string) => Promise>; + addProblem: (collectionId:string,problemIds:string[]) => Promise>; + removeProblem: (collectionId:string,problemIds:string[]) => Promise>; + updateProblem: (collectionId:string,problemIds:string[]) => Promise>; + updateGroupPermissions: (collectionId:string,accountId:string,groups:CollectionGroupPermissionCreateRequest[]) => Promise>; } \ No newline at end of file diff --git a/src/types/apis/Group.api.ts b/src/types/apis/Group.api.ts index 6bc39c5..7e37af9 100644 --- a/src/types/apis/Group.api.ts +++ b/src/types/apis/Group.api.ts @@ -5,9 +5,16 @@ export type GroupCreateRequest = { name: string; description?: string | null; color?: string | null; + permission_manage_topics?: boolean + permission_view_topics?: boolean + permission_view_topics_log?: boolean + permission_manage_collections?: boolean + permission_view_collections?: boolean + permission_manage_problems?: boolean + permission_view_problems?: boolean } -export type GroupGetAllByAccountResponse = { +export type GroupgetAllAsCreatorResponse = { groups: GroupModel[] | GroupPopulateGroupMemberPopulateAccountSecureModel[]; } @@ -16,10 +23,10 @@ export type GroupGetQuery = { } export type GroupSerivceAPI = { - get: (groupId:number,query?:GroupGetQuery) => Promise>; - getAllByAccount: (accountId:number,query?:GroupGetQuery) => Promise>; - create: (accountId:number,request:GroupCreateRequest) => Promise>; - update: (groupId:number,request:GroupCreateRequest) => Promise>; - delete: (groupId:number) => Promise>; - updateMembers: (groupId:number,accountIds:number[]) => Promise>; + get: (groupId:string,query?:GroupGetQuery) => Promise>; + getAllAsCreator: (accountId:string,query?:GroupGetQuery) => Promise>; + create: (accountId:string,request:GroupCreateRequest) => Promise>; + update: (groupId:string,request:GroupCreateRequest) => Promise>; + delete: (groupId:string) => Promise>; + updateMembers: (groupId:string,accountIds:string[]) => Promise>; } \ No newline at end of file diff --git a/src/types/apis/Problem.api.ts b/src/types/apis/Problem.api.ts index cf746e4..6076ce8 100644 --- a/src/types/apis/Problem.api.ts +++ b/src/types/apis/Problem.api.ts @@ -1,61 +1,107 @@ import { AxiosResponse } from "axios"; import { ErrorResponse } from "./ErrorHandling"; -import { ProblemModel, ProblemPoplulateCreatorModel, ProblemPopulateAccountAndSubmissionPopulateSubmissionTestcasesSecureModel, ProblemPopulateTestcases } from "../models/Problem.model"; +import { + ProblemModel, + ProblemPoplulateCreatorModel, + ProblemPopulateAccountAndSubmissionPopulateSubmissionTestcasesSecureModel, + ProblemPopulateAccountAndTestcasesAndProblemGroupPermissionsPopulateGroupModel, + ProblemPopulateCreatorSecureModel, + ProblemPopulateTestcases, + ProblemSecureModel, + TestcaseModel, +} from "../models/Problem.model"; +import { AccountModel } from "../models/Account.model"; +import { GroupModel, ProblemGroupPermissionModel } from "../models/Group.model"; export type CreateProblemRequest = { - title: string; - language: string; - description: string | null; - solution: string; - testcases: string[]; - time_limit: number; -} + title: string; + language: string; + description: string | null; + solution: string; + testcases: string[]; + time_limit: number; + allowed_languages: string; +}; export type UpdateProblemRequest = { - title?: string; - language?: string; - description?: string; - solution?: string; - testcases?: string[]; - time_limit?: number; -} + title?: string; + language?: string; + description?: string; + solution?: string; + testcases?: string[]; + time_limit?: number; +}; export type GetAllProblemsByAccountResponse = { - problems: ProblemPopulateTestcases[]; -} + problems: ProblemPopulateTestcases[]; + manageable_problems: ProblemPopulateTestcases[]; +}; export type ValidateProgramRequest = { - language: string; - source_code: string; - testcases: string[]; - time_limited: number; -} + language: string; + source_code: string; + testcases: string[]; + time_limited: number; +}; export type RuntimeResult = { - input: string; - output: string | null; - runtime_status: string; -} + input: string; + output: string | null; + runtime_status: string; +}; export type ValidateProgramResponse = { - runnable: boolean; - has_error: boolean; - has_timeout: boolean; - runtime_results: RuntimeResult[]; -} + runnable: boolean; + has_error: boolean; + has_timeout: boolean; + runtime_results: RuntimeResult[]; +}; export type GetAllProblemsResponse = { - problems: ProblemPopulateAccountAndSubmissionPopulateSubmissionTestcasesSecureModel[] -} + problems: ProblemPopulateAccountAndSubmissionPopulateSubmissionTestcasesSecureModel[]; +}; + +export type ProblemGroupPermissionCreateRequest = { + group_id: string; + permission_manage_problems?: boolean; + permission_view_problems?: boolean; +}; + +export type ProblemGroupPermissionPopulateGroupModel = + ProblemGroupPermissionModel & { + group: GroupModel; + }; export type ProblemServiceAPI = { - create: (accountId:number,request: CreateProblemRequest) => Promise>; - getAll: () => Promise>; - getAllByAccount: (accountId:number) => Promise>; - get: (problemId:number) => Promise>; - update: (problemId:number, request: UpdateProblemRequest | CreateProblemRequest) => Promise>; - // deleteMultiple: (problemIds:number[]) => Promise>; - delete: (problemId:number) => Promise>; - - validateProgram: (request: ValidateProgramRequest) => Promise>; -} \ No newline at end of file + create: ( + accountId: string, + request: CreateProblemRequest + ) => Promise>; + getAll: () => Promise>; + getAllAsCreator: ( + accountId: string + ) => Promise>; + get: ( + accountId: string, + problemId: string + ) => Promise< + AxiosResponse + >; + update: ( + problemId: string, + request: UpdateProblemRequest | CreateProblemRequest + ) => Promise>; + // deleteMultiple: (problemIds:string[]) => Promise>; + delete: (problemId: string) => Promise>; + updateGroupPermissions: ( + problemId: string, + accountId: string, + groups: ProblemGroupPermissionCreateRequest[] + ) => Promise< + AxiosResponse + >; + validateProgram: ( + request: ValidateProgramRequest + ) => Promise>; + getPublic: (problemId: string) => Promise>; +}; diff --git a/src/types/apis/Submission.api.ts b/src/types/apis/Submission.api.ts index ec9e0ca..1c75ea9 100644 --- a/src/types/apis/Submission.api.ts +++ b/src/types/apis/Submission.api.ts @@ -8,7 +8,7 @@ export type SubmitProblemRequest = { } export type SubmitProblemResponse = { - submission_id: number + submission_id: string problem: ProblemPopulateAccountSecureModel language: string submission_code: string @@ -22,8 +22,8 @@ export type SubmitProblemResponse = { } export type GetAllSubmissionsQuery = { - problem_id?: number - account_id?: number + problem_id?: string + account_id?: string passed?: number sort_date?: number sort_score?: number @@ -36,9 +36,9 @@ export type GetAllSubmissionsResponse = { } export type SubmissionServiceAPI = { - submit: (accountId:number,problemId:number,request: SubmitProblemRequest) => Promise>; - topicSubmit: (accountId:number,topicId:number,problemId:number,request: SubmitProblemRequest) => Promise>; - getByAccountProblem: (accountId:number,problemId:number) => Promise>; - getByAccountProblemInTopic: (accountId:number,problemId:number,topicId:number) => Promise>; + submit: (accountId:string,problemId:string,request: SubmitProblemRequest) => Promise>; + topicSubmit: (accountId:string,topicId:string,problemId:string,request: SubmitProblemRequest) => Promise>; + getByAccountProblem: (accountId:string,problemId:string) => Promise>; + getByAccountProblemInTopic: (accountId:string,problemId:string,topicId:string) => Promise>; getAll: (query?:GetAllSubmissionsQuery) => Promise>; } \ No newline at end of file diff --git a/src/types/apis/Topic.api.ts b/src/types/apis/Topic.api.ts index b9e75d0..795c831 100644 --- a/src/types/apis/Topic.api.ts +++ b/src/types/apis/Topic.api.ts @@ -1,15 +1,30 @@ import { AxiosResponse } from "axios"; -import { TopicModel, TopicPopulateTopicCollectionPopulateCollectionModel, TopicPopulateTopicCollectionPopulateCollectionPopulateCollectionProblemPopulateProblemPopulateAccountAndSubmissionPopulateSubmissionTestcasesSecureModel } from "../models/Topic.model"; +import { TopicModel, TopicPopulateTopicCollectionPopulateCollectionAndTopicGroupPermissionPopulateGroupModel, TopicPopulateTopicCollectionPopulateCollectionModel, TopicPopulateTopicCollectionPopulateCollectionPopulateCollectionProblemPopulateProblemAndTopicGroupPermissionPopulateGroupModel, TopicPopulateTopicCollectionPopulateCollectionPopulateCollectionProblemPopulateProblemPopulateAccountAndSubmissionPopulateSubmissionTestcasesSecureModel, TopicPopulateTopicCollectionPopulateCollectionPopulateCollectionProblemsPopulateProblemAndCollectionGroupPermissionsPopulateGroupAndTopicGroupPermissionPopulateGroupModel } from "../models/Topic.model"; export type GetAllTopicsByAccountResponse = { topics: TopicPopulateTopicCollectionPopulateCollectionModel[]; + manageable_topics: TopicPopulateTopicCollectionPopulateCollectionModel[]; +} + +export type GetAllTopicsByAccessibleAccountResponse = { + topics: TopicModel[]; +} + +export type CourseGroupPermissionCreateRequest = { + group_id: string; + permission_manage_topics?: boolean + permission_view_topics?: boolean + permission_view_topics_log?: boolean } export type TopicSerivceAPI = { - create: (accountId: number,request: FormData) => Promise>; - get: (accountId: number,courseId:number) => Promise>; - update: (courseId:number,request: FormData) => Promise>; - getAllByAccount: (accountId:number) => Promise>; - updateCollections: (topicId:number,collectionIds:number[]) => Promise>; - getPublicByAccount: (accountId:number,courseId:number) => Promise>; + create: (accountid: string,request: FormData) => Promise>; + get: (accountId: string,courseId:string) => Promise>; + getAllAsCreator: (accountId:string) => Promise>; + getAllAccessibleByAccount: (accountId:string) => Promise>; + getPublicByAccount: (accountId:string,courseId:string) => Promise>; + update: (courseId:string,accountId:string,request: FormData) => Promise>; + delete: (courseId:string,accountId:string) => Promise>; + updateCollections: (topicId:string,collectionIds:string[]) => Promise>; + updateGroupPermissions: (topicId:string,accountId:string,groups:CourseGroupPermissionCreateRequest[]) => Promise>; } \ No newline at end of file diff --git a/src/types/forms/CreateCollectionRequestForm.ts b/src/types/forms/CreateCollectionRequestForm.ts index 74300ae..69aa1fd 100644 --- a/src/types/forms/CreateCollectionRequestForm.ts +++ b/src/types/forms/CreateCollectionRequestForm.ts @@ -1,9 +1,25 @@ import { ItemInterface } from "react-sortablejs"; -import { CollectionProblemPopulateProblemSecureModel } from "../models/Collection.model"; +import { CollectionPopulateCollectionProblemsPopulateProblemAndCollectionGroupPermissionsPopulateGroupModel, CollectionPopulateCollectionProblemsPopulateProblemPopulateAccountAndTestcasesAndProblemGroupPermissionsPopulateGroupAndCollectionGroupPermissionsPopulateGroupModel, CollectionProblemPopulateProblemSecureModel } from "../models/Collection.model"; import { PlateEditorValueType } from "../PlateEditorValueType"; +import { GroupModel } from "../models/Group.model"; +import { CollectionPermissionRequestForm } from "./CreateGroupRequestForm"; +import { ProblemGroupPermissionRequestForm } from "./CreateProblemRequestForm"; +import { ProblemPopulateTestcases } from "../models/Problem.model"; + +export type CollectionGroupPermissionRequestForm = { + group_id: string; + group: GroupModel; +} & CollectionPermissionRequestForm + +export type ProblemItemInterface = ItemInterface & { + problem: ProblemPopulateTestcases; + groupPermissions: ProblemGroupPermissionRequestForm[]; +} export type CreateCollectionRequestForm = { title: string; description: PlateEditorValueType; - problemsInterface: ItemInterface[]; + problemsInterface: ProblemItemInterface[]; + groupPermissions: CollectionGroupPermissionRequestForm[]; + collection: null | CollectionPopulateCollectionProblemsPopulateProblemPopulateAccountAndTestcasesAndProblemGroupPermissionsPopulateGroupAndCollectionGroupPermissionsPopulateGroupModel; } \ No newline at end of file diff --git a/src/types/forms/CreateCourseRequestForm.ts b/src/types/forms/CreateCourseRequestForm.ts index 68531ed..523686f 100644 --- a/src/types/forms/CreateCourseRequestForm.ts +++ b/src/types/forms/CreateCourseRequestForm.ts @@ -1,10 +1,34 @@ import { ItemInterface } from "react-sortablejs"; import { PlateEditorValueType } from "../PlateEditorValueType"; +import { CoursePermissionRequestForm } from "./CreateGroupRequestForm"; +import { GroupModel, TopicGroupPermissionPopulateGroupModel } from "../models/Group.model"; +import { TopicPopulateTopicCollectionPopulateCollectionAndTopicGroupPermissionPopulateGroupModel, TopicPopulateTopicCollectionPopulateCollectionPopulateCollectionProblemPopulateProblemAndTopicGroupPermissionPopulateGroupModel, TopicPopulateTopicCollectionPopulateCollectionPopulateCollectionProblemsPopulateProblemAndCollectionGroupPermissionsPopulateGroupAndTopicGroupPermissionPopulateGroupModel } from "../models/Topic.model"; +import { CollectionGroupPermissionRequestForm } from "./CreateCollectionRequestForm"; +import { CollectionModel, CollectionPopulateCollectionProblemPopulateProblemModel, CollectionPopulateCollectionProblemsPopulateProblemAndCollectionGroupPermissionsPopulateGroupModel } from "../models/Collection.model"; + +export type CourseGroupPermissionRequestForm = { + group_id: string; + group: GroupModel; +} & CoursePermissionRequestForm + +export type CourseCollectionsGroupPermissionRequestForm = { + collection_id: string; + collection: CollectionPopulateCollectionProblemsPopulateProblemAndCollectionGroupPermissionsPopulateGroupModel; + groupPermissions: CollectionGroupPermissionRequestForm[]; +} + +export type CollectionItemInterface = ItemInterface & { + collection: CollectionPopulateCollectionProblemPopulateProblemModel; + groupPermissions: CollectionGroupPermissionRequestForm[]; +} export type CreateCourseRequestForm = { title: string; description: PlateEditorValueType; - image?: File | null; + image?: File | string | null; isPrivate?: boolean; - collectionsInterface: ItemInterface[]; -} \ No newline at end of file + collectionsInterface: CollectionItemInterface[] //ItemInterface[]; + groupPermissions: CourseGroupPermissionRequestForm[]; + course: null | TopicPopulateTopicCollectionPopulateCollectionPopulateCollectionProblemsPopulateProblemAndCollectionGroupPermissionsPopulateGroupAndTopicGroupPermissionPopulateGroupModel //| TopicPopulateTopicCollectionPopulateCollectionPopulateCollectionProblemPopulateProblemAndTopicGroupPermissionPopulateGroupModel | TopicPopulateTopicCollectionPopulateCollectionAndTopicGroupPermissionPopulateGroupModel | null; + // collectionGroupPermissions: CourseCollectionsGroupPermissionRequestForm[]; +} diff --git a/src/types/forms/CreateGroupRequestForm.ts b/src/types/forms/CreateGroupRequestForm.ts index de76c5f..28b669e 100644 --- a/src/types/forms/CreateGroupRequestForm.ts +++ b/src/types/forms/CreateGroupRequestForm.ts @@ -1,9 +1,25 @@ import { ItemInterface } from "react-sortablejs"; import { AccountSecureModel } from "../models/Account.model"; +export type CoursePermissionRequestForm = { + manageCourses: boolean; + viewCourseLogs: boolean; + viewCourses: boolean; +} + +export type CollectionPermissionRequestForm = { + manageCollections: boolean; + viewCollections: boolean; +} + +export type ProblemPermissionRequestForm = { + manageProblems: boolean; + viewProblems: boolean; +} + export type CreateGroupRequestForm = { name: string; description: string | null; color: string | null; membersInterface: ItemInterface[]; -} \ No newline at end of file +} & CoursePermissionRequestForm & CollectionPermissionRequestForm & ProblemPermissionRequestForm \ No newline at end of file diff --git a/src/types/forms/CreateProblemRequestForm.ts b/src/types/forms/CreateProblemRequestForm.ts index 955cbad..273d197 100644 --- a/src/types/forms/CreateProblemRequestForm.ts +++ b/src/types/forms/CreateProblemRequestForm.ts @@ -1,5 +1,12 @@ import { PlateEditorValueType } from "../PlateEditorValueType"; +import { GroupModel } from "../models/Group.model"; import { TestcaseModel } from "../models/Problem.model"; +import { ProblemPermissionRequestForm } from "./CreateGroupRequestForm"; + +export type ProblemGroupPermissionRequestForm = { + groupId: string; + group: GroupModel +} & ProblemPermissionRequestForm export type CreateProblemRequestForm = { title: string; @@ -10,4 +17,6 @@ export type CreateProblemRequestForm = { testcase_delimeter: string; time_limit: number; validated_testcases?: TestcaseModel[]; + groupPermissions: ProblemGroupPermissionRequestForm[]; + allowedLanguage: string[]; }; \ No newline at end of file diff --git a/src/types/models/Account.model.ts b/src/types/models/Account.model.ts index 969fbc1..e943acb 100644 --- a/src/types/models/Account.model.ts +++ b/src/types/models/Account.model.ts @@ -1,5 +1,5 @@ export type AccountModel = { - account_id: number + account_id: string email: string username: string password: string @@ -10,10 +10,10 @@ export type AccountModel = { } export type AccountSecureModel = { - account_id: number + account_id: string username: string } export type AccountHashedTable = { - [id:number]: AccountModel | AccountSecureModel + [id:string]: AccountModel | AccountSecureModel } \ No newline at end of file diff --git a/src/types/models/Collection.model.ts b/src/types/models/Collection.model.ts index 8543bfa..f8a2ccf 100644 --- a/src/types/models/Collection.model.ts +++ b/src/types/models/Collection.model.ts @@ -1,56 +1,104 @@ -import { ProblemModel, ProblemPopulateAccountAndSubmissionPopulateSubmissionTestcasesSecureModel, ProblemSecureModel } from "./Problem.model"; +import { GroupModel } from "./Group.model"; +import { + ProblemModel, + ProblemPopulateAccountAndSubmissionPopulateSubmissionTestcasesSecureModel, + ProblemPopulateAccountAndTestcasesAndProblemGroupPermissionsPopulateGroupModel, + ProblemSecureModel, +} from "./Problem.model"; export type CollectionModel = { - collection_id: number; - creator: number; - name: string; - description: string | null; - is_active: boolean; - is_private: boolean; - created_date: string; - updated_date: string; -} + collection_id: string; + creator: string; + name: string; + description: string | null; + is_active: boolean; + is_private: boolean; + created_date: string; + updated_date: string; +}; export type CollectionPopulateProblemSecureModel = CollectionModel & { - problems: ProblemSecureModel[]; -} + problems: ProblemSecureModel[]; +}; export type CollectionProblemModel = CollectionModel & { - problems: CollectionProblemPopulateProblemSecureModel[]; -} + problems: CollectionProblemPopulateProblemSecureModel[]; +}; export type CollectionCreateRequest = { - name: string; - description?: string; -} + name: string; + description?: string; +}; export type CollectionUpdateRequest = { - name?: string; - description?: string; -} + name?: string; + description?: string; +}; -export type CollectionProblemPopulateProblemSecureModel = { - id: number; - problem: ProblemSecureModel; - order: number; - collection: number; -} +export type CollectionProblemPopulateProblemModel = { + id: string; + problem: ProblemModel; + order: number; + collection: number; +}; -export type GetCollectionByAccountResponse = { - collections: CollectionProblemModel[]; -} +export type CollectionProblemPopulateProblemSecureModel = { + id: string; + problem: ProblemSecureModel; + order: number; + collection: number; +}; export type CollectionHashedTable = { - [id:number]: CollectionModel | CollectionPopulateProblemSecureModel -} - -export type CollectionProblemPopulateProblemPopulateAccountAndSubmissionPopulateSubmissionTestcasesSecureModel = { - id: number; - problem: ProblemPopulateAccountAndSubmissionPopulateSubmissionTestcasesSecureModel; - order: number; - collection: number; -} - -export type CollectionPopulateCollectionProblemPopulateProblemPopulateAccountAndSubmissionPopulateSubmissionTestcasesSecureModel = CollectionModel & { - problems: CollectionProblemPopulateProblemPopulateAccountAndSubmissionPopulateSubmissionTestcasesSecureModel[] -} \ No newline at end of file + [ + collection_id: string + ]: CollectionPopulateCollectionProblemPopulateProblemModel; +}; + +export type CollectionProblemPopulateProblemPopulateAccountAndSubmissionPopulateSubmissionTestcasesSecureModel = + { + id: string; + problem: ProblemPopulateAccountAndSubmissionPopulateSubmissionTestcasesSecureModel; + order: number; + collection: number; + }; + +export type CollectionPopulateCollectionProblemPopulateProblemPopulateAccountAndSubmissionPopulateSubmissionTestcasesSecureModel = + CollectionModel & { + problems: CollectionProblemPopulateProblemPopulateAccountAndSubmissionPopulateSubmissionTestcasesSecureModel[]; + }; + +export type CollectionPopulateCollectionProblemPopulateProblemModel = + CollectionModel & { + problems: CollectionProblemPopulateProblemModel[]; + }; + +export type CollectionGroupPermissionModel = { + collection_group_permission_id: string; + group: string; + permission_manage_collections: boolean; + permission_view_collections: boolean; + collection: string; +}; + +export type CollectionGroupPermissionPopulateGroupModel = + CollectionGroupPermissionModel & { + group: GroupModel; + }; + +export type CollectionPopulateCollectionProblemsPopulateProblemAndCollectionGroupPermissionsPopulateGroupModel = + CollectionModel & { + problems: CollectionProblemPopulateProblemModel[]; + group_permissions: CollectionGroupPermissionPopulateGroupModel[]; + }; + +export type CollectionProblemsPopulateProblemPopulateAccountAndTestcasesAndProblemGroupPermissionsPopulateGroupModel = + CollectionProblemModel & { + problem: ProblemPopulateAccountAndTestcasesAndProblemGroupPermissionsPopulateGroupModel; + }; + +export type CollectionPopulateCollectionProblemsPopulateProblemPopulateAccountAndTestcasesAndProblemGroupPermissionsPopulateGroupAndCollectionGroupPermissionsPopulateGroupModel = + CollectionModel & { + group_permissions: CollectionGroupPermissionPopulateGroupModel[]; + problems: CollectionProblemsPopulateProblemPopulateAccountAndTestcasesAndProblemGroupPermissionsPopulateGroupModel[]; + }; diff --git a/src/types/models/Group.model.ts b/src/types/models/Group.model.ts index 39bbcd0..8085037 100644 --- a/src/types/models/Group.model.ts +++ b/src/types/models/Group.model.ts @@ -1,17 +1,24 @@ import { AccountSecureModel } from "./Account.model"; export type GroupModel = { - group_id: number; - creator: number; + group_id: string; + creator: string; name: string; description: string | null; color: string; created_date: string; updated_date: string; + permission_manage_topics: boolean; + permission_view_topics: boolean; + permission_view_topics_log: boolean; + permission_manage_collections: boolean; + permission_view_collections: boolean; + permission_manage_problems: boolean; + permission_view_problems: boolean; } export type GroupMemberModel = { - group_member_id: number; + group_member_id: string; group: number; account: number; created_date: string; @@ -26,5 +33,26 @@ export type GroupPopulateGroupMemberPopulateAccountSecureModel = GroupModel & { } export type GroupHashedTable = { - [id:number]: GroupModel | GroupPopulateGroupMemberPopulateAccountSecureModel + [id:string]: GroupModel | GroupPopulateGroupMemberPopulateAccountSecureModel +} + +export type TopicGroupPermissionModel = { + topic_group_permission_id: string + group: string + permission_manage_topics: boolean + permission_view_topics: boolean + permission_view_topics_log: boolean + topic: string +} + +export type ProblemGroupPermissionModel = { + problem_group_permission_id: string + group: string + permission_manage_problems: boolean + permission_view_problems: boolean + problem: string +} + +export type TopicGroupPermissionPopulateGroupModel = TopicGroupPermissionModel & { + group: GroupModel } \ No newline at end of file diff --git a/src/types/models/Problem.model.ts b/src/types/models/Problem.model.ts index 3943e22..db63125 100644 --- a/src/types/models/Problem.model.ts +++ b/src/types/models/Problem.model.ts @@ -1,8 +1,9 @@ -import { AccountSecureModel } from "./Account.model" +import { ProblemGroupPermissionPopulateGroupModel } from "../apis/Problem.api" +import { AccountModel, AccountSecureModel } from "./Account.model" import { SubmissionPopulateSubmissionTestcasesSecureModel } from "./Submission.model" export type TestcaseModel = { - testcase_id: number + testcase_id: string input: string output: string | null problem: number @@ -10,7 +11,7 @@ export type TestcaseModel = { } export type ProblemModel = { - problem_id: number + problem_id: string language: string title: string description: string | null @@ -19,19 +20,27 @@ export type ProblemModel = { is_active: boolean is_private: boolean submission_regex: string - creator: number + creator: string testcases: TestcaseModel[] created_date: string; updated_date: string; + allowed_languages: string } export type ProblemSecureModel = { - problem_id: number + problem_id: string + language: string title: string description: string + time_limit: string + created_date: string + updated_date: string + allowed_languages: string + creator: string +} + +export type ProblemPopulateCreatorSecureModel = ProblemSecureModel & { creator: AccountSecureModel - created_date: string; - updated_date: string; } export type ProblemPoplulateCreatorModel = ProblemModel & { @@ -39,7 +48,7 @@ export type ProblemPoplulateCreatorModel = ProblemModel & { } export type ProblemPopulateAccountSecureModel = { - problem_id: number + problem_id: string title: string description: string | null creator: AccountSecureModel @@ -55,11 +64,18 @@ export type ProblemPopulateTestcases = ProblemModel & { } export type ProblemHashedTable = { - [id:number]: ProblemModel | ProblemPopulateTestcases + [id:string]: ProblemModel | ProblemPopulateTestcases | ProblemPopulateAccountAndTestcasesAndProblemGroupPermissionsPopulateGroupModel } export type ProblemHealth = { has_source_code: boolean testcase_count: number no_runtime_error: boolean -} \ No newline at end of file +} + +export type ProblemPopulateAccountAndTestcasesAndProblemGroupPermissionsPopulateGroupModel = + ProblemModel & { + creator: AccountModel; + testcases: TestcaseModel[]; + group_permissions: ProblemGroupPermissionPopulateGroupModel[]; + }; \ No newline at end of file diff --git a/src/types/models/Submission.model.ts b/src/types/models/Submission.model.ts index 8436597..0693779 100644 --- a/src/types/models/Submission.model.ts +++ b/src/types/models/Submission.model.ts @@ -1,7 +1,7 @@ import { ProblemModel, ProblemSecureModel } from "./Problem.model"; export type SubmissionTestcaseModel = { - submission_testcase_id: number; + submission_testcase_id: string; submission: number; testcase: number; output: string; @@ -15,7 +15,7 @@ export type SubmissionTestcaseSecureModel = { }; export type SubmissionModel = { - submission_id: number; + submission_id: string; problem: number; topic: number | null; language: string; @@ -29,7 +29,7 @@ export type SubmissionModel = { }; export type SubmissionPopulateProblemModel = { - submission_id: number; + submission_id: string; account: number; problem: ProblemModel; language: string; @@ -43,7 +43,7 @@ export type SubmissionPopulateProblemModel = { } export type SubmissionPoplulateProblemSecureModel = { - submission_id: number; + submission_id: string; problem: ProblemModel; submission_code: string; is_passed: boolean; @@ -55,7 +55,7 @@ export type SubmissionPoplulateProblemSecureModel = { } export type SubmissionPopulateSubmissionTestcaseSecureModel = { - submission_id: number + submission_id: string problem: number language: string submission_code: string @@ -69,8 +69,8 @@ export type SubmissionPopulateSubmissionTestcaseSecureModel = { } export type GetSubmissionByAccountProblemSubmissionModel = { - submission_id: number - problem: number + submission_id: string + problem: string language: string submission_code: string is_passed: boolean @@ -88,7 +88,7 @@ export type GetSubmissionByAccountProblemResponse = { } export type SubmissionPopulateSubmissionTestcasesSecureModel = { - submission_id: number + submission_id: string problem: number language: string submission_code: string diff --git a/src/types/models/Topic.model.ts b/src/types/models/Topic.model.ts index 37b1219..f85d09b 100644 --- a/src/types/models/Topic.model.ts +++ b/src/types/models/Topic.model.ts @@ -1,8 +1,9 @@ -import { CollectionModel, CollectionPopulateCollectionProblemPopulateProblemPopulateAccountAndSubmissionPopulateSubmissionTestcasesSecureModel } from "./Collection.model" +import { CollectionModel, CollectionPopulateCollectionProblemPopulateProblemModel, CollectionPopulateCollectionProblemPopulateProblemPopulateAccountAndSubmissionPopulateSubmissionTestcasesSecureModel, CollectionPopulateCollectionProblemsPopulateProblemAndCollectionGroupPermissionsPopulateGroupModel } from "./Collection.model" +import { TopicGroupPermissionPopulateGroupModel } from "./Group.model" export type TopicModel = { - topic_id: number - creator: number + topic_id: string + creator: string name: string description: string | null image_url: string | null @@ -13,8 +14,8 @@ export type TopicModel = { } export type TopicSecureModel = { - topic_id: number - creator: number + topic_id: string + creator: string name: string description: string | null image_url: string | null @@ -28,7 +29,7 @@ export type TopicCollectionModel = { } export type TopicCollectionPopulateCollectionModel = { - id: number; + id: string; collection: CollectionModel; order: number; topic: number; @@ -39,7 +40,7 @@ export type TopicPopulateTopicCollectionPopulateCollectionModel = TopicModel & { } export type TopicCollectionPopulateCollectionPopulateCollectionProblemPopulateProblemPopulateAccountAndSubmissionPopulateSubmissionTestcasesSecureModel = { - id: number; + id: string; collection: CollectionPopulateCollectionProblemPopulateProblemPopulateAccountAndSubmissionPopulateSubmissionTestcasesSecureModel; order: number; topic: number; @@ -47,4 +48,30 @@ export type TopicCollectionPopulateCollectionPopulateCollectionProblemPopulatePr export type TopicPopulateTopicCollectionPopulateCollectionPopulateCollectionProblemPopulateProblemPopulateAccountAndSubmissionPopulateSubmissionTestcasesSecureModel = TopicSecureModel & { collections: TopicCollectionPopulateCollectionPopulateCollectionProblemPopulateProblemPopulateAccountAndSubmissionPopulateSubmissionTestcasesSecureModel[] +} + +export type TopicPopulateTopicCollectionPopulateCollectionAndTopicGroupPermissionPopulateGroupModel = TopicModel & { + collections: TopicCollectionPopulateCollectionModel[] + group_permissions: TopicGroupPermissionPopulateGroupModel[] +} + +export type TopicCollectionPopulateCollectionPopulateCollectionProblemPopulateProblemModel = TopicCollectionPopulateCollectionModel & { + collection: CollectionPopulateCollectionProblemPopulateProblemModel +} + +export type TopicPopulateTopicCollectionPopulateCollectionPopulateCollectionProblemPopulateProblemAndTopicGroupPermissionPopulateGroupModel = TopicModel & { + collections: TopicCollectionPopulateCollectionPopulateCollectionProblemPopulateProblemModel[] + group_permissions: TopicGroupPermissionPopulateGroupModel[] +} + +export type TopicCollectionPopulateCollectionPopulateCollectionProblemPopulateProblemAndCollectionGroupPermissionsPopulateGroupModel = { + id: string; + collection: CollectionPopulateCollectionProblemsPopulateProblemAndCollectionGroupPermissionsPopulateGroupModel; + order: number; + topic: number; +} + +export type TopicPopulateTopicCollectionPopulateCollectionPopulateCollectionProblemsPopulateProblemAndCollectionGroupPermissionsPopulateGroupAndTopicGroupPermissionPopulateGroupModel = TopicModel & { + collections: TopicCollectionPopulateCollectionPopulateCollectionProblemPopulateProblemAndCollectionGroupPermissionsPopulateGroupModel[] + group_permissions: TopicGroupPermissionPopulateGroupModel[] } \ No newline at end of file diff --git a/src/utilities/DeleteProblem.ts b/src/utilities/DeleteProblem.ts index 570f419..bce617b 100644 --- a/src/utilities/DeleteProblem.ts +++ b/src/utilities/DeleteProblem.ts @@ -1,3 +1,3 @@ -export function handleDeleteProblem(problemId: number) { +export function handleDeleteProblem(problemid: string) { } \ No newline at end of file diff --git a/src/utilities/OnMiddleClickOpenInNewTab.ts b/src/utilities/OnMiddleClickOpenInNewTab.ts new file mode 100644 index 0000000..ae0d5b6 --- /dev/null +++ b/src/utilities/OnMiddleClickOpenInNewTab.ts @@ -0,0 +1,9 @@ +export function onMiddleClickOpenInNewTab( + e: React.MouseEvent | React.MouseEvent, + url: string +) { + if (e.button === 1) { + const win = window.open(url, "_blank"); + if (win) win.focus(); + } +} diff --git a/src/views/Dashboard.tsx b/src/views/Dashboard.tsx index 2335256..c639867 100644 --- a/src/views/Dashboard.tsx +++ b/src/views/Dashboard.tsx @@ -3,26 +3,46 @@ import NavbarSidebarLayout from "../layout/NavbarSidebarLayout"; import NavbarMenuLayout from "../layout/NavbarMenuLayout"; import { SubmissionService } from "../services/Submission.service"; import { SubmissionPopulateSubmissionTestcaseAndProblemSecureModel } from "../types/models/Submission.model"; -import SubmissionCard from "../components/SubmissionCard"; +import SubmissionCard from "../components/Cards/SubmissionCard"; +import { TopicModel } from "../types/models/Topic.model"; +import { TopicService } from "../services/Topic.service"; +import PublicCourseCard from "../components/Cards/CourseCards/PublicCourseCard"; +import { + Carousel, + CarouselContent, + CarouselItem, + CarouselNext, + CarouselPrevious, +} from "../components/shadcn/Carousel"; const Dashboard = () => { - const accountId = Number(localStorage.getItem("account_id")); - const username = localStorage.getItem("username"); + const accountId = String(localStorage.getItem("account_id")); + const username = localStorage.getItem("username"); const [previousAttemptedProblems, setPreviousAttemptedProblems] = useState< SubmissionPopulateSubmissionTestcaseAndProblemSecureModel[] >([]); + const [accessibleCourses, setAccessibleCourses] = useState( + [] + ); + useEffect(() => { SubmissionService.getAll({ account_id: accountId, sort_date: 1, start: 0, - end: 4, + end: 10, }).then((response) => { setPreviousAttemptedProblems(response.data.submissions); + // console.log(response.data.submissions); + }); + + TopicService.getAllAccessibleByAccount(accountId).then((response) => { + console.log("result", response.data.topics); + setAccessibleCourses(response.data.topics); }); - }, []); + }, [accountId]); return ( @@ -32,20 +52,44 @@ const Dashboard = () => { {username}

-

Previous Attempted

+

+ Previous Attempted +

+ + + + {previousAttemptedProblems.map((submission, index) => ( + + + + ))} + + + + -
+ {/*
{previousAttemptedProblems.map((submission, index) => ( -
- -
+ ))} -
+
*/} -

Courses

+

Courses

+ + + {accessibleCourses.map((course,index) => ( + + + + ))} + + + + +
); diff --git a/src/views/ExploreCourses.tsx b/src/views/ExploreCourses.tsx index a24df63..2b58571 100644 --- a/src/views/ExploreCourses.tsx +++ b/src/views/ExploreCourses.tsx @@ -6,11 +6,11 @@ import { TopicService } from '../services/Topic.service' const ExploreCourses = () => { - const accountId = Number(localStorage.getItem("account_id")) + const accountId = String(localStorage.getItem("account_id")) const {courseId} = useParams() useEffect(( )=> { - TopicService.getPublicByAccount(accountId,Number(courseId)).then(response => { + TopicService.getPublicByAccount(accountId,String(courseId)).then(response => { console.log(response.data) }) diff --git a/src/views/ExploreProblems.tsx b/src/views/ExploreProblems.tsx index 5f74a2e..fc5e96a 100644 --- a/src/views/ExploreProblems.tsx +++ b/src/views/ExploreProblems.tsx @@ -1,6 +1,6 @@ import React, { useEffect, useState } from "react"; import NavbarMenuLayout from "../layout/NavbarMenuLayout"; -import PublicProblemCard from "../components/PublicProblemCard"; +import PublicProblemCard from "../components/Cards/ProblemCards/PublicProblemCard"; import CardContainer from "../components/CardContainer"; import { ProblemPopulateAccountAndSubmissionPopulateSubmissionTestcasesSecureModel } from "../types/models/Problem.model"; import { ProblemService } from "../services/Problem.service"; diff --git a/src/views/Login.tsx b/src/views/Login.tsx index fad7f04..fd96863 100644 --- a/src/views/Login.tsx +++ b/src/views/Login.tsx @@ -1,4 +1,4 @@ -import React from "react"; +import React, { useState } from "react"; import { Form, @@ -23,36 +23,47 @@ import CenterContainer from "../layout/CenterLayout"; import { AuthService } from "../services/Auth.service"; import { AccountModel } from "../types/models/Account.model"; import { useNavigate } from "react-router-dom"; +import { Loader2 } from "lucide-react"; // import { getAuthorization, login } from "../services/auth.service"; const Login = () => { const form = useForm(); const navigate = useNavigate(); + const [loading, setLoading] = useState(false); + const [userNotFound, setUserNotFound] = useState(false); + const [wrongPassword, setWrongPassword] = useState(false); + const handleSubmit = async (e: React.FormEvent) => { e.preventDefault(); + setLoading(true); + setWrongPassword(false); + setUserNotFound(false); const { username, password } = form.getValues(); - const { data, status } = await AuthService.login({ + AuthService.login({ username, password, - }); - - const WRONG_PASSWORD = status === 406 - const SUCCESS = status === 202 - - if (WRONG_PASSWORD) { - return - } - else if (SUCCESS) { - const account = data - localStorage.setItem('account_id',String(account.account_id)) - localStorage.setItem('username',account.username) - if (account.token) { - localStorage.setItem('token',account.token) + }).then((response) => { + if (response.status === 202) { + const account = response.data; + localStorage.setItem("account_id", String(account.account_id)); + localStorage.setItem("username", account.username); + if (account.token) { + localStorage.setItem("token", account.token); + } + window.location.reload(); + navigate(-1); + } + setLoading(false); + }).catch((error) => { + if (error.response.status === 404) { + setUserNotFound(true); } - window.location.reload() - navigate(-1) - } + else if (error.response.status === 406) { + setWrongPassword(true); + } + setLoading(false); + }) }; return ( @@ -77,7 +88,9 @@ const Login = () => { - + + {userNotFound && "User doesn't exist."} + )} /> @@ -91,7 +104,9 @@ const Login = () => { - + + {wrongPassword && "Wrong password."} + )} /> @@ -114,8 +129,19 @@ const Login = () => { )} /> - diff --git a/src/views/My/Collections/CreateCollection.tsx b/src/views/My/Collections/CreateCollection.tsx index 6dd3a38..1f789d2 100644 --- a/src/views/My/Collections/CreateCollection.tsx +++ b/src/views/My/Collections/CreateCollection.tsx @@ -9,6 +9,7 @@ import { transformCreateCollectionRequestForm2CreateCollectionRequestForm } from import { CollectionService } from "../../../services/Collection.service"; import { toast } from "../../../components/shadcn/UseToast"; import { useNavigate } from "react-router-dom"; +import { ProblemService } from "../../../services/Problem.service"; const formInitialValue: CreateCollectionRequestForm = { title: "", @@ -20,60 +21,74 @@ const formInitialValue: CreateCollectionRequestForm = { }, ], problemsInterface: [], + groupPermissions: [], + collection: null, }; const CreateCollection = () => { - - const accountId = Number(localStorage.getItem("account_id")); + const accountId = String(localStorage.getItem("account_id")); const navigate = useNavigate(); - const handleSave = ({ createRequest,collectionId,setCollectionId,setLoading }: OnCollectionSavedCallback) => { - - if (!setCollectionId || !setLoading || !createRequest || !collectionId) { - return + const handleSave = ({ + createRequest, + setLoading, + }: OnCollectionSavedCallback) => { + if (!setLoading || !createRequest) { + return; } - const createCollectionRequest = + const { request, problemIds, problemGroupPermissions,groups } = transformCreateCollectionRequestForm2CreateCollectionRequestForm( createRequest as CreateCollectionRequestForm ); - const problemIds = (createRequest as CreateCollectionRequestForm).problemsInterface.map( - (problem) => problem.id as number - ); - setLoading(true) + setLoading(true); - if (collectionId === -1) { + CollectionService.create(accountId, request) + .then((response) => { + return CollectionService.updateProblem( + response.data.collection_id, + problemIds + ); + }) + .then((response) => { + return CollectionService.updateGroupPermissions( + response.data.collection_id, + accountId, + groups + ); + }) + .then((response) => { + let promise = []; + for (const problem of problemGroupPermissions) { + promise.push( + ProblemService.updateGroupPermissions( + problem.problem_id, + accountId, + problem.groupPermissions + ) + ); + } - - CollectionService.create(accountId,createCollectionRequest).then(response => { - return CollectionService.updateProblem(response.data.collection_id,problemIds) - }).then(response => { - setCollectionId(response.data.collection_id) - toast({ - title: "Create Completed" - }) - navigate(`/my/collections/${response.data.collection_id}`) - setLoading(false) + return { + promise: Promise.all(promise), + collection_id: response.data.collection_id, + }; }) - } else { - CollectionService.update(collectionId,createCollectionRequest).then(response => { - return CollectionService.updateProblem(response.data.collection_id,problemIds) - }).then(response => { - setLoading(false) + .then((response) => { toast({ - title: "Update Completed" - }) - console.log("Save") - }) - } + title: "Create Completed", + }); + navigate(`/my/collections/${response.collection_id}/edit`); + setLoading(false); + }); }; return ( - handleSave({ createRequest,collectionId,setCollectionId,setLoading }) + onCollectionSave={({ createRequest, setLoading }) => + handleSave({ createRequest, setLoading }) } createRequestInitialValue={formInitialValue} /> diff --git a/src/views/My/Collections/EditCollection.tsx b/src/views/My/Collections/EditCollection.tsx index a19b43b..30bb6f7 100644 --- a/src/views/My/Collections/EditCollection.tsx +++ b/src/views/My/Collections/EditCollection.tsx @@ -1,67 +1,99 @@ -import React, { useEffect } from 'react' -import NavbarSidebarLayout from '../../../layout/NavbarSidebarLayout' -import CreateCollectionForm, { OnCollectionSavedCallback } from '../../../components/Forms/CreateCollectionForm' -import { CreateCollectionRequestForm } from '../../../types/forms/CreateCollectionRequestForm' -import { useParams } from 'react-router-dom' -import { CollectionService } from '../../../services/Collection.service' -import { ItemInterface } from 'react-sortablejs' -import { transformCreateProblemRequestForm2CreateProblemRequest } from '../../../types/adapters/CreateProblemRequestForm.adapter' -import { transformCreateCollectionRequestForm2CreateCollectionRequestForm } from '../../../types/adapters/CreateCollectionRequestForm.adapter' -import { set } from 'react-hook-form' -import { toast } from '../../../components/shadcn/UseToast' -import { handleDeprecatedDescription } from '../../../utilities/HandleDeprecatedDescription' +import React, { useEffect } from "react"; +import { useParams } from "react-router-dom"; +import CreateCollectionForm, { + OnCollectionSavedCallback, +} from "../../../components/Forms/CreateCollectionForm"; +import { toast } from "../../../components/shadcn/UseToast"; +import NavbarSidebarLayout from "../../../layout/NavbarSidebarLayout"; +import { CollectionService } from "../../../services/Collection.service"; +import { transformCollectionPopulateCollectionProblemsPopulateProblemAndCollectionGroupPermissionsPopulateGroupModel2CreateCollectionRequest } from "../../../types/adapters/Collection.adapter"; +import { transformCreateCollectionRequestForm2CreateCollectionRequestForm } from "../../../types/adapters/CreateCollectionRequestForm.adapter"; +import { CreateCollectionRequestForm } from "../../../types/forms/CreateCollectionRequestForm"; +import { ProblemService } from "../../../services/Problem.service"; const EditCollection = () => { + const accountId = String(localStorage.getItem("account_id")); - const {collectionId} = useParams(); - const editCollectionId = Number(collectionId); + const { collectionId } = useParams(); + const editCollectionId = String(collectionId); - const [createRequest, setCreateRequest] = React.useState() + const [createRequest, setCreateRequest] = + React.useState(); - const handleSave = ({setLoading,createRequest}: OnCollectionSavedCallback) => { + const handleSave = ({ + setLoading, + createRequest, + }: OnCollectionSavedCallback) => { + if (!setLoading || !createRequest) { + return; + } - if (!setLoading || !createRequest) { - return; - } + const { request, problemIds, groups, problemGroupPermissions } = + transformCreateCollectionRequestForm2CreateCollectionRequestForm( + createRequest as CreateCollectionRequestForm + ); - const problemIds = (createRequest as CreateCollectionRequestForm).problemsInterface.map( - (problem) => problem.id as number - ); - const request = transformCreateCollectionRequestForm2CreateCollectionRequestForm(createRequest as CreateCollectionRequestForm) + setLoading(true); - setLoading(true) + CollectionService.update(editCollectionId, request) + .then((response) => { + return CollectionService.updateProblem( + response.data.collection_id, + problemIds + ); + }) + .then((response) => { + return CollectionService.updateGroupPermissions( + response.data.collection_id, + accountId, + groups + ); + }) + .then(() => { + let promise = []; + for (const problem of problemGroupPermissions) { + promise.push( + ProblemService.updateGroupPermissions( + problem.problem_id, + accountId, + problem.groupPermissions + ) + ); + } - CollectionService.update(editCollectionId,request).then(response => { - return CollectionService.updateProblem(response.data.collection_id,problemIds) - }).then(response => { - setLoading(false) - console.log("Save") - toast({ - title: "Update Completed" - }) - }) - } + return Promise.all(promise); + }) + .then((response) => { + setLoading(false); + console.log("Save"); + toast({ + title: "Update Completed", + }); + }); + }; - useEffect(()=> { - CollectionService.get(editCollectionId).then(response => { - setCreateRequest({ - title: response.data.name, - description: JSON.parse(handleDeprecatedDescription(String(response.data.description))), - problemsInterface: response.data.problems.map(collectionProblem => ( - { - id: collectionProblem.problem.problem_id, - name: collectionProblem.problem.title - } as ItemInterface - )) - }) - }) - },[editCollectionId]) + useEffect(() => { + CollectionService.get(editCollectionId, accountId).then((response) => { + setCreateRequest( + transformCollectionPopulateCollectionProblemsPopulateProblemAndCollectionGroupPermissionsPopulateGroupModel2CreateCollectionRequest( + response.data + ) + ); + }); + }, [editCollectionId]); - return ( - - {createRequest && handleSave({setLoading,createRequest})} createRequestInitialValue={createRequest}/>} - - ) -} + return ( + + {createRequest && ( + + handleSave({ setLoading, createRequest }) + } + createRequestInitialValue={createRequest} + /> + )} + + ); +}; -export default EditCollection \ No newline at end of file +export default EditCollection; diff --git a/src/views/My/Collections/MyCollections.tsx b/src/views/My/Collections/MyCollections.tsx index 697a86e..6d3fc6c 100644 --- a/src/views/My/Collections/MyCollections.tsx +++ b/src/views/My/Collections/MyCollections.tsx @@ -2,26 +2,58 @@ import React, { useContext, useEffect, useState } from "react"; import NavbarSidebarLayout from "../../../layout/NavbarSidebarLayout"; import { Button } from "../../../components/shadcn/Button"; import { Input } from "../../../components/shadcn/Input"; -import MyCollectionCard from "../../../components/MyCollectionCard"; +import MyCollectionCard from "../../../components/Cards/CollectionCards/MyCollectionCard"; import { useNavigate } from "react-router-dom"; import CardContainer from "../../../components/CardContainer"; import { NavSidebarContext } from "../../../contexts/NavSidebarContext"; import { CollectionService } from "../../../services/Collection.service"; -import { CollectionModel, CollectionProblemModel } from "../../../types/models/Collection.model"; +import { + CollectionModel, + CollectionPopulateCollectionProblemPopulateProblemModel, + CollectionProblemModel, +} from "../../../types/models/Collection.model"; import { FolderPlus } from "lucide-react"; +import { Tabs, TabsList, TabsTrigger } from "../../../components/shadcn/Tabs"; const MyCollections = () => { const navigate = useNavigate(); - const accountId = Number(localStorage.getItem("account_id")); + const accountId = String(localStorage.getItem("account_id")); - const [collections, setCollections] = useState([]); - const {setSection} = useContext(NavSidebarContext) + const [collections, setCollections] = useState< + CollectionPopulateCollectionProblemPopulateProblemModel[] + >([]); + const [manageableCollections, setManageableCollections] = useState< + CollectionPopulateCollectionProblemPopulateProblemModel[] + >([]); + const [filteredCollections, setFilteredCollections] = useState< + CollectionPopulateCollectionProblemPopulateProblemModel[] + >([]); + const [filteredManageableCollections, setFilteredManageableCollections] = useState< + CollectionPopulateCollectionProblemPopulateProblemModel[] + >([]); + + const { setSection } = useContext(NavSidebarContext); + + const [tabValue, setTabValue] = useState("personal"); + const [searchValue, setSearchValue] = useState("") + + useEffect(() => { + if (!searchValue || searchValue === "") { + setFilteredCollections(collections) + setFilteredManageableCollections(manageableCollections) + } + else { + setFilteredCollections(collections.filter((collection) => collection.name.toLowerCase().includes(searchValue.toLowerCase()))) + setFilteredManageableCollections(manageableCollections.filter((collection) => collection.name.toLowerCase().includes(searchValue.toLowerCase()))) + } + },[searchValue,collections,manageableCollections]) useEffect(() => { - setSection("COLLECTIONS") - CollectionService.getAllByAccount(accountId).then((response => { - setCollections(response.data.collections) - })) + setSection("COLLECTIONS"); + CollectionService.getAllAsCreator(accountId).then((response) => { + setCollections(response.data.collections); + setManageableCollections(response.data.manageable_collections); + }); }, []); return ( @@ -33,8 +65,23 @@ const MyCollections = () => { My Collections
-
- +
+ setSearchValue(e.target.value)} placeholder="Search ..." /> +
+
+ setTabValue(e)} + > + + + Personal + + + Manageable + + +
- {collections.map(collection => ( - - ))} + {tabValue === "personal" && + filteredCollections.map((collection) => ( + + ))} + {tabValue === "manageable" && + filteredManageableCollections.map((collection) => ( + + ))}
diff --git a/src/views/My/Courses/CreateCourse.tsx b/src/views/My/Courses/CreateCourse.tsx index c0407ea..59fe8dd 100644 --- a/src/views/My/Courses/CreateCourse.tsx +++ b/src/views/My/Courses/CreateCourse.tsx @@ -12,53 +12,76 @@ import { CreateCourseRequestForm } from "../../../types/forms/CreateCourseReques import CreateCourseForm, { OnCourseSavedCallback, } from "../../../components/Forms/CreateCourseForm"; -import { transformCreateCourseRequestForm2CreateTopicRequestFormData } from "../../../types/adapters/CreateCourseRequestForm.adapter"; +import { transformCreateCourseRequestForm2CreateTopicRequest } from "../../../types/adapters/CreateCourseRequestForm.adapter"; import { TopicService } from "../../../services/Topic.service"; import { useNavigate } from "react-router-dom"; +import { EmptyEditorValue } from "../../../constants/DummyEditorValue"; const formInitialValue: CreateCourseRequestForm = { title: "", - description: [ - { - id: "1", - type: ELEMENT_PARAGRAPH, - children: [{ text: "" }], - }, - ], + description: EmptyEditorValue, image: null, isPrivate: false, collectionsInterface: [], + groupPermissions: [], + course: null, + // collectionGroupPermissions: [], }; const CreateCourse = () => { - - const navigate = useNavigate() - const accountId = Number(localStorage.getItem("account_id")); + const navigate = useNavigate(); + const accountId = String(localStorage.getItem("account_id")); const handleSave = ({ setLoading, createRequest, - courseId, - setCourseId, }: OnCourseSavedCallback) => { - if (!setCourseId || !setLoading || !createRequest || !courseId) { + if (!setLoading || !createRequest) { return; } - const formData = transformCreateCourseRequestForm2CreateTopicRequestFormData(createRequest) - const collectionIds = createRequest.collectionsInterface.map((collection) => collection.id as number) + const { formData, collectionIds, groups, collectionGroupsPermissions } = + transformCreateCourseRequestForm2CreateTopicRequest(createRequest); - setLoading(true) - TopicService.create(accountId, formData).then((response) => { - return TopicService.updateCollections(response.data.topic_id,collectionIds) - }).then((response) => { - console.log("OK!") - setLoading(false) - toast({ - title: "Create Completed" + setLoading(true); + TopicService.create(accountId, formData) + .then((response) => { + return TopicService.updateCollections( + response.data.topic_id, + collectionIds + ); + }) + .then((response) => { + return TopicService.updateGroupPermissions( + response.data.topic_id, + accountId, + groups + ); }) - navigate(`/my/courses/${response.data.topic_id}`) - }) + .then((response) => { + let promise = []; + for (const collection of collectionGroupsPermissions) { + promise.push( + CollectionService.updateGroupPermissions( + collection.collection_id, + accountId, + collection.groupPermissions + ) + ); + } + + return { + promise: Promise.all(promise), + topic_id: response.data.topic_id, + }; + }) + .then(({ topic_id }) => { + setLoading(false); + toast({ + title: "Create Completed", + }); + navigate(`/my/courses/${topic_id}/edit`); + }); }; return ( @@ -69,14 +92,12 @@ const CreateCourse = () => { // } onCourseSave={({ createRequest, - courseId, - setCourseId, + setLoading, }) => handleSave({ createRequest, - courseId, - setCourseId, + setLoading, }) } diff --git a/src/views/My/Courses/EditCourse.tsx b/src/views/My/Courses/EditCourse.tsx index a52fc31..ea8e0de 100644 --- a/src/views/My/Courses/EditCourse.tsx +++ b/src/views/My/Courses/EditCourse.tsx @@ -1,26 +1,20 @@ -import React, { useEffect, useState } from "react"; -import NavbarSidebarLayout from "../../../layout/NavbarSidebarLayout"; -import CreateCollectionForm, { - OnCollectionSavedCallback, -} from "../../../components/Forms/CreateCollectionForm"; -import { CreateCollectionRequestForm } from "../../../types/forms/CreateCollectionRequestForm"; -import { ELEMENT_PARAGRAPH } from "@udecode/plate-paragraph"; -import { transformCreateCollectionRequestForm2CreateCollectionRequestForm } from "../../../types/adapters/CreateCollectionRequestForm.adapter"; -import { CollectionService } from "../../../services/Collection.service"; -import { toast } from "../../../components/shadcn/UseToast"; -import { CreateCourseRequestForm } from "../../../types/forms/CreateCourseRequestForm"; +import { useEffect, useState } from "react"; +import { useParams } from "react-router-dom"; import CreateCourseForm, { OnCourseSavedCallback, } from "../../../components/Forms/CreateCourseForm"; -import { transformCreateCourseRequestForm2CreateTopicRequestFormData } from "../../../types/adapters/CreateCourseRequestForm.adapter"; +import { toast } from "../../../components/shadcn/UseToast"; +import NavbarSidebarLayout from "../../../layout/NavbarSidebarLayout"; import { TopicService } from "../../../services/Topic.service"; -import { useParams } from "react-router-dom"; -import { ItemInterface } from "react-sortablejs"; +import { transformCreateCourseRequestForm2CreateTopicRequest } from "../../../types/adapters/CreateCourseRequestForm.adapter"; +import { transformTopicPopulateTopicCollectionPopulateCollectionAndTopicGroupPermissionPopulateGroupModel2CreateCourseRequest } from "../../../types/adapters/Topic.adapter"; +import { CreateCourseRequestForm } from "../../../types/forms/CreateCourseRequestForm"; +import { CollectionService } from "../../../services/Collection.service"; const EditCourse = () => { const { courseId } = useParams(); - const editCourseId = Number(courseId); - const accountId = Number(localStorage.getItem("account_id")); + const editCourseId = String(courseId); + const accountId = String(localStorage.getItem("account_id")); const [createRequest, setCreateRequest] = useState(); @@ -28,23 +22,18 @@ const EditCourse = () => { const handleSave = ({ setLoading, createRequest, - courseId, - setCourseId, }: OnCourseSavedCallback) => { - if (!setCourseId || !setLoading || !createRequest || !courseId) { + if (!setLoading || !createRequest || !courseId) { return; } - const formData = - transformCreateCourseRequestForm2CreateTopicRequestFormData( - createRequest - ); - const collectionIds = createRequest.collectionsInterface.map( - (collection) => collection.id as number - ); + const { formData, collectionIds, groups, collectionGroupsPermissions } = + transformCreateCourseRequestForm2CreateTopicRequest(createRequest); + + console.log(formData.get("name")); setLoading(true); - TopicService.update(editCourseId, formData) + TopicService.update(editCourseId, accountId, formData) .then(() => { return TopicService.updateCollections( editCourseId, @@ -52,29 +41,43 @@ const EditCourse = () => { ); }) .then(() => { - console.log("OK!"); + return TopicService.updateGroupPermissions( + editCourseId, + accountId, + groups + ); + }) + .then(() => { + let promise = []; + for (const collection of collectionGroupsPermissions) { + promise.push( + CollectionService.updateGroupPermissions( + collection.collection_id, + accountId, + collection.groupPermissions + ) + ); + } + + return Promise.all(promise); + }) + .then(() => { setLoading(false); toast({ title: "Update Completed", - }) + }); }); }; useEffect(() => { - TopicService.get(accountId,editCourseId).then((response) => { + TopicService.get(accountId, editCourseId).then((response) => { const { data } = response; - setCreateRequest({ - title: data.name, - description: JSON.parse(String(data.description)), - isPrivate: data.is_private, - collectionsInterface: data.collections.map( - (topicCollection) => - ({ - id: topicCollection.collection.collection_id, - name: topicCollection.collection.name, - } as ItemInterface) - ), - }); + console.log(data); + setCreateRequest( + transformTopicPopulateTopicCollectionPopulateCollectionAndTopicGroupPermissionPopulateGroupModel2CreateCourseRequest( + data + ) + ); }); }, [editCourseId]); @@ -84,14 +87,12 @@ const EditCourse = () => { handleSave({ createRequest, - courseId, - setCourseId, + setLoading, }) } diff --git a/src/views/My/Courses/MyCourses.tsx b/src/views/My/Courses/MyCourses.tsx index f67bdc8..239da5a 100644 --- a/src/views/My/Courses/MyCourses.tsx +++ b/src/views/My/Courses/MyCourses.tsx @@ -4,24 +4,45 @@ import { Input } from "../../../components/shadcn/Input"; import { Button } from "../../../components/shadcn/Button"; import { useNavigate } from "react-router-dom"; import CardContainer from "../../../components/CardContainer"; -import MyCourseCard from "../../../components/MyCourseCard"; +import MyCourseCard from "../../../components/Cards/CourseCards/MyCourseCard"; import { NavSidebarContext } from "../../../contexts/NavSidebarContext"; import { TopicService } from "../../../services/Topic.service"; import { TopicPopulateTopicCollectionPopulateCollectionModel } from "../../../types/models/Topic.model"; import { LibraryBig } from "lucide-react"; +import { Tabs, TabsList, TabsTrigger } from "../../../components/shadcn/Tabs"; const MyCourses = () => { const navigate = useNavigate(); - const accountId = Number(localStorage.getItem("account_id")); + const accountId = String(localStorage.getItem("account_id")); const {setSection} = useContext(NavSidebarContext) const [topics, setTopics] = useState([]) + const [manageableTopics, setManageableTopics] = useState([]) + + const [filteredTopics, setFilteredTopics] = useState([]) + const [filteredManageableTopics, setFilteredManageableTopics] = useState([]) + + const [tabValue, setTabValue] = useState("personal") + const [searchValue, setSearchValue] = useState("") + + useEffect(() => { + if (!searchValue || searchValue === "") { + setFilteredTopics(topics) + setFilteredManageableTopics(manageableTopics) + } + else { + setFilteredTopics(topics.filter((topic) => topic.name.toLowerCase().includes(searchValue.toLowerCase()))) + setFilteredManageableTopics(manageableTopics.filter((topic) => topic.name.toLowerCase().includes(searchValue.toLowerCase()))) + } + },[searchValue,topics,manageableTopics]) + useEffect(( )=> { setSection("COURSES") - TopicService.getAllByAccount(accountId).then((response) => { + TopicService.getAllAsCreator(accountId).then((response) => { setTopics(response.data.topics) + setManageableTopics(response.data.manageable_topics) }) },[]) @@ -34,8 +55,20 @@ const MyCourses = () => { My Courses
-
- +
+ setSearchValue(e.target.value)} placeholder="Search ..." /> +
+
+ setTabValue(e)}> + + + Personal + + + Manageable + + +
- + setSearchValue(e.target.value)}/>
-
- +
+ setSearchValue(e.target.value)} placeholder="Search ..." /> +
+
+ setTabValue(e)}> + + + Personal + + + Manageable + + +
- {problems.map((problem, index) => ( + {tabValue === "personal" && filteredProblems.map((problem, index) => ( + + ))} + {tabValue === "manageable" && filteredManageableProblems.map((problem, index) => ( ))} diff --git a/src/views/ViewCourse.tsx b/src/views/ViewCourse.tsx index eb47d25..9f54a30 100644 --- a/src/views/ViewCourse.tsx +++ b/src/views/ViewCourse.tsx @@ -19,7 +19,7 @@ import { } from "../components/shadcn/Accordion"; import { LibraryBig } from "lucide-react"; import { Card } from "../components/shadcn/Card"; -import TopicCollectionAccordionCard from "../components/TopicCollectionAccordionCard"; +import TopicCollectionAccordionCard from "../components/Cards/CollectionCards/TopicCollectionAccordionCard"; import CardContainer from "../components/CardContainer"; import { ScrollArea } from "../components/shadcn/ScrollArea"; import TopicCollectionsAccordion from "../components/TopicCollectionsAccordion"; @@ -28,7 +28,7 @@ import CourseNavbarSidebarLayout from "../layout/CourseNavbarSidebarLayout"; import { CourseNavSidebarContext } from "../contexts/CourseNavSidebarContexnt"; const ViewCourse = () => { - const accountId = Number(localStorage.getItem("account_id")); + const accountId = String(localStorage.getItem("account_id")); const { courseId } = useParams(); // const [course, setCourse] = @@ -36,10 +36,10 @@ const ViewCourse = () => { // const [course,setCourse] - const {course,setCourse} = useContext(CourseNavSidebarContext); + const { course, setCourse } = useContext(CourseNavSidebarContext); // useEffect(() => { - // TopicService.getPublicByAccount(accountId, Number(courseId)).then( + // TopicService.getPublicByAccount(accountId, String(courseId)).then( // (response) => { // console.log(response.data); // setCourse(response.data); @@ -65,13 +65,15 @@ const ViewCourse = () => { collections={course?.collections as TopicCollectionPopulateCollectionPopulateCollectionProblemPopulateProblemPopulateAccountAndSubmissionPopulateSubmissionTestcasesSecureModel[]} /> */} -
- {course?.collections.map((tc) => ( - - ))} -
+ +
+ {course?.collections.map((tc) => ( + + ))} +
+
{/* */} {/* */} diff --git a/src/views/ViewCourseProblem.tsx b/src/views/ViewCourseProblem.tsx index 0057571..d8ef133 100644 --- a/src/views/ViewCourseProblem.tsx +++ b/src/views/ViewCourseProblem.tsx @@ -1,35 +1,32 @@ -import React, { useContext, useEffect, useState } from "react"; +import { useEffect, useState } from "react"; import { useParams } from "react-router-dom"; -import { ProblemPoplulateCreatorModel } from "../types/models/Problem.model"; -import { GetSubmissionByAccountProblemResponse } from "../types/models/Submission.model"; -import { ProblemService } from "../services/Problem.service"; -import { SubmissionService } from "../services/Submission.service"; -import NavbarMenuLayout from "../layout/NavbarMenuLayout"; import ProblemViewLayout, { OnSubmitProblemViewLayoutCallback, } from "../components/ProblemViewLayout"; import CourseNavbarSidebarLayout from "../layout/CourseNavbarSidebarLayout"; -import { CourseNavSidebarContext } from "../contexts/CourseNavSidebarContexnt"; -import { TopicService } from "../services/Topic.service"; +import { ProblemService } from "../services/Problem.service"; +import { SubmissionService } from "../services/Submission.service"; +import { ProblemPopulateCreatorSecureModel } from "../types/models/Problem.model"; +import { GetSubmissionByAccountProblemResponse } from "../types/models/Submission.model"; const ViewCourseProblem = () => { - const accountId = Number(localStorage.getItem("account_id")); + const accountId = String(localStorage.getItem("account_id")); const { courseId, problemId } = useParams(); - const [problem, setProblem] = useState(); + const [problem, setProblem] = useState(); const [previousSubmissions, setPreviousSubmissions] = useState(); useEffect(() => { - ProblemService.get(Number(problemId)).then((response) => { + ProblemService.getPublic(String(problemId)).then((response) => { setProblem(response.data); }); SubmissionService.getByAccountProblemInTopic( accountId, - Number(problemId), - Number(courseId) + String(problemId), + String(courseId) ).then((response) => { setPreviousSubmissions(response.data); }); @@ -44,8 +41,8 @@ const ViewCourseProblem = () => { setGrading(true); SubmissionService.topicSubmit( accountId, - Number(courseId), - Number(problemId), + String(courseId), + String(problemId), { language: selectedLanguage, submission_code: String(submitCodeValue), @@ -54,8 +51,8 @@ const ViewCourseProblem = () => { setLastedSubmission(response.data); SubmissionService.getByAccountProblemInTopic( accountId, - Number(problemId), - Number(courseId) + String(problemId), + String(courseId) ).then((response) => { setPreviousSubmissions(response.data); }); @@ -68,7 +65,7 @@ const ViewCourseProblem = () => {
handleSubmit(e)} - problem={problem as ProblemPoplulateCreatorModel} + problem={problem as ProblemPopulateCreatorSecureModel} previousSubmissions={ previousSubmissions as GetSubmissionByAccountProblemResponse } diff --git a/src/views/ViewProblem.tsx b/src/views/ViewProblem.tsx index beb9e11..c620eee 100644 --- a/src/views/ViewProblem.tsx +++ b/src/views/ViewProblem.tsx @@ -1,38 +1,18 @@ -import React, { useEffect, useState } from "react"; -import NavbarMenuLayout from "../layout/NavbarMenuLayout"; +import { ELEMENT_PARAGRAPH } from "@udecode/plate-paragraph"; +import { useEffect, useState } from "react"; import { useNavigate, useParams } from "react-router-dom"; -import { Separator } from "../components/shadcn/Seperator"; -import PlateEditor from "../components/PlateEditor"; -import ReadOnlyPlate from "../components/ReadOnlyPlate"; -import { Editor as MonacoEditor } from "@monaco-editor/react"; -import { Label } from "../components/shadcn/Label"; -import { Combobox } from "../components/shadcn/Combobox"; -import { ProgrammingLanguageOptions } from "../constants/ProgrammingLanguage"; -import { Button } from "../components/shadcn/Button"; -import TestcasesGradingIndicator from "../components/TestcasesGradingIndicator"; import { styled } from "styled-components"; +import ProblemViewLayout, { + OnSubmitProblemViewLayoutCallback, +} from "../components/ProblemViewLayout"; +import NavbarMenuLayout from "../layout/NavbarMenuLayout"; import { ProblemService } from "../services/Problem.service"; -import { ProblemPoplulateCreatorModel } from "../types/models/Problem.model"; import { SubmissionService } from "../services/Submission.service"; +import { ProblemPoplulateCreatorModel, ProblemPopulateCreatorSecureModel } from "../types/models/Problem.model"; import { GetSubmissionByAccountProblemResponse, - SubmissionModel, - SubmissionPopulateSubmissionTestcasesSecureModel, + SubmissionPopulateSubmissionTestcasesSecureModel } from "../types/models/Submission.model"; -import { SubmitProblemResponse } from "../types/apis/Submission.api"; -import PreviousSubmissionsCombobox from "../components/PreviousSubmissionsCombobox"; -import { SubmitProblemResponse2GetSubmissionByAccountProblemResponse } from "../types/adapters/Submission.adapter"; -import { ELEMENT_PARAGRAPH } from "@udecode/plate-paragraph"; -import { - ArrowLeft, - ChevronLeftIcon, - ChevronLeftSquareIcon, - Loader2, -} from "lucide-react"; -import { readableDateFormat } from "../utilities/ReadableDateFormat"; -import ProblemViewLayout, { - OnSubmitProblemViewLayoutCallback, -} from "../components/ProblemViewLayout"; const handleDeprecatedDescription = (description: string): string => { if (description[0] === "[") { @@ -51,10 +31,10 @@ const handleDeprecatedDescription = (description: string): string => { const ViewProblem = () => { const navigate = useNavigate(); const { problemId } = useParams(); - const accountId = Number(localStorage.getItem("account_id")); + const accountId = String(localStorage.getItem("account_id")); const [selectedLanguage, setSelectedLanguage] = useState("python"); - const [problem, setProblem] = useState(); + const [problem, setProblem] = useState(); const [grading, setGrading] = useState(false); const [submitCodeValue, setSubmitCodeValue] = useState(""); @@ -64,7 +44,7 @@ const ViewProblem = () => { const [lastedSubmission, setLastedSubmission] = useState(); - const handleSelectPreviousSubmission = (submissionId: number) => { + const handleSelectPreviousSubmission = (submissionId: string) => { let submission = null; if ( submissionId === previousSubmissions?.best_submission?.submission_id @@ -87,13 +67,13 @@ const ViewProblem = () => { }; useEffect(() => { - ProblemService.get(Number(problemId)).then((response) => { + ProblemService.getPublic(String(problemId)).then((response) => { setProblem(response.data); }); SubmissionService.getByAccountProblem( accountId, - Number(problemId) + String(problemId) ).then((response) => { setPreviousSubmissions(response.data); }); @@ -106,7 +86,7 @@ const ViewProblem = () => { submitCodeValue, }: OnSubmitProblemViewLayoutCallback) => { setGrading(true); - SubmissionService.submit(accountId, Number(problemId), { + SubmissionService.submit(accountId, String(problemId), { language: selectedLanguage, submission_code: String(submitCodeValue), }).then((response) => { @@ -114,7 +94,7 @@ const ViewProblem = () => { setLastedSubmission(response.data); SubmissionService.getByAccountProblem( accountId, - Number(problemId) + String(problemId) ).then((response) => { setPreviousSubmissions(response.data); }); @@ -127,7 +107,7 @@ const ViewProblem = () => {
handleSubmit(e)} - problem={problem as ProblemPoplulateCreatorModel} + problem={problem as ProblemPopulateCreatorSecureModel} previousSubmissions={ previousSubmissions as GetSubmissionByAccountProblemResponse }