Skip to content
6 changes: 4 additions & 2 deletions codewit/api/src/models/exercise.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,9 @@ class Exercise extends Model<
declare title?: string | null;
declare difficulty?: Difficulty | null;

declare createdAt?: Date;
declare updatedAt?: Date;

declare getTags: BelongsToManyGetAssociationsMixin<Tag>;
declare addTag: BelongsToManyAddAssociationMixin<Tag, number>;
declare addTags: BelongsToManyAddAssociationsMixin<Tag, number>;
Expand Down Expand Up @@ -84,15 +87,14 @@ class Exercise extends Model<
key: 'uid',
},
},
title: {
title: {
type: DataTypes.STRING,
allowNull: true,
},
difficulty: {
type: DataTypes.ENUM('easy', 'hard', 'worked example'),
allowNull: true,
}

},
{
sequelize,
Expand Down
30 changes: 30 additions & 0 deletions codewit/api/src/routes/exercise.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { Router } from 'express';
import { z } from "zod";

import {
createExercise,
Expand All @@ -17,6 +18,7 @@ import { checkAdmin } from '../middleware/auth';
import multer from 'multer';
import { asyncHandle } from '../middleware/catch';
import { importExercisesCsv } from '../controllers/exerciseImport';
import { executeCodeEvaluation } from "../utils/codeEvalService";

const exerciseRouter = Router();

Expand Down Expand Up @@ -161,4 +163,32 @@ exerciseRouter.delete('/:uid', checkAdmin, async (req, res) => {
}
});

const test_schema = z.object({
code: z.string(),
language: z.string(),
reference_test: z.string(),
});

exerciseRouter.post("/test", checkAdmin, asyncHandle(async (req, res) => {
const valid = test_schema.safeParse(req.body);

// ts does not like it if you try to do `!valid.success` as it will consider
// `valid.error` to not exist
if (valid.success === false) {
return res.status(400).json({
error: "Validation",
payload: fromZodError(valid.error)
});
}

let result = await executeCodeEvaluation({
language: valid.data.language,
code: valid.data.code,
testCode: valid.data.reference_test,
runTests: true,
});

return res.status(200).json(result);
}));

export default exerciseRouter;
2 changes: 2 additions & 0 deletions codewit/api/src/typings/response.types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@ export interface ExerciseResponse {
starterCode: string;
title?: string;
difficulty?: Difficulty | null;
createdAt: string,
updatedAt: string,
}

export interface CourseResponse {
Expand Down
2 changes: 2 additions & 0 deletions codewit/api/src/utils/responseFormatter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,8 @@ export function formatSingleExercise(
starterCode: exercise.starterCode,
title: exercise.title,
difficulty: exercise.difficulty,
createdAt: exercise.createdAt.toJSON(),
updatedAt: exercise.updatedAt.toJSON(),
};
}

Expand Down
28 changes: 28 additions & 0 deletions codewit/client/src/form.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { createFormHook } from "@tanstack/react-form";

import {
fieldContext,
formContext,
useFieldContext,
useFormContext
} from "./form/context";
import {
SubmitButton,
ConfirmReset,
ConfirmDelete,
ConfirmAway
} from "./form/button";

const { useAppForm } = createFormHook({
fieldContext,
formContext,
fieldComponents: {},
formComponents: {
SubmitButton,
ConfirmReset,
ConfirmDelete,
ConfirmAway,
},
});

export { useAppForm, useFieldContext, useFormContext };
203 changes: 203 additions & 0 deletions codewit/client/src/form/button.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,203 @@
import { ArrowLeftIcon, TrashIcon, ArrowPathIcon } from "@heroicons/react/24/solid";
import { Button, Modal, ModalHeader, ModalBody, ModalFooter } from "flowbite-react";
import { useState } from "react";

import { useFormContext } from "./context";

interface SubmitButtonProps {}

export function SubmitButton({}) {
const form = useFormContext();

return <form.Subscribe selector={state => ({
submitting: state.isSubmitting,
dirty: state.isDirty,
can_submit: state.canSubmit,
})}>
{({submitting, dirty, can_submit}) => (
<Button type="submit" disabled={submitting || !dirty || !can_submit}>
Save
</Button>
)}
</form.Subscribe>
}

interface ConfirmResetProps {
/**
* will be called when the user requests the reset
*/
on_reset: () => void,
/**
* will be called when the user requests to cancel the reset
*/
on_cancel?: () => void,
}

/**
* a simple reset confirmation modal that will trigger the `on_reset` when the
* user confirms the reset action. requires a form context as it will enable
* and disable based on if the form is `submitting` or is `dirty`.
*/
export function ConfirmReset({
on_reset,
on_cancel = () => {},
}: ConfirmResetProps) {
const form = useFormContext();

const [open, set_open] = useState(false);

const cancel_cb = () => {
set_open(false);
on_cancel();
};

const reset_cb = () => {
set_open(false);
on_reset();
};

return <form.Subscribe selector={state => ({
submitting: state.isSubmitting,
dirty: state.isDirty,
})}>
{({submitting, dirty}) => <>
<Button type="button" color="dark" disabled={submitting || !dirty} onClick={() => set_open(true)}>
<ArrowPathIcon className="mr-2 w-4 h-4"/> Reset
</Button>
<Modal dismissible show={open} onClose={cancel_cb}>
<ModalHeader>Reset Data</ModalHeader>
<ModalBody>
<p>This will reset any changes you made to its original state. Are you sure?</p>
</ModalBody>
<ModalFooter>
<Button type="button" onClick={reset_cb}>Reset</Button>
<Button type="button" color="dark" onClick={cancel_cb}>Cancel</Button>
</ModalFooter>
</Modal>
</>}
</form.Subscribe>
}

interface ConfirmDeleteProps {
/**
* will be called when the user requests the delete
*/
on_delete: () => void,
/**
* will be called when the user reqests to cancel the delete
*/
on_cancel?: () => void,
}

/**
* a simple delete confirmation modal that will trigger the `on_delete` when the
* user confirms the delete action. requires a form context as it will enable
* and disable based on if the form is `submitting`
*/
export function ConfirmDelete({
on_delete,
on_cancel = () => {},
}: ConfirmDeleteProps) {
const form = useFormContext();

const [open, set_open] = useState(false);

const cancel_cb = () => {
set_open(false);
on_cancel();
};

const delete_cb = () => {
set_open(false);
on_delete();
};

return <form.Subscribe selector={state => ({
submitting: state.isSubmitting,
})}>
{({submitting}) => <>
<Button type="button" color="red" disabled={submitting} onClick={() => set_open(true)}>
<TrashIcon className="mr-2 w-4 h-4"/> Delete
</Button>
<Modal dismissible show={open} onClose={cancel_cb}>
<ModalHeader>Delete Data</ModalHeader>
<ModalBody>
<p>This will delete the current data from the server. Are you sure?</p>
</ModalBody>
<ModalFooter>
<Button type="button" color="red" onClick={delete_cb}>Delete</Button>
<Button type="button" color="dark" onClick={cancel_cb}>Cancel</Button>
</ModalFooter>
</Modal>
</>}
</form.Subscribe>
}

interface ConfirmAwayProps {
/**
* will be called when the user reqests to away
*/
on_away: () => void,
/**
* will be called when the user requests to cancel the away
*/
on_cancel?: () => void,
}

/**
* a simple away confirmation modal that will trigger the `on_away` when the
* user confirms the awayt action. requires a form context as it will enable
* and disable based on if the form is `submitting`. the modal will only appear
* if the form is marked as dirty otherwise it will not prompt the user to
* confirm the away request.
*/
export function ConfirmAway({
on_away,
on_cancel = () => {},
}: ConfirmAwayProps) {
const form = useFormContext();

const [open, set_open] = useState(false);

const away_cb = () => {
set_open(false);
on_away();
};

const cancel_cb = () => {
set_open(false);
on_cancel();
};

return <form.Subscribe selector={state => ({
submitting: state.isSubmitting,
dirty: state.isDirty,
})}>
{({submitting, dirty}) => <>
<Button
type="button"
color="light"
disabled={submitting}
onClick={() => {
if (dirty) {
set_open(true);
} else {
on_away();
}
}}
>
<ArrowLeftIcon className="w-6 h-6"/>
</Button>
<Modal dismissible show={open} onClose={cancel_cb}>
<ModalHeader>Cancel Changes</ModalHeader>
<ModalBody>
<p>You have made changes to the data and did not save. Are you sure?</p>
</ModalBody>
<ModalFooter>
<Button type="button" color="light" onClick={away_cb}>Leave</Button>
<Button type="button" color="dark" onClick={cancel_cb}>Cancel</Button>
</ModalFooter>
</Modal>
</>}
</form.Subscribe>
}
6 changes: 6 additions & 0 deletions codewit/client/src/form/context.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import { createFormHookContexts, createFormHook } from "@tanstack/react-form";

const { fieldContext, formContext, useFieldContext, useFormContext } = createFormHookContexts();

export { fieldContext, formContext, useFieldContext, useFormContext };

19 changes: 17 additions & 2 deletions codewit/client/src/main.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,10 @@ import { StrictMode } from 'react';
import * as ReactDOM from 'react-dom/client';
import { BrowserRouter } from 'react-router-dom';
import { ToastContainer } from "react-toastify";
import { TanStackDevtools } from "@tanstack/react-devtools";
import { FormDevtoolsPanel } from "@tanstack/react-form-devtools";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { ReactQueryDevtools } from "@tanstack/react-query-devtools";
import { ReactQueryDevtoolsPanel } from "@tanstack/react-query-devtools";
import "react-toastify/dist/ReactToastify.css";

import App from './app/app';
Expand All @@ -22,7 +24,20 @@ root.render(
<App />
<ToastContainer position="bottom-right" autoClose={1000} />
</BrowserRouter>
<ReactQueryDevtools initialIsOpen={false}/>
<TanStackDevtools
plugins={[
{
name: "TanStack Query",
render: <ReactQueryDevtoolsPanel/>,
defaultOpen: true,
},
{
name: "TanStack Form",
render: <FormDevtoolsPanel/>,
defaultOpen: false,
},
]}
/>
</QueryClientProvider>
// </StrictMode>
);
Loading