From a618472fa6ff54cc0d2d970c4dd51a3f81d404d0 Mon Sep 17 00:00:00 2001 From: RockCrafts <44756861+RockCrafts@users.noreply.github.com> Date: Sun, 29 Sep 2024 14:01:43 -0400 Subject: [PATCH 01/11] TA Timer Label --- .../InstructorQueuePage/QuestionCard.tsx | 95 +++++++++++-------- .../InstructorQueuePage/QuestionTimer.tsx | 22 +++++ frontend/package.json | 1 + frontend/yarn.lock | 70 ++++++++++++++ 4 files changed, 148 insertions(+), 40 deletions(-) create mode 100644 frontend/components/Course/InstructorQueuePage/QuestionTimer.tsx diff --git a/frontend/components/Course/InstructorQueuePage/QuestionCard.tsx b/frontend/components/Course/InstructorQueuePage/QuestionCard.tsx index a00f7a36..23fd4de8 100644 --- a/frontend/components/Course/InstructorQueuePage/QuestionCard.tsx +++ b/frontend/components/Course/InstructorQueuePage/QuestionCard.tsx @@ -16,6 +16,7 @@ import { Question, QuestionStatus, User } from "../../../types"; import MessageQuestionModal from "./MessageQuestionModal"; import FinishConfirmModal from "./FinishConfirmModal"; import LinkedText from "../../common/ui/LinkedText"; +import QuestionTimer from "./QuestionTimer"; export const fullName = (user: User) => `${user.firstName} ${user.lastName}`; @@ -80,12 +81,12 @@ const QuestionCard = (props: QuestionCardProps) => { return false; // todo: pass loading down props }; - // Re-render timestamp every 5 seconds + // Re-render timestamp every 1 second const [, setTime] = useState(Date.now()); useEffect(() => { const interval = setInterval(() => { setTime(Date.now()); - }, 5000); + }, 1000); return () => clearInterval(interval); }, []); @@ -137,48 +138,62 @@ const QuestionCard = (props: QuestionCardProps) => {
- - Asked{" "} - {moment - .duration( - moment().diff( - moment( - question.timeAsked +
+ + Asked{" "} + {moment + .duration( + moment().diff( + moment( + question.timeAsked + ) ) ) - ) - .humanize()}{" "} - ago - - ) : ( - - Started{" "} - {moment - .duration( - moment().diff( - moment( - question.timeResponseStarted + .humanize()}{" "} + ago + + ) : ( + + Started{" "} + {moment + .duration( + moment().diff( + moment( + question.timeResponseStarted + ) ) ) - ) - .humanize()}{" "} - ago - - ) - } - content={timeString( - question.timeResponseStarted - ? question.timeResponseStarted - : question.timeAsked, - false - )} - basic - inverted - position="left center" - /> + .humanize()}{" "} + ago + + ) + } + content={timeString( + question.timeResponseStarted + ? question.timeResponseStarted + : question.timeAsked, + false + )} + basic + inverted + position="left center" + /> + +
diff --git a/frontend/components/Course/InstructorQueuePage/QuestionTimer.tsx b/frontend/components/Course/InstructorQueuePage/QuestionTimer.tsx new file mode 100644 index 00000000..3b798879 --- /dev/null +++ b/frontend/components/Course/InstructorQueuePage/QuestionTimer.tsx @@ -0,0 +1,22 @@ +import React from "react"; +import { Label } from "semantic-ui-react"; + +const QuestionTimer = ({ startTime }) => { + const now = Date.now(); + const TATimerMaxTime = 10; // 10 minutes + const diff = + TATimerMaxTime * 60 * 1000 - (now - new Date(startTime).getTime()); + const minutes = Math.floor(Math.abs(diff) / (1000 * 60)); + const seconds = Math.floor((Math.abs(diff) % (1000 * 60)) / 1000); + + const sign = diff < 0 ? "-" : ""; // Add a negative sign if the time difference is negative + + const formated = `${sign}${minutes.toString().padStart(2, "0")}:${seconds + .toString() + .padStart(2, "0")}`; + + if (!startTime) return null; + return ; +}; + +export default QuestionTimer; diff --git a/frontend/package.json b/frontend/package.json index d206398f..cd4291d8 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -62,6 +62,7 @@ "@types/react-select": "^5.0.1", "@typescript-eslint/eslint-plugin": "^5.59.5", "@typescript-eslint/parser": "^5.59.5", + "autoprefixer": "^10.4.20", "babel-eslint": "^10.1.0", "babel-plugin-styled-components": "^1.10.7", "eslint": "^8.40.0", diff --git a/frontend/yarn.lock b/frontend/yarn.lock index 4125b6d4..e8ed7570 100644 --- a/frontend/yarn.lock +++ b/frontend/yarn.lock @@ -2403,6 +2403,18 @@ attr-accept@^2.2.2: resolved "https://registry.yarnpkg.com/attr-accept/-/attr-accept-2.2.2.tgz#646613809660110749e92f2c10833b70968d929b" integrity sha512-7prDjvt9HmqiZ0cl5CRjtS84sEyhsHP2coDkaZKRKVfCDo9s7iw7ChVmar78Gu9pC4SoR/28wFu/G5JJhTnqEg== +autoprefixer@^10.4.20: + version "10.4.20" + resolved "https://registry.yarnpkg.com/autoprefixer/-/autoprefixer-10.4.20.tgz#5caec14d43976ef42e32dcb4bd62878e96be5b3b" + integrity sha512-XY25y5xSv/wEoqzDyXXME4AFfkZI0P23z6Fs3YgymDnKJkCGOnkL0iTxCa85UTqaSgfcqyf3UA6+c7wUvx/16g== + dependencies: + browserslist "^4.23.3" + caniuse-lite "^1.0.30001646" + fraction.js "^4.3.7" + normalize-range "^0.1.2" + picocolors "^1.0.1" + postcss-value-parser "^4.2.0" + available-typed-arrays@^1.0.5: version "1.0.5" resolved "https://registry.yarnpkg.com/available-typed-arrays/-/available-typed-arrays-1.0.5.tgz#92f95616501069d07d10edb2fc37d3e1c65123b7" @@ -2595,6 +2607,16 @@ browserslist@^4.21.3, browserslist@^4.21.5: node-releases "^2.0.8" update-browserslist-db "^1.0.10" +browserslist@^4.23.3: + version "4.24.0" + resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.24.0.tgz#a1325fe4bc80b64fda169629fc01b3d6cecd38d4" + integrity sha512-Rmb62sR1Zpjql25eSanFGEhAxcFwfA1K0GuQcLoaJBAcENegrQut3hYdhXFF1obQfiDyqIW/cLM5HSJ/9k884A== + dependencies: + caniuse-lite "^1.0.30001663" + electron-to-chromium "^1.5.28" + node-releases "^2.0.18" + update-browserslist-db "^1.1.0" + busboy@1.6.0: version "1.6.0" resolved "https://registry.yarnpkg.com/busboy/-/busboy-1.6.0.tgz#966ea36a9502e43cdb9146962523b92f531f6893" @@ -2641,6 +2663,11 @@ caniuse-lite@^1.0.30001449: resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001486.tgz#56a08885228edf62cbe1ac8980f2b5dae159997e" integrity sha512-uv7/gXuHi10Whlj0pp5q/tsK/32J2QSqVRKQhs2j8VsDCjgyruAh/eEXHF822VqO9yT6iZKw3nRwZRSPBE9OQg== +caniuse-lite@^1.0.30001646, caniuse-lite@^1.0.30001663: + version "1.0.30001664" + resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001664.tgz#d588d75c9682d3301956b05a3749652a80677df4" + integrity sha512-AmE7k4dXiNKQipgn7a2xg558IRqPN3jMQY/rOsbxDhrd0tyChwbITBfiwtnqz8bi2M5mIWbxAYBvk7W7QBUS2g== + ccount@^2.0.0: version "2.0.1" resolved "https://registry.yarnpkg.com/ccount/-/ccount-2.0.1.tgz#17a3bf82302e0870d6da43a01311a8bc02a3ecf5" @@ -3153,6 +3180,11 @@ electron-to-chromium@^1.4.284: resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.4.392.tgz#57ec91fa02393ab32e46df6925ef309642a44680" integrity sha512-TXQOMW9tnhIms3jGy/lJctLjICOgyueZFJ1KUtm6DTQ+QpxX3p7ZBwB6syuZ9KBuT5S4XX7bgY1ECPgfxKUdOg== +electron-to-chromium@^1.5.28: + version "1.5.29" + resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.5.29.tgz#aa592a3caa95d07cc26a66563accf99fa573a1ee" + integrity sha512-PF8n2AlIhCKXQ+gTpiJi0VhcHDb69kYX4MtCiivctc2QD3XuNZ/XIOlbGzt7WAjjEev0TtaH6Cu3arZExm5DOw== + emoji-regex@^8.0.0: version "8.0.0" resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-8.0.0.tgz#e818fd69ce5ccfcb404594f842963bf53164cc37" @@ -3369,6 +3401,11 @@ escalade@^3.1.1: resolved "https://registry.yarnpkg.com/escalade/-/escalade-3.1.1.tgz#d8cfdc7000965c5a0174b4a82eaa5c0552742e40" integrity sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw== +escalade@^3.2.0: + version "3.2.0" + resolved "https://registry.yarnpkg.com/escalade/-/escalade-3.2.0.tgz#011a3f69856ba189dffa7dc8fcce99d2a87903e5" + integrity sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA== + escape-html@~1.0.3: version "1.0.3" resolved "https://registry.yarnpkg.com/escape-html/-/escape-html-1.0.3.tgz#0258eae4d3d0c0974de1c169188ef0051d1d1988" @@ -3858,6 +3895,11 @@ forwarded@0.2.0: resolved "https://registry.yarnpkg.com/forwarded/-/forwarded-0.2.0.tgz#2269936428aad4c15c7ebe9779a84bf0b2a81811" integrity sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow== +fraction.js@^4.3.7: + version "4.3.7" + resolved "https://registry.yarnpkg.com/fraction.js/-/fraction.js-4.3.7.tgz#06ca0085157e42fda7f9e726e79fefc4068840f7" + integrity sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew== + fresh@0.5.2: version "0.5.2" resolved "https://registry.yarnpkg.com/fresh/-/fresh-0.5.2.tgz#3d8cadd90d976569fa835ab1f8e4b23a105605a7" @@ -5303,6 +5345,11 @@ next@^13.4.2: "@next/swc-win32-ia32-msvc" "13.4.2" "@next/swc-win32-x64-msvc" "13.4.2" +node-releases@^2.0.18: + version "2.0.18" + resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-2.0.18.tgz#f010e8d35e2fe8d6b2944f03f70213ecedc4ca3f" + integrity sha512-d9VeXT4SJ7ZeOqGX6R5EM022wpL+eWPooLI+5UpWn2jCT1aosUQEhQP214x33Wkwx3JQMvIm+tIoVOdodFS40g== + node-releases@^2.0.8: version "2.0.10" resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-2.0.10.tgz#c311ebae3b6a148c89b1813fd7c4d3c024ef537f" @@ -5313,6 +5360,11 @@ normalize-path@^3.0.0: resolved "https://registry.yarnpkg.com/normalize-path/-/normalize-path-3.0.0.tgz#0dcd69ff23a1c9b11fd0978316644a0388216a65" integrity sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA== +normalize-range@^0.1.2: + version "0.1.2" + resolved "https://registry.yarnpkg.com/normalize-range/-/normalize-range-0.1.2.tgz#2d10c06bdfd312ea9777695a4d28439456b75942" + integrity sha512-bdok/XvKII3nUpklnV6P2hxtMNrCboOjAcyBuQnWEhO665FwrSNRxU+AqpsyvO6LgGYPspN+lu5CLtw4jPRKNA== + npm-run-path@^4.0.0: version "4.0.1" resolved "https://registry.yarnpkg.com/npm-run-path/-/npm-run-path-4.0.1.tgz#b7ecd1e5ed53da8e37a55e1c2269e0b97ed748ea" @@ -5534,6 +5586,11 @@ picocolors@^1.0.0: resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-1.0.0.tgz#cb5bdc74ff3f51892236eaf79d68bc44564ab81c" integrity sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ== +picocolors@^1.0.1, picocolors@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-1.1.0.tgz#5358b76a78cde483ba5cef6a9dc9671440b27d59" + integrity sha512-TQ92mBOW0l3LeMeyLV6mzy/kWr8lkd/hp3mTg7wYK7zJhuBStmGMBG0BdeDZS/dZx1IukaX6Bk11zcln25o1Aw== + picomatch@^2.2.3, picomatch@^2.3.1: version "2.3.1" resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.3.1.tgz#3ba3833733646d9d3e4995946c1365a67fb07a42" @@ -5563,6 +5620,11 @@ possible-typed-array-names@^1.0.0: resolved "https://registry.yarnpkg.com/possible-typed-array-names/-/possible-typed-array-names-1.0.0.tgz#89bb63c6fada2c3e90adc4a647beeeb39cc7bf8f" integrity sha512-d7Uw+eZoloe0EHDIYoe+bQ5WXnGMOpmiZFTuMWCwpjzzkL2nTjcKiAk4hh8TjnGye2TwWOk3UXucZ+3rbmBa8Q== +postcss-value-parser@^4.2.0: + version "4.2.0" + resolved "https://registry.yarnpkg.com/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz#723c09920836ba6d3e5af019f92bc0971c02e514" + integrity sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ== + postcss@8.4.14: version "8.4.14" resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.4.14.tgz#ee9274d5622b4858c1007a74d76e42e56fd21caf" @@ -6843,6 +6905,14 @@ update-browserslist-db@^1.0.10: escalade "^3.1.1" picocolors "^1.0.0" +update-browserslist-db@^1.1.0: + version "1.1.1" + resolved "https://registry.yarnpkg.com/update-browserslist-db/-/update-browserslist-db-1.1.1.tgz#80846fba1d79e82547fb661f8d141e0945755fe5" + integrity sha512-R8UzCaa9Az+38REPiJ1tXlImTJXlVfgHZsglwBD/k6nj76ctsH1E3q4doGrukiLQd3sGQYu56r5+lo5r94l29A== + dependencies: + escalade "^3.2.0" + picocolors "^1.1.0" + uri-js@^4.2.2: version "4.4.1" resolved "https://registry.yarnpkg.com/uri-js/-/uri-js-4.4.1.tgz#9b1a52595225859e55f669d928f88c6c57f2a77e" From 869d5f5d94d95994e615a0df54abaad00afdd8c1 Mon Sep 17 00:00:00 2001 From: benjmnxu Date: Fri, 11 Oct 2024 17:11:37 -0400 Subject: [PATCH 02/11] backend for timers --- ...1_queue_question_timer_enabled_and_more.py | 23 +++++++++++++++++++ backend/ohq/models.py | 4 ++++ backend/ohq/serializers.py | 2 ++ 3 files changed, 29 insertions(+) create mode 100644 backend/ohq/migrations/0021_queue_question_timer_enabled_and_more.py diff --git a/backend/ohq/migrations/0021_queue_question_timer_enabled_and_more.py b/backend/ohq/migrations/0021_queue_question_timer_enabled_and_more.py new file mode 100644 index 00000000..e72bb68b --- /dev/null +++ b/backend/ohq/migrations/0021_queue_question_timer_enabled_and_more.py @@ -0,0 +1,23 @@ +# Generated by Django 5.0.3 on 2024-10-11 21:08 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("ohq", "0020_auto_20240326_0226"), + ] + + operations = [ + migrations.AddField( + model_name="queue", + name="question_timer_enabled", + field=models.BooleanField(default=False), + ), + migrations.AddField( + model_name="queue", + name="question_timer_start_time", + field=models.IntegerField(blank=True, null=True), + ), + ] diff --git a/backend/ohq/models.py b/backend/ohq/models.py index 316951c1..31b59e9a 100644 --- a/backend/ohq/models.py +++ b/backend/ohq/models.py @@ -75,6 +75,7 @@ class Course(models.Model): invite_only = models.BooleanField(default=False) members = models.ManyToManyField(User, through="Membership", through_fields=("course", "user")) + # MAX_NUMBER_COURSE_USERS = 1000 class Meta: @@ -215,6 +216,9 @@ class Queue(models.Model): rate_limit_questions = models.IntegerField(blank=True, null=True) rate_limit_minutes = models.IntegerField(blank=True, null=True) + question_timer_enabled = models.BooleanField(default=False) + question_timer_start_time = models.IntegerField(blank=True, null=True) + video_chat_setting = models.CharField( max_length=8, choices=VIDEO_CHOICES, default=VIDEO_OPTIONAL ) diff --git a/backend/ohq/serializers.py b/backend/ohq/serializers.py index d43e2752..1ae87c58 100644 --- a/backend/ohq/serializers.py +++ b/backend/ohq/serializers.py @@ -157,6 +157,8 @@ class Meta: "rate_limit_length", "rate_limit_questions", "rate_limit_minutes", + "question_timer_enabled", + "question_timer_start_time", "video_chat_setting", "pin", "pin_enabled", From d2f2ab782fa0052108f01fd97a28f0c7456f9509 Mon Sep 17 00:00:00 2001 From: Ryan Tanenholz <44756861+RockCrafts@users.noreply.github.com> Date: Fri, 11 Oct 2024 15:14:56 -0400 Subject: [PATCH 03/11] added queue form options TODO: fix create form --- .../QueueSettings/QueueForm.tsx | 56 +++++++++++++++++++ frontend/types.tsx | 39 ++++++++++--- 2 files changed, 87 insertions(+), 8 deletions(-) diff --git a/frontend/components/Course/InstructorQueuePage/QueueSettings/QueueForm.tsx b/frontend/components/Course/InstructorQueuePage/QueueSettings/QueueForm.tsx index b656c436..4a5d572f 100644 --- a/frontend/components/Course/InstructorQueuePage/QueueSettings/QueueForm.tsx +++ b/frontend/components/Course/InstructorQueuePage/QueueSettings/QueueForm.tsx @@ -24,6 +24,8 @@ interface QueueFormInput { rateLimitLength?: number; rateLimitQuestions?: number; rateLimitMinutes?: number; + questionTimerEnabled: boolean; + questionTimerStartTime?: number; } enum RateLimitFields { @@ -32,6 +34,10 @@ enum RateLimitFields { RATE_LIMIT_LENGTH = "rateLimitLength", } +enum QuestionTimerFields { + QUESTION_TIMER_START_TIME = "questionTimerStartTime", +} + const castInt = (n: string): number | undefined => { let casted: number | undefined = parseInt(n, 10); if (isNaN(casted)) { @@ -67,11 +73,16 @@ const QueueForm = (props: QueueFormProps) => { rateLimitQuestions: queue.rateLimitEnabled ? queue.rateLimitQuestions : undefined, + questionTimerEnabled: queue.questionTimerEnabled, + questionTimerStartTime: queue.questionTimerEnabled + ? queue.questionTimerStartTime + : undefined, }); const [validQuestionRate, setValidQuestionRate] = useState(true); const [validMinsRate, setValidMinsRate] = useState(true); const [validLenRate, setValidLenRate] = useState(true); + const [validQuestionTime, setValidQuestionTime] = useState(true); const [nameCharCount, setNameCharCount] = useState(input.name.length); const [descCharCount, setDescCharCount] = useState( input.description.length @@ -144,6 +155,11 @@ const QueueForm = (props: QueueFormProps) => { setValidLenRate(input[name] >= 0); } + if (name === QuestionTimerFields.QUESTION_TIMER_START_TIME) { + input[name] = castInt(input[name]); + setValidQuestionTime(input[name] > 0); + } + setInput({ ...input }); setDescCharCount(input.description.length); setTemplCharCount(input.questionTemplate.length); @@ -376,6 +392,46 @@ const QueueForm = (props: QueueFormProps) => { /> + + + + setInput({ + ...input, + questionTimerEnabled: + !input.questionTimerEnabled, + }) + } + /> + + + {input.questionTimerEnabled && ( + + + + + + )} - setOpen(true)}> - Archive - - } - > - Archive Queue - - You are about to archive this queue:{" "} - {queue.name}. - - - + setOpen(true)}> + Archive + + } + > + Archive Queue + + You are about to archive this queue:{" "} + {queue.name}. + + +