analytica-frontend-lib 1.5.39 → 1.5.41
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/ActivitiesHistory/index.js +11 -11
- package/dist/ActivitiesHistory/index.mjs +10 -10
- package/dist/ActivityCardQuestionBanks/index.js +11 -11
- package/dist/ActivityCardQuestionBanks/index.mjs +10 -10
- package/dist/ActivityCardQuestionPreview/index.js +11 -11
- package/dist/ActivityCardQuestionPreview/index.mjs +10 -10
- package/dist/ActivityCreate/index.js +11 -11
- package/dist/ActivityCreate/index.mjs +10 -10
- package/dist/ActivityDetails/index.js +11 -11
- package/dist/ActivityDetails/index.mjs +10 -10
- package/dist/ActivityFilters/index.js +11 -11
- package/dist/ActivityFilters/index.mjs +10 -10
- package/dist/ActivityPageLayout/index.js +11 -11
- package/dist/ActivityPageLayout/index.mjs +10 -10
- package/dist/ActivityPreview/index.js +11 -11
- package/dist/ActivityPreview/index.mjs +10 -10
- package/dist/AlertManager/index.js +11 -11
- package/dist/AlertManager/index.mjs +10 -10
- package/dist/AppHeader/index.d.ts.map +1 -1
- package/dist/AppHeader/index.js +5 -5
- package/dist/AppHeader/index.mjs +4 -4
- package/dist/CalendarCard/index.d.ts.map +1 -1
- package/dist/CalendarCard/index.js +3 -3
- package/dist/CalendarCard/index.mjs +2 -2
- package/dist/CheckBoxGroup/index.js +11 -11
- package/dist/CheckBoxGroup/index.mjs +10 -10
- package/dist/DropdownMenu/index.d.ts +8 -0
- package/dist/DropdownMenu/index.d.ts.map +1 -1
- package/dist/DropdownMenu/index.js +2 -2
- package/dist/DropdownMenu/index.mjs +1 -1
- package/dist/ExamDetailsLayout/ExamDetailsPage/index.js +11 -11
- package/dist/ExamDetailsLayout/ExamDetailsPage/index.mjs +10 -10
- package/dist/ExamPageLayout/index.js +11 -11
- package/dist/ExamPageLayout/index.mjs +10 -10
- package/dist/Forum/index.js +3 -3
- package/dist/Forum/index.mjs +2 -2
- package/dist/NotificationCard/index.js +3 -3
- package/dist/NotificationCard/index.mjs +2 -2
- package/dist/PrintableUsersTable/index.js +11 -11
- package/dist/PrintableUsersTable/index.mjs +10 -10
- package/dist/RecommendedLessonCreate/index.js +11 -11
- package/dist/RecommendedLessonCreate/index.mjs +10 -10
- package/dist/RecommendedLessonsHistory/index.js +11 -11
- package/dist/RecommendedLessonsHistory/index.mjs +10 -10
- package/dist/Search/index.js +3 -3
- package/dist/Search/index.mjs +2 -2
- package/dist/SendActivityModal/SendActivityModal.js +11 -11
- package/dist/SendActivityModal/SendActivityModal.mjs +10 -10
- package/dist/SendActivityModal/index.js +11 -11
- package/dist/SendActivityModal/index.mjs +10 -10
- package/dist/SendLessonModal/index.js +11 -11
- package/dist/SendLessonModal/index.mjs +10 -10
- package/dist/SimulatedPerformance/SimulatedPerformanceView/index.js +11 -11
- package/dist/SimulatedPerformance/SimulatedPerformanceView/index.mjs +10 -10
- package/dist/SimulationsPage/index.js +11 -11
- package/dist/SimulationsPage/index.mjs +10 -10
- package/dist/TableProvider/index.js +11 -11
- package/dist/TableProvider/index.mjs +10 -10
- package/dist/UnifiedDraftModelPage/index.js +11 -11
- package/dist/UnifiedDraftModelPage/index.mjs +10 -10
- package/dist/UnifiedHistoryPage/index.js +11 -11
- package/dist/UnifiedHistoryPage/index.mjs +10 -10
- package/dist/{chunk-OCIBLTVL.js → chunk-2DPLSITC.js} +58 -31
- package/dist/chunk-2DPLSITC.js.map +1 -0
- package/dist/{chunk-Q5VRX5TR.js → chunk-4SBNOQ4H.js} +6 -5
- package/dist/chunk-4SBNOQ4H.js.map +1 -0
- package/dist/{chunk-AFBTGILL.mjs → chunk-4TBI57KA.mjs} +20 -13
- package/dist/{chunk-AFBTGILL.mjs.map → chunk-4TBI57KA.mjs.map} +1 -1
- package/dist/{chunk-MTZ6C74W.js → chunk-55TDQZMC.js} +10 -6
- package/dist/chunk-55TDQZMC.js.map +1 -0
- package/dist/{chunk-SIER6LRJ.mjs → chunk-66A37LW7.mjs} +6 -2
- package/dist/chunk-66A37LW7.mjs.map +1 -0
- package/dist/{chunk-EWG6Q2QF.mjs → chunk-6MZEJVNV.mjs} +3 -2
- package/dist/{chunk-EWG6Q2QF.mjs.map → chunk-6MZEJVNV.mjs.map} +1 -1
- package/dist/{chunk-FA6GCOJW.js → chunk-DH6WK44A.js} +7 -7
- package/dist/{chunk-FA6GCOJW.js.map → chunk-DH6WK44A.js.map} +1 -1
- package/dist/{chunk-MVEXCJVJ.js → chunk-DOIJGYYP.js} +10 -10
- package/dist/{chunk-MVEXCJVJ.js.map → chunk-DOIJGYYP.js.map} +1 -1
- package/dist/{chunk-LH4UP3H6.mjs → chunk-DTVNS6NN.mjs} +2 -2
- package/dist/{chunk-HFY4S3JV.mjs → chunk-GAKQ52B6.mjs} +2 -2
- package/dist/chunk-GAKQ52B6.mjs.map +1 -0
- package/dist/{chunk-2MDHKQ33.js → chunk-KHES64RG.js} +6 -6
- package/dist/{chunk-2MDHKQ33.js.map → chunk-KHES64RG.js.map} +1 -1
- package/dist/{chunk-MLOZE23Y.mjs → chunk-KX6BL4OV.mjs} +1 -1
- package/dist/{chunk-MLOZE23Y.mjs.map → chunk-KX6BL4OV.mjs.map} +1 -1
- package/dist/{chunk-VMWF6DND.js → chunk-LZSPF5CN.js} +2 -2
- package/dist/chunk-LZSPF5CN.js.map +1 -0
- package/dist/{chunk-5RBZCQGH.mjs → chunk-NI6GSD6A.mjs} +4 -4
- package/dist/chunk-NI6GSD6A.mjs.map +1 -0
- package/dist/{chunk-EBGFR7IQ.mjs → chunk-RS2OOTJS.mjs} +2 -2
- package/dist/{chunk-TYQP2J4H.js → chunk-TU3BJQBV.js} +30 -23
- package/dist/chunk-TU3BJQBV.js.map +1 -0
- package/dist/{chunk-CFLFCBRM.js → chunk-UVY6KLC7.js} +17 -17
- package/dist/{chunk-CFLFCBRM.js.map → chunk-UVY6KLC7.js.map} +1 -1
- package/dist/{chunk-7LRIQOCS.mjs → chunk-VAYB6LNV.mjs} +51 -24
- package/dist/chunk-VAYB6LNV.mjs.map +1 -0
- package/dist/{chunk-FZUPX44N.mjs → chunk-VG5EMAOF.mjs} +3 -3
- package/dist/{chunk-FZUPX44N.mjs.map → chunk-VG5EMAOF.mjs.map} +1 -1
- package/dist/{chunk-TXHL7XLU.js → chunk-VSQ7UFGJ.js} +1 -1
- package/dist/{chunk-TXHL7XLU.js.map → chunk-VSQ7UFGJ.js.map} +1 -1
- package/dist/hooks/useAppContent/index.js +11 -11
- package/dist/hooks/useAppContent/index.mjs +10 -10
- package/dist/hooks/useRecommendedLessonsPage/index.js +2 -2
- package/dist/hooks/useRecommendedLessonsPage/index.mjs +1 -1
- package/dist/index.js +11 -11
- package/dist/index.mjs +10 -10
- package/dist/types/lessonAvailability/index.d.ts +7 -0
- package/dist/types/lessonAvailability/index.d.ts.map +1 -1
- package/dist/types/lessonAvailability/index.js +2 -2
- package/dist/types/lessonAvailability/index.mjs +1 -1
- package/dist/types/lessonAvailability.d.ts +7 -0
- package/dist/types/lessonAvailability.d.ts.map +1 -1
- package/dist/utils/lessonAvailabilityUtils/index.d.ts +1 -0
- package/dist/utils/lessonAvailabilityUtils/index.d.ts.map +1 -1
- package/dist/utils/lessonAvailabilityUtils/index.js +3 -3
- package/dist/utils/lessonAvailabilityUtils/index.mjs +2 -2
- package/dist/utils/lessonAvailabilityUtils.d.ts +1 -0
- package/dist/utils/lessonAvailabilityUtils.d.ts.map +1 -1
- package/package.json +1 -1
- package/dist/chunk-5RBZCQGH.mjs.map +0 -1
- package/dist/chunk-7LRIQOCS.mjs.map +0 -1
- package/dist/chunk-HFY4S3JV.mjs.map +0 -1
- package/dist/chunk-MTZ6C74W.js.map +0 -1
- package/dist/chunk-OCIBLTVL.js.map +0 -1
- package/dist/chunk-Q5VRX5TR.js.map +0 -1
- package/dist/chunk-SIER6LRJ.mjs.map +0 -1
- package/dist/chunk-TYQP2J4H.js.map +0 -1
- package/dist/chunk-VMWF6DND.js.map +0 -1
- /package/dist/{chunk-LH4UP3H6.mjs.map → chunk-DTVNS6NN.mjs.map} +0 -0
- /package/dist/{chunk-EBGFR7IQ.mjs.map → chunk-RS2OOTJS.mjs.map} +0 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["/home/runner/work/analytica-frontend-lib/analytica-frontend-lib/dist/chunk-55TDQZMC.js","../src/utils/lessonAvailabilityUtils.ts"],"names":[],"mappings":"AAAA;AACE;AACF,sDAA4B;AAC5B;AACE;AACF,sDAA4B;AAC5B;AACA;ACFA,4EAAkB;AAClB,8BAAO;AAkBP,IAAM,qBAAA,EAAuB,CAAC,UAAA,EAAA,GAC5B,6BAAA,UAAgB,CAAA,CAAE,MAAA,CAAO,OAAO,CAAA,CAAE,MAAA,CAAO,qCAAkC,CAAA;AAO7E,IAAM,aAAA,EAAe,CAAC,UAAA,EAAA,GAAgC;AACpD,EAAA,MAAM,KAAA,EAAO,IAAI,IAAA,CAAK,UAAU,CAAA;AAChC,EAAA,MAAM,IAAA,kBAAM,IAAI,IAAA,CAAK,CAAA;AACrB,EAAA,OAAO,IAAA,EAAM,IAAA;AACf,CAAA;AAOA,IAAM,eAAA,EAAiB,CAAC,UAAA,EAAA,GAAgC;AACtD,EAAA,MAAM,KAAA,EAAO,IAAI,IAAA,CAAK,UAAU,CAAA;AAChC,EAAA,MAAM,IAAA,kBAAM,IAAI,IAAA,CAAK,CAAA;AACrB,EAAA,OAAO,IAAA,EAAM,IAAA;AACf,CAAA;AAcO,IAAM,wBAAA,EAA0B,CACrC,SAAA,EACA,SAAA,EAAA,GAC6B;AAC7B,EAAA,MAAM,MAAA,EAAQ,UAAA,EAAY,IAAI,IAAA,CAAK,SAAS,EAAA,EAAI,IAAA;AAChD,EAAA,MAAM,IAAA,EAAM,UAAA,EAAY,IAAI,IAAA,CAAK,SAAS,EAAA,EAAI,IAAA;AAE9C,EAAA,IAAI,OAAA,EAA6B,oCAAA,CAAoB,UAAA;AAErD,EAAA,GAAA,CAAI,UAAA,GAAa,cAAA,CAAe,SAAS,CAAA,EAAG;AAC1C,IAAA,OAAA,EAAS,oCAAA,CAAoB,YAAA;AAAA,EAC/B,EAAA,KAAA,GAAA,CAAW,UAAA,GAAa,YAAA,CAAa,SAAS,CAAA,EAAG;AAC/C,IAAA,OAAA,EAAS,oCAAA,CAAoB,QAAA;AAAA,EAC/B;AAEA,EAAA,OAAO;AAAA,IACL,MAAA;AAAA,IACA,SAAA,EAAW,KAAA;AAAA,IACX,OAAA,EAAS,GAAA;AAAA,IACT,kBAAA,EAAoB,UAAA,EAAY,oDAAA,SAA+B,EAAA,EAAI,IAAA;AAAA,IACnE,sBAAA,EAAwB,UAAA,EAAY,oBAAA,CAAqB,SAAS,EAAA,EAAI,IAAA;AAAA,IACtE,gBAAA,EAAkB,UAAA,EAAY,oDAAA,SAA+B,EAAA,EAAI;AAAA,EACnE,CAAA;AACF,CAAA;AAcO,IAAM,wBAAA,EAA0B,CAAC,SAAA,EAAA,GAAsC;AAC5E,EAAA,GAAA,CAAI,CAAC,SAAA,EAAW,OAAO,KAAA;AACvB,EAAA,OAAO,cAAA,CAAe,SAAS,CAAA;AACjC,CAAA;AAcO,IAAM,gBAAA,EAAkB,CAAC,SAAA,EAAA,GAAsC;AACpE,EAAA,GAAA,CAAI,CAAC,SAAA,EAAW,OAAO,KAAA;AACvB,EAAA,OAAO,YAAA,CAAa,SAAS,CAAA;AAC/B,CAAA;ADvEA;AACA;AACE;AACA;AACA;AACF,gKAAC","file":"/home/runner/work/analytica-frontend-lib/analytica-frontend-lib/dist/chunk-55TDQZMC.js","sourcesContent":[null,"/**\n * Lesson Availability Utilities\n * Helper functions for checking recommended lesson availability based on dates\n */\n\nimport dayjs from 'dayjs';\nimport 'dayjs/locale/pt-br';\nimport {\n LESSON_AVAILABILITY,\n type LessonAvailability,\n type LessonAvailabilityResult,\n} from '../types/lessonAvailability';\nimport { formatDateToBrazilian } from './activityDetailsUtils';\n\n/**\n * Format a lesson date including its time, in LOCAL time.\n *\n * Lesson start/final dates are full timestamps — a teacher can schedule a\n * lesson to open at 14:30. Showing only the day (see formatDateToBrazilian,\n * which also formats in UTC) makes a lesson that opens later today look like\n * it should already be available.\n * @param dateString - ISO date string\n * @returns Formatted date string \"D de MMMM de YYYY às HH:mm\"\n */\nconst formatLessonDateTime = (dateString: string): string =>\n dayjs(dateString).locale('pt-br').format('D [de] MMMM [de] YYYY [às] HH:mm');\n\n/**\n * Check if a date string represents a date in the past\n * @param dateString - ISO date string to check\n * @returns true if the date is in the past\n */\nconst isDateInPast = (dateString: string): boolean => {\n const date = new Date(dateString);\n const now = new Date();\n return now > date;\n};\n\n/**\n * Check if a date string represents a date in the future\n * @param dateString - ISO date string to check\n * @returns true if the date is in the future\n */\nconst isDateInFuture = (dateString: string): boolean => {\n const date = new Date(dateString);\n const now = new Date();\n return now < date;\n};\n\n/**\n * Check lesson availability based on start and end dates\n * @param startDate - ISO string for lesson start date (when it becomes available)\n * @param finalDate - ISO string for lesson end date (when it expires)\n * @returns LessonAvailabilityResult with status and formatted dates\n *\n * @example\n * ```typescript\n * const result = checkLessonAvailability('2024-01-01', '2024-12-31');\n * // { status: 'DISPONIVEL', startDate: Date, endDate: Date, ... }\n * ```\n */\nexport const checkLessonAvailability = (\n startDate: string | null,\n finalDate: string | null\n): LessonAvailabilityResult => {\n const start = startDate ? new Date(startDate) : null;\n const end = finalDate ? new Date(finalDate) : null;\n\n let status: LessonAvailability = LESSON_AVAILABILITY.DISPONIVEL;\n\n if (startDate && isDateInFuture(startDate)) {\n status = LESSON_AVAILABILITY.NAO_INICIADA;\n } else if (finalDate && isDateInPast(finalDate)) {\n status = LESSON_AVAILABILITY.EXPIRADA;\n }\n\n return {\n status,\n startDate: start,\n endDate: end,\n formattedStartDate: startDate ? formatDateToBrazilian(startDate) : null,\n formattedStartDateTime: startDate ? formatLessonDateTime(startDate) : null,\n formattedEndDate: finalDate ? formatDateToBrazilian(finalDate) : null,\n };\n};\n\n/**\n * Check if a lesson is not yet available (before start date)\n * @param startDate - ISO string for lesson start date\n * @returns true if the lesson has not started yet\n *\n * @example\n * ```typescript\n * if (isLessonNotYetAvailable('2025-01-01')) {\n * showNotAvailableModal();\n * }\n * ```\n */\nexport const isLessonNotYetAvailable = (startDate: string | null): boolean => {\n if (!startDate) return false;\n return isDateInFuture(startDate);\n};\n\n/**\n * Check if a lesson has expired (after end date)\n * @param finalDate - ISO string for lesson end date\n * @returns true if the lesson has expired\n *\n * @example\n * ```typescript\n * if (isLessonExpired('2023-12-31')) {\n * showExpiredView();\n * }\n * ```\n */\nexport const isLessonExpired = (finalDate: string | null): boolean => {\n if (!finalDate) return false;\n return isDateInPast(finalDate);\n};\n"]}
|
|
@@ -1,11 +1,14 @@
|
|
|
1
1
|
import {
|
|
2
2
|
LESSON_AVAILABILITY
|
|
3
|
-
} from "./chunk-
|
|
3
|
+
} from "./chunk-KX6BL4OV.mjs";
|
|
4
4
|
import {
|
|
5
5
|
formatDateToBrazilian
|
|
6
6
|
} from "./chunk-YG7W6JE3.mjs";
|
|
7
7
|
|
|
8
8
|
// src/utils/lessonAvailabilityUtils.ts
|
|
9
|
+
import dayjs from "dayjs";
|
|
10
|
+
import "dayjs/locale/pt-br";
|
|
11
|
+
var formatLessonDateTime = (dateString) => dayjs(dateString).locale("pt-br").format("D [de] MMMM [de] YYYY [\xE0s] HH:mm");
|
|
9
12
|
var isDateInPast = (dateString) => {
|
|
10
13
|
const date = new Date(dateString);
|
|
11
14
|
const now = /* @__PURE__ */ new Date();
|
|
@@ -30,6 +33,7 @@ var checkLessonAvailability = (startDate, finalDate) => {
|
|
|
30
33
|
startDate: start,
|
|
31
34
|
endDate: end,
|
|
32
35
|
formattedStartDate: startDate ? formatDateToBrazilian(startDate) : null,
|
|
36
|
+
formattedStartDateTime: startDate ? formatLessonDateTime(startDate) : null,
|
|
33
37
|
formattedEndDate: finalDate ? formatDateToBrazilian(finalDate) : null
|
|
34
38
|
};
|
|
35
39
|
};
|
|
@@ -47,4 +51,4 @@ export {
|
|
|
47
51
|
isLessonNotYetAvailable,
|
|
48
52
|
isLessonExpired
|
|
49
53
|
};
|
|
50
|
-
//# sourceMappingURL=chunk-
|
|
54
|
+
//# sourceMappingURL=chunk-66A37LW7.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/utils/lessonAvailabilityUtils.ts"],"sourcesContent":["/**\n * Lesson Availability Utilities\n * Helper functions for checking recommended lesson availability based on dates\n */\n\nimport dayjs from 'dayjs';\nimport 'dayjs/locale/pt-br';\nimport {\n LESSON_AVAILABILITY,\n type LessonAvailability,\n type LessonAvailabilityResult,\n} from '../types/lessonAvailability';\nimport { formatDateToBrazilian } from './activityDetailsUtils';\n\n/**\n * Format a lesson date including its time, in LOCAL time.\n *\n * Lesson start/final dates are full timestamps — a teacher can schedule a\n * lesson to open at 14:30. Showing only the day (see formatDateToBrazilian,\n * which also formats in UTC) makes a lesson that opens later today look like\n * it should already be available.\n * @param dateString - ISO date string\n * @returns Formatted date string \"D de MMMM de YYYY às HH:mm\"\n */\nconst formatLessonDateTime = (dateString: string): string =>\n dayjs(dateString).locale('pt-br').format('D [de] MMMM [de] YYYY [às] HH:mm');\n\n/**\n * Check if a date string represents a date in the past\n * @param dateString - ISO date string to check\n * @returns true if the date is in the past\n */\nconst isDateInPast = (dateString: string): boolean => {\n const date = new Date(dateString);\n const now = new Date();\n return now > date;\n};\n\n/**\n * Check if a date string represents a date in the future\n * @param dateString - ISO date string to check\n * @returns true if the date is in the future\n */\nconst isDateInFuture = (dateString: string): boolean => {\n const date = new Date(dateString);\n const now = new Date();\n return now < date;\n};\n\n/**\n * Check lesson availability based on start and end dates\n * @param startDate - ISO string for lesson start date (when it becomes available)\n * @param finalDate - ISO string for lesson end date (when it expires)\n * @returns LessonAvailabilityResult with status and formatted dates\n *\n * @example\n * ```typescript\n * const result = checkLessonAvailability('2024-01-01', '2024-12-31');\n * // { status: 'DISPONIVEL', startDate: Date, endDate: Date, ... }\n * ```\n */\nexport const checkLessonAvailability = (\n startDate: string | null,\n finalDate: string | null\n): LessonAvailabilityResult => {\n const start = startDate ? new Date(startDate) : null;\n const end = finalDate ? new Date(finalDate) : null;\n\n let status: LessonAvailability = LESSON_AVAILABILITY.DISPONIVEL;\n\n if (startDate && isDateInFuture(startDate)) {\n status = LESSON_AVAILABILITY.NAO_INICIADA;\n } else if (finalDate && isDateInPast(finalDate)) {\n status = LESSON_AVAILABILITY.EXPIRADA;\n }\n\n return {\n status,\n startDate: start,\n endDate: end,\n formattedStartDate: startDate ? formatDateToBrazilian(startDate) : null,\n formattedStartDateTime: startDate ? formatLessonDateTime(startDate) : null,\n formattedEndDate: finalDate ? formatDateToBrazilian(finalDate) : null,\n };\n};\n\n/**\n * Check if a lesson is not yet available (before start date)\n * @param startDate - ISO string for lesson start date\n * @returns true if the lesson has not started yet\n *\n * @example\n * ```typescript\n * if (isLessonNotYetAvailable('2025-01-01')) {\n * showNotAvailableModal();\n * }\n * ```\n */\nexport const isLessonNotYetAvailable = (startDate: string | null): boolean => {\n if (!startDate) return false;\n return isDateInFuture(startDate);\n};\n\n/**\n * Check if a lesson has expired (after end date)\n * @param finalDate - ISO string for lesson end date\n * @returns true if the lesson has expired\n *\n * @example\n * ```typescript\n * if (isLessonExpired('2023-12-31')) {\n * showExpiredView();\n * }\n * ```\n */\nexport const isLessonExpired = (finalDate: string | null): boolean => {\n if (!finalDate) return false;\n return isDateInPast(finalDate);\n};\n"],"mappings":";;;;;;;;AAKA,OAAO,WAAW;AAClB,OAAO;AAkBP,IAAM,uBAAuB,CAAC,eAC5B,MAAM,UAAU,EAAE,OAAO,OAAO,EAAE,OAAO,qCAAkC;AAO7E,IAAM,eAAe,CAAC,eAAgC;AACpD,QAAM,OAAO,IAAI,KAAK,UAAU;AAChC,QAAM,MAAM,oBAAI,KAAK;AACrB,SAAO,MAAM;AACf;AAOA,IAAM,iBAAiB,CAAC,eAAgC;AACtD,QAAM,OAAO,IAAI,KAAK,UAAU;AAChC,QAAM,MAAM,oBAAI,KAAK;AACrB,SAAO,MAAM;AACf;AAcO,IAAM,0BAA0B,CACrC,WACA,cAC6B;AAC7B,QAAM,QAAQ,YAAY,IAAI,KAAK,SAAS,IAAI;AAChD,QAAM,MAAM,YAAY,IAAI,KAAK,SAAS,IAAI;AAE9C,MAAI,SAA6B,oBAAoB;AAErD,MAAI,aAAa,eAAe,SAAS,GAAG;AAC1C,aAAS,oBAAoB;AAAA,EAC/B,WAAW,aAAa,aAAa,SAAS,GAAG;AAC/C,aAAS,oBAAoB;AAAA,EAC/B;AAEA,SAAO;AAAA,IACL;AAAA,IACA,WAAW;AAAA,IACX,SAAS;AAAA,IACT,oBAAoB,YAAY,sBAAsB,SAAS,IAAI;AAAA,IACnE,wBAAwB,YAAY,qBAAqB,SAAS,IAAI;AAAA,IACtE,kBAAkB,YAAY,sBAAsB,SAAS,IAAI;AAAA,EACnE;AACF;AAcO,IAAM,0BAA0B,CAAC,cAAsC;AAC5E,MAAI,CAAC,UAAW,QAAO;AACvB,SAAO,eAAe,SAAS;AACjC;AAcO,IAAM,kBAAkB,CAAC,cAAsC;AACpE,MAAI,CAAC,UAAW,QAAO;AACvB,SAAO,aAAa,SAAS;AAC/B;","names":[]}
|
|
@@ -8,7 +8,7 @@ import {
|
|
|
8
8
|
DropdownMenuContent,
|
|
9
9
|
DropdownMenuTrigger,
|
|
10
10
|
DropdownMenu_default
|
|
11
|
-
} from "./chunk-
|
|
11
|
+
} from "./chunk-VAYB6LNV.mjs";
|
|
12
12
|
import {
|
|
13
13
|
Modal_default
|
|
14
14
|
} from "./chunk-HEP6VNLE.mjs";
|
|
@@ -66,6 +66,7 @@ var CalendarCard = ({
|
|
|
66
66
|
DropdownMenuTrigger,
|
|
67
67
|
{
|
|
68
68
|
className: `text-primary cursor-pointer ${className ?? ""}`,
|
|
69
|
+
asChild: true,
|
|
69
70
|
children: /* @__PURE__ */ jsx(IconButton_default, { active: effectiveOpen, icon: triggerIcon })
|
|
70
71
|
}
|
|
71
72
|
),
|
|
@@ -86,4 +87,4 @@ export {
|
|
|
86
87
|
CalendarCard,
|
|
87
88
|
CalendarCard_default
|
|
88
89
|
};
|
|
89
|
-
//# sourceMappingURL=chunk-
|
|
90
|
+
//# sourceMappingURL=chunk-6MZEJVNV.mjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/components/CalendarCard/CalendarCard.tsx"],"sourcesContent":["import { CalendarBlankIcon } from '@phosphor-icons/react/dist/csr/CalendarBlank';\nimport type { ReactNode } from 'react';\nimport { useCallback, useState } from 'react';\nimport DropdownMenu, {\n DropdownMenuContent,\n DropdownMenuTrigger,\n} from '../DropdownMenu/DropdownMenu';\nimport IconButton from '../IconButton/IconButton';\nimport Modal from '../Modal/Modal';\nimport { useMobile } from '../../hooks/useMobile';\n\nexport interface CalendarCardProps {\n /** Content rendered inside the calendar (calendar grid + activities list). */\n content: ReactNode;\n /**\n * Whether the calendar surface is currently open.\n * When provided, the component is **controlled**: the consumer is the source of truth.\n * When omitted, the component manages internal state.\n */\n isOpen?: boolean;\n /**\n * Fired whenever the open state changes (user click, outside click,\n * programmatic close, ESC, etc.).\n */\n onOpenChange?: (open: boolean) => void;\n /** Title displayed in the mobile Modal. Default: \"Calendário\". */\n title?: string;\n /** Optional className applied to the trigger button. */\n className?: string;\n}\n\n/**\n * Calendar widget for the app header. Renders a calendar icon trigger that\n * opens a `DropdownMenu` on tablet/desktop and a `Modal` on mobile (<500px).\n *\n * Pairs naturally with `AppHeader`'s `calendarContent` prop. Use the\n * controlled `isOpen` + `onOpenChange` pair when the consumer needs to close\n * the surface programmatically (e.g. on route change).\n */\nexport const CalendarCard = ({\n content,\n isOpen,\n onOpenChange,\n title = 'Calendário',\n className,\n}: CalendarCardProps) => {\n const { isMobile } = useMobile();\n const [internalOpen, setInternalOpen] = useState(false);\n\n const isControlled = isOpen !== undefined;\n const effectiveOpen = isControlled ? isOpen : internalOpen;\n\n // `useCallback` keeps the reference stable across renders, so the\n // `DropdownMenu`'s `useEffect [open, onOpenChange]` doesn't re-fire on\n // every parent re-render (which is what triggers the infinite update loop\n // observed before).\n const handleOpenChange = useCallback(\n (open: boolean) => {\n if (!isControlled) {\n setInternalOpen(open);\n }\n onOpenChange?.(open);\n },\n [isControlled, onOpenChange]\n );\n\n const iconColor = effectiveOpen ? 'text-primary-950' : 'text-primary';\n const triggerIcon = <CalendarBlankIcon size={24} className={iconColor} />;\n\n if (isMobile) {\n return (\n <>\n <IconButton\n active={effectiveOpen}\n onClick={() => handleOpenChange(!effectiveOpen)}\n icon={triggerIcon}\n className={className}\n />\n <Modal\n isOpen={effectiveOpen}\n onClose={() => handleOpenChange(false)}\n title={title}\n >\n <div className=\"flex justify-center\">{content}</div>\n </Modal>\n </>\n );\n }\n\n return (\n <DropdownMenu open={effectiveOpen} onOpenChange={handleOpenChange}>\n <DropdownMenuTrigger\n className={`text-primary cursor-pointer ${className ?? ''}`}\n >\n <IconButton active={effectiveOpen} icon={triggerIcon} />\n </DropdownMenuTrigger>\n <DropdownMenuContent\n className=\"w-[320px] max-w-[calc(100vw-16px)] max-h-[80vh] overflow-y-auto\"\n align=\"end\"\n side=\"bottom\"\n >\n {content}\n </DropdownMenuContent>\n </DropdownMenu>\n );\n};\n\nexport default CalendarCard;\n"],"mappings":";;;;;;;;;;;;;;;;AAAA,SAAS,yBAAyB;AAElC,SAAS,aAAa,gBAAgB;AAiEhB,SAIhB,UAJgB,KAIhB,YAJgB;AA5Bf,IAAM,eAAe,CAAC;AAAA,EAC3B;AAAA,EACA;AAAA,EACA;AAAA,EACA,QAAQ;AAAA,EACR;AACF,MAAyB;AACvB,QAAM,EAAE,SAAS,IAAI,UAAU;AAC/B,QAAM,CAAC,cAAc,eAAe,IAAI,SAAS,KAAK;AAEtD,QAAM,eAAe,WAAW;AAChC,QAAM,gBAAgB,eAAe,SAAS;AAM9C,QAAM,mBAAmB;AAAA,IACvB,CAAC,SAAkB;AACjB,UAAI,CAAC,cAAc;AACjB,wBAAgB,IAAI;AAAA,MACtB;AACA,qBAAe,IAAI;AAAA,IACrB;AAAA,IACA,CAAC,cAAc,YAAY;AAAA,EAC7B;AAEA,QAAM,YAAY,gBAAgB,qBAAqB;AACvD,QAAM,cAAc,oBAAC,qBAAkB,MAAM,IAAI,WAAW,WAAW;AAEvE,MAAI,UAAU;AACZ,WACE,iCACE;AAAA;AAAA,QAAC;AAAA;AAAA,UACC,QAAQ;AAAA,UACR,SAAS,MAAM,iBAAiB,CAAC,aAAa;AAAA,UAC9C,MAAM;AAAA,UACN;AAAA;AAAA,MACF;AAAA,MACA;AAAA,QAAC;AAAA;AAAA,UACC,QAAQ;AAAA,UACR,SAAS,MAAM,iBAAiB,KAAK;AAAA,UACrC;AAAA,UAEA,8BAAC,SAAI,WAAU,uBAAuB,mBAAQ;AAAA;AAAA,MAChD;AAAA,OACF;AAAA,EAEJ;AAEA,SACE,qBAAC,wBAAa,MAAM,eAAe,cAAc,kBAC/C;AAAA;AAAA,MAAC;AAAA;AAAA,QACC,WAAW,+BAA+B,aAAa,EAAE;AAAA,
|
|
1
|
+
{"version":3,"sources":["../src/components/CalendarCard/CalendarCard.tsx"],"sourcesContent":["import { CalendarBlankIcon } from '@phosphor-icons/react/dist/csr/CalendarBlank';\nimport type { ReactNode } from 'react';\nimport { useCallback, useState } from 'react';\nimport DropdownMenu, {\n DropdownMenuContent,\n DropdownMenuTrigger,\n} from '../DropdownMenu/DropdownMenu';\nimport IconButton from '../IconButton/IconButton';\nimport Modal from '../Modal/Modal';\nimport { useMobile } from '../../hooks/useMobile';\n\nexport interface CalendarCardProps {\n /** Content rendered inside the calendar (calendar grid + activities list). */\n content: ReactNode;\n /**\n * Whether the calendar surface is currently open.\n * When provided, the component is **controlled**: the consumer is the source of truth.\n * When omitted, the component manages internal state.\n */\n isOpen?: boolean;\n /**\n * Fired whenever the open state changes (user click, outside click,\n * programmatic close, ESC, etc.).\n */\n onOpenChange?: (open: boolean) => void;\n /** Title displayed in the mobile Modal. Default: \"Calendário\". */\n title?: string;\n /** Optional className applied to the trigger button. */\n className?: string;\n}\n\n/**\n * Calendar widget for the app header. Renders a calendar icon trigger that\n * opens a `DropdownMenu` on tablet/desktop and a `Modal` on mobile (<500px).\n *\n * Pairs naturally with `AppHeader`'s `calendarContent` prop. Use the\n * controlled `isOpen` + `onOpenChange` pair when the consumer needs to close\n * the surface programmatically (e.g. on route change).\n */\nexport const CalendarCard = ({\n content,\n isOpen,\n onOpenChange,\n title = 'Calendário',\n className,\n}: CalendarCardProps) => {\n const { isMobile } = useMobile();\n const [internalOpen, setInternalOpen] = useState(false);\n\n const isControlled = isOpen !== undefined;\n const effectiveOpen = isControlled ? isOpen : internalOpen;\n\n // `useCallback` keeps the reference stable across renders, so the\n // `DropdownMenu`'s `useEffect [open, onOpenChange]` doesn't re-fire on\n // every parent re-render (which is what triggers the infinite update loop\n // observed before).\n const handleOpenChange = useCallback(\n (open: boolean) => {\n if (!isControlled) {\n setInternalOpen(open);\n }\n onOpenChange?.(open);\n },\n [isControlled, onOpenChange]\n );\n\n const iconColor = effectiveOpen ? 'text-primary-950' : 'text-primary';\n const triggerIcon = <CalendarBlankIcon size={24} className={iconColor} />;\n\n if (isMobile) {\n return (\n <>\n <IconButton\n active={effectiveOpen}\n onClick={() => handleOpenChange(!effectiveOpen)}\n icon={triggerIcon}\n className={className}\n />\n <Modal\n isOpen={effectiveOpen}\n onClose={() => handleOpenChange(false)}\n title={title}\n >\n <div className=\"flex justify-center\">{content}</div>\n </Modal>\n </>\n );\n }\n\n return (\n <DropdownMenu open={effectiveOpen} onOpenChange={handleOpenChange}>\n <DropdownMenuTrigger\n className={`text-primary cursor-pointer ${className ?? ''}`}\n asChild\n >\n <IconButton active={effectiveOpen} icon={triggerIcon} />\n </DropdownMenuTrigger>\n <DropdownMenuContent\n className=\"w-[320px] max-w-[calc(100vw-16px)] max-h-[80vh] overflow-y-auto\"\n align=\"end\"\n side=\"bottom\"\n >\n {content}\n </DropdownMenuContent>\n </DropdownMenu>\n );\n};\n\nexport default CalendarCard;\n"],"mappings":";;;;;;;;;;;;;;;;AAAA,SAAS,yBAAyB;AAElC,SAAS,aAAa,gBAAgB;AAiEhB,SAIhB,UAJgB,KAIhB,YAJgB;AA5Bf,IAAM,eAAe,CAAC;AAAA,EAC3B;AAAA,EACA;AAAA,EACA;AAAA,EACA,QAAQ;AAAA,EACR;AACF,MAAyB;AACvB,QAAM,EAAE,SAAS,IAAI,UAAU;AAC/B,QAAM,CAAC,cAAc,eAAe,IAAI,SAAS,KAAK;AAEtD,QAAM,eAAe,WAAW;AAChC,QAAM,gBAAgB,eAAe,SAAS;AAM9C,QAAM,mBAAmB;AAAA,IACvB,CAAC,SAAkB;AACjB,UAAI,CAAC,cAAc;AACjB,wBAAgB,IAAI;AAAA,MACtB;AACA,qBAAe,IAAI;AAAA,IACrB;AAAA,IACA,CAAC,cAAc,YAAY;AAAA,EAC7B;AAEA,QAAM,YAAY,gBAAgB,qBAAqB;AACvD,QAAM,cAAc,oBAAC,qBAAkB,MAAM,IAAI,WAAW,WAAW;AAEvE,MAAI,UAAU;AACZ,WACE,iCACE;AAAA;AAAA,QAAC;AAAA;AAAA,UACC,QAAQ;AAAA,UACR,SAAS,MAAM,iBAAiB,CAAC,aAAa;AAAA,UAC9C,MAAM;AAAA,UACN;AAAA;AAAA,MACF;AAAA,MACA;AAAA,QAAC;AAAA;AAAA,UACC,QAAQ;AAAA,UACR,SAAS,MAAM,iBAAiB,KAAK;AAAA,UACrC;AAAA,UAEA,8BAAC,SAAI,WAAU,uBAAuB,mBAAQ;AAAA;AAAA,MAChD;AAAA,OACF;AAAA,EAEJ;AAEA,SACE,qBAAC,wBAAa,MAAM,eAAe,cAAc,kBAC/C;AAAA;AAAA,MAAC;AAAA;AAAA,QACC,WAAW,+BAA+B,aAAa,EAAE;AAAA,QACzD,SAAO;AAAA,QAEP,8BAAC,sBAAW,QAAQ,eAAe,MAAM,aAAa;AAAA;AAAA,IACxD;AAAA,IACA;AAAA,MAAC;AAAA;AAAA,QACC,WAAU;AAAA,QACV,OAAM;AAAA,QACN,MAAK;AAAA,QAEJ;AAAA;AAAA,IACH;AAAA,KACF;AAEJ;AAEA,IAAO,uBAAQ;","names":[]}
|
|
@@ -26,7 +26,7 @@ var _chunkLGPTKTMPjs = require('./chunk-LGPTKTMP.js');
|
|
|
26
26
|
|
|
27
27
|
|
|
28
28
|
|
|
29
|
-
var
|
|
29
|
+
var _chunk2DPLSITCjs = require('./chunk-2DPLSITC.js');
|
|
30
30
|
|
|
31
31
|
|
|
32
32
|
var _chunkLS4EGCEJjs = require('./chunk-LS4EGCEJ.js');
|
|
@@ -111,25 +111,25 @@ function AuthorMeta({ authorName, authorPhoto, createdAt }) {
|
|
|
111
111
|
] });
|
|
112
112
|
}
|
|
113
113
|
function PostActionsMenu({ onEdit, onDelete }) {
|
|
114
|
-
return /* @__PURE__ */ _jsxruntime.jsxs.call(void 0,
|
|
114
|
+
return /* @__PURE__ */ _jsxruntime.jsxs.call(void 0, _chunk2DPLSITCjs.DropdownMenu_default, { children: [
|
|
115
115
|
/* @__PURE__ */ _jsxruntime.jsx.call(void 0,
|
|
116
|
-
|
|
116
|
+
_chunk2DPLSITCjs.DropdownMenuTrigger,
|
|
117
117
|
{
|
|
118
118
|
"aria-label": "Abrir a\xE7\xF5es do post",
|
|
119
119
|
className: "p-1 rounded hover:bg-background-100 transition-colors shrink-0",
|
|
120
120
|
children: /* @__PURE__ */ _jsxruntime.jsx.call(void 0, _react3.DotsThreeVerticalIcon, { size: 20, className: "text-text-600" })
|
|
121
121
|
}
|
|
122
122
|
),
|
|
123
|
-
/* @__PURE__ */ _jsxruntime.jsxs.call(void 0,
|
|
123
|
+
/* @__PURE__ */ _jsxruntime.jsxs.call(void 0, _chunk2DPLSITCjs.DropdownMenuContent, { align: "end", children: [
|
|
124
124
|
/* @__PURE__ */ _jsxruntime.jsx.call(void 0,
|
|
125
|
-
|
|
125
|
+
_chunk2DPLSITCjs.DropdownMenuItem,
|
|
126
126
|
{
|
|
127
127
|
iconLeft: /* @__PURE__ */ _jsxruntime.jsx.call(void 0, _react3.PencilSimpleIcon, { size: 16 }),
|
|
128
128
|
onClick: onEdit,
|
|
129
129
|
children: "Editar"
|
|
130
130
|
}
|
|
131
131
|
),
|
|
132
|
-
/* @__PURE__ */ _jsxruntime.jsx.call(void 0,
|
|
132
|
+
/* @__PURE__ */ _jsxruntime.jsx.call(void 0, _chunk2DPLSITCjs.DropdownMenuItem, { iconLeft: /* @__PURE__ */ _jsxruntime.jsx.call(void 0, _react3.TrashIcon, { size: 16 }), onClick: onDelete, children: "Deletar" })
|
|
133
133
|
] })
|
|
134
134
|
] });
|
|
135
135
|
}
|
|
@@ -932,4 +932,4 @@ var Forum_default = Forum;
|
|
|
932
932
|
|
|
933
933
|
|
|
934
934
|
exports.Forum = Forum; exports.Forum_default = Forum_default;
|
|
935
|
-
//# sourceMappingURL=chunk-
|
|
935
|
+
//# sourceMappingURL=chunk-DH6WK44A.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["/home/runner/work/analytica-frontend-lib/analytica-frontend-lib/dist/chunk-FA6GCOJW.js","../src/components/Forum/Forum.tsx"],"names":[],"mappings":"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","file":"/home/runner/work/analytica-frontend-lib/analytica-frontend-lib/dist/chunk-FA6GCOJW.js","sourcesContent":[null,"import { useState, useEffect, useCallback } from 'react';\nimport {\n CaretLeftIcon,\n ChatCircleTextIcon,\n CheckIcon,\n DotsThreeVerticalIcon,\n ImageIcon,\n PencilSimpleIcon,\n TrashIcon,\n} from '@phosphor-icons/react';\nimport Text from '../Text/Text';\nimport Button from '../Button/Button';\nimport Input from '../Input/Input';\nimport TextArea from '../TextArea/TextArea';\nimport Modal from '../Modal/Modal';\nimport Radio from '../Radio/Radio';\nimport DropdownMenu, {\n DropdownMenuTrigger,\n DropdownMenuContent,\n DropdownMenuItem,\n} from '../DropdownMenu/DropdownMenu';\nimport { RichEditor } from '../RichEditor/RichEditor';\nimport ImageUpload from '../ImageUpload/ImageUpload';\nimport { SkeletonList, SkeletonCard } from '../Skeleton/Skeleton';\nimport { stripHtml, sanitizeHtmlForDisplay } from '../HtmlMathRenderer';\nimport { cn } from '../../utils/utils';\nimport type { ForumApiClient, ForumTopic, ForumReply } from '../../types/forum';\nimport { PROFILE_ROLES } from '../../types/chat';\n\n// ---------------------------------------------------------------------------\n// Helpers\n// ---------------------------------------------------------------------------\n\nconst MONTHS_PT = [\n 'Janeiro',\n 'Fevereiro',\n 'Março',\n 'Abril',\n 'Maio',\n 'Junho',\n 'Julho',\n 'Agosto',\n 'Setembro',\n 'Outubro',\n 'Novembro',\n 'Dezembro',\n];\n\nfunction formatForumDate(isoDate: string): string {\n const d = new Date(isoDate);\n const day = d.getDate();\n const month = MONTHS_PT[d.getMonth()];\n const year = d.getFullYear();\n const hours = d.getHours();\n const minutes = d.getMinutes().toString().padStart(2, '0');\n return `${day} ${month} ${year} • ${hours}:${minutes}`;\n}\n\nfunction getInitials(name: string): string {\n return name\n .split(' ')\n .slice(0, 2)\n .map((word) => word[0] ?? '')\n .join('')\n .toUpperCase();\n}\n\nfunction formatReplyCount(count: number): string {\n if (count === 1) return '1 resposta';\n return `${count} respostas`;\n}\n\n// ---------------------------------------------------------------------------\n// Sub-components\n// ---------------------------------------------------------------------------\n\ninterface AuthorAvatarProps {\n readonly name: string;\n readonly photoUrl: string | null;\n}\n\nfunction AuthorAvatar({ name, photoUrl }: AuthorAvatarProps) {\n if (photoUrl) {\n return (\n <img\n src={photoUrl}\n alt={name}\n className=\"w-8 h-8 rounded-full object-cover shrink-0\"\n />\n );\n }\n return (\n <div className=\"w-8 h-8 rounded-full bg-primary-600 flex items-center justify-center shrink-0\">\n <Text size=\"xs\" weight=\"semibold\" color=\"text-white\">\n {getInitials(name)}\n </Text>\n </div>\n );\n}\n\ninterface AuthorMetaProps {\n readonly authorName: string;\n readonly authorPhoto: string | null;\n readonly createdAt: string;\n}\n\nfunction AuthorMeta({ authorName, authorPhoto, createdAt }: AuthorMetaProps) {\n return (\n <div className=\"flex items-center gap-2\">\n <AuthorAvatar name={authorName} photoUrl={authorPhoto} />\n <div className=\"flex items-center gap-1\">\n <Text size=\"xs\" weight=\"semibold\" color=\"text-primary-700\">\n Postado por {authorName}\n </Text>\n <Text size=\"xs\" color=\"text-text-600\">\n • {formatForumDate(createdAt)}\n </Text>\n </div>\n </div>\n );\n}\n\ninterface PostActionsMenuProps {\n readonly onEdit: () => void;\n readonly onDelete: () => void;\n}\n\nfunction PostActionsMenu({ onEdit, onDelete }: PostActionsMenuProps) {\n return (\n <DropdownMenu>\n <DropdownMenuTrigger\n aria-label=\"Abrir ações do post\"\n className=\"p-1 rounded hover:bg-background-100 transition-colors shrink-0\"\n >\n <DotsThreeVerticalIcon size={20} className=\"text-text-600\" />\n </DropdownMenuTrigger>\n <DropdownMenuContent align=\"end\">\n <DropdownMenuItem\n iconLeft={<PencilSimpleIcon size={16} />}\n onClick={onEdit}\n >\n Editar\n </DropdownMenuItem>\n <DropdownMenuItem iconLeft={<TrashIcon size={16} />} onClick={onDelete}>\n Deletar\n </DropdownMenuItem>\n </DropdownMenuContent>\n </DropdownMenu>\n );\n}\n\ninterface PostContentModalProps {\n readonly isOpen: boolean;\n readonly title: string;\n readonly submitLabel: string;\n readonly content: string;\n readonly imageUrl: string | undefined;\n readonly isSubmitting: boolean;\n readonly onContentChange: (value: string) => void;\n readonly onClose: () => void;\n readonly onSubmit: () => void;\n readonly onUploadImage?: (file: File) => Promise<string>;\n readonly onImageUploaded: (url: string) => void;\n readonly maxLength: number;\n /** Use RichEditor with toolbar instead of plain TextArea */\n readonly richText?: boolean;\n /** Show \"Este fórum vale nota no boletim?\" question (professor only) */\n readonly showGradeQuestion?: boolean;\n readonly countsForGrade?: boolean;\n readonly onCountsForGradeChange?: (value: boolean) => void;\n}\n\nfunction PostContentModal({\n isOpen,\n title,\n submitLabel,\n content,\n imageUrl,\n isSubmitting,\n onContentChange,\n onClose,\n onSubmit,\n onUploadImage,\n onImageUploaded,\n maxLength,\n richText = false,\n showGradeQuestion = false,\n countsForGrade,\n onCountsForGradeChange,\n}: PostContentModalProps) {\n const handleImageSelect = useCallback(\n async (file: File) => {\n if (!onUploadImage) return;\n try {\n const url = await onUploadImage(file);\n onImageUploaded(url);\n } catch {\n // silently fail — consuming app should handle upload errors\n }\n },\n [onUploadImage, onImageUploaded]\n );\n\n // Determine if content is effectively empty or exceeds the limit\n const plainTextContent = richText ? stripHtml(content) : content;\n const isContentEmpty = !plainTextContent.trim();\n const exceedsMaxLength = plainTextContent.trim().length > maxLength;\n\n return (\n <Modal\n isOpen={isOpen}\n onClose={onClose}\n title={title}\n size=\"md\"\n footer={\n <div className=\"flex justify-end gap-2\">\n <Button\n variant=\"outline\"\n action=\"secondary\"\n size=\"medium\"\n onClick={onClose}\n >\n Cancelar\n </Button>\n <Button\n variant=\"solid\"\n action=\"primary\"\n size=\"medium\"\n disabled={isContentEmpty || exceedsMaxLength || isSubmitting}\n onClick={onSubmit}\n >\n {submitLabel}\n </Button>\n </div>\n }\n >\n <div className=\"flex flex-col gap-3\">\n {richText ? (\n // key forces remount (and thus reset) whenever the modal opens/closes\n <RichEditor\n key={String(isOpen)}\n content={content}\n onChange={(data) => onContentChange(data.html)}\n placeholder=\"Escreva o conteúdo do seu post aqui.\"\n />\n ) : (\n <TextArea\n placeholder=\"Escreva o conteúdo do seu post aqui.\"\n value={content}\n onChange={(e) => onContentChange(e.target.value)}\n autoResize\n minHeight={120}\n maxLength={maxLength}\n />\n )}\n\n <ImageUpload\n variant=\"compact\"\n onFileSelect={handleImageSelect}\n onRemoveFile={() => onImageUploaded('')}\n buttonIcon={<ImageIcon size={16} />}\n />\n {imageUrl && (\n <img\n src={imageUrl}\n alt=\"Preview da imagem\"\n className=\"max-h-40 rounded-lg object-contain\"\n />\n )}\n\n {showGradeQuestion && (\n <div className=\"flex flex-col gap-2\">\n <Text size=\"sm\" weight=\"medium\">\n Este fórum vale nota no boletim?\n </Text>\n <div className=\"flex items-center gap-4\">\n <Radio\n name=\"countsForGrade\"\n value=\"yes\"\n label=\"Sim\"\n size=\"small\"\n checked={countsForGrade === true}\n onChange={() => onCountsForGradeChange?.(true)}\n />\n <Radio\n name=\"countsForGrade\"\n value=\"no\"\n label=\"Não\"\n size=\"small\"\n checked={countsForGrade === false}\n onChange={() => onCountsForGradeChange?.(false)}\n />\n </div>\n </div>\n )}\n </div>\n </Modal>\n );\n}\n\n// ---------------------------------------------------------------------------\n// Skeleton\n// ---------------------------------------------------------------------------\n\nfunction TopicListSkeleton() {\n return (\n <SkeletonList items={3} showAvatar showTitle showDescription lines={2} />\n );\n}\n\nfunction TopicDetailSkeleton() {\n return (\n <SkeletonCard\n showAvatar\n showTitle\n showDescription\n showActions={false}\n lines={2}\n />\n );\n}\n\n// ---------------------------------------------------------------------------\n// Main component\n// ---------------------------------------------------------------------------\n\nexport type ForumProps = {\n /** API client with forum CRUD methods */\n readonly apiClient: ForumApiClient;\n /** Current user's userInstitutionId — used to identify own posts */\n readonly currentUserId: string;\n /** User role — enables professor-specific features (e.g. grade question on create) */\n readonly userRole?: PROFILE_ROLES.STUDENT | PROFILE_ROLES.TEACHER;\n /** Forum page title */\n readonly title?: string;\n /** Forum page subtitle */\n readonly subtitle?: string;\n /** Callback to upload an image file and return its hosted URL */\n readonly onUploadImage?: (file: File) => Promise<string>;\n /**\n * Callback to evaluate a reply (teacher only).\n * If provided, an \"Avaliar\" button appears on each reply when the topic counts for grade.\n */\n readonly onEvaluateReply?: (replyId: string, grade: number) => Promise<void>;\n /** Additional CSS classes */\n readonly className?: string;\n};\n\nexport function Forum({\n apiClient,\n currentUserId,\n userRole = PROFILE_ROLES.STUDENT,\n title = 'Fórum',\n subtitle = 'Espaço para troca de conhecimento',\n onUploadImage,\n onEvaluateReply,\n className,\n}: ForumProps) {\n const isTeacher = userRole === PROFILE_ROLES.TEACHER;\n const [view, setView] = useState<'list' | 'detail'>('list');\n const [topics, setTopics] = useState<ForumTopic[]>([]);\n const [isLoadingTopics, setIsLoadingTopics] = useState(false);\n const [selectedTopic, setSelectedTopic] = useState<ForumTopic | null>(null);\n const [replies, setReplies] = useState<ForumReply[]>([]);\n const [isLoadingTopic, setIsLoadingTopic] = useState(false);\n const [isSubmitting, setIsSubmitting] = useState(false);\n\n // Create post modal state\n const [isCreateModalOpen, setIsCreateModalOpen] = useState(false);\n const [createContent, setCreateContent] = useState('');\n const [createImageUrl, setCreateImageUrl] = useState<string | undefined>();\n const [createCountsForGrade, setCreateCountsForGrade] = useState<\n boolean | undefined\n >();\n\n // Reply modal state\n const [isReplyModalOpen, setIsReplyModalOpen] = useState(false);\n const [replyContent, setReplyContent] = useState('');\n const [replyImageUrl, setReplyImageUrl] = useState<string | undefined>();\n\n // Edit topic modal state\n const [editingTopic, setEditingTopic] = useState<ForumTopic | null>(null);\n const [editTopicContent, setEditTopicContent] = useState('');\n const [editTopicImageUrl, setEditTopicImageUrl] = useState<\n string | undefined\n >();\n\n // Edit reply modal state\n const [editingReply, setEditingReply] = useState<ForumReply | null>(null);\n const [editReplyContent, setEditReplyContent] = useState('');\n const [editReplyImageUrl, setEditReplyImageUrl] = useState<\n string | undefined\n >();\n\n // Evaluate reply modal state (teacher only)\n const [evaluatingReplyId, setEvaluatingReplyId] = useState<string | null>(\n null\n );\n const [gradeValue, setGradeValue] = useState<number | null>(null);\n const [isEditingGrade, setIsEditingGrade] = useState(false);\n\n // Delete confirmation state\n const [pendingDelete, setPendingDelete] = useState<\n { type: 'topic'; id: string } | { type: 'reply'; id: string } | null\n >(null);\n\n const fetchTopics = useCallback(async () => {\n setIsLoadingTopics(true);\n try {\n const response = await apiClient.getTopics({ limit: 50 });\n setTopics(response.topics);\n } finally {\n setIsLoadingTopics(false);\n }\n }, [apiClient]);\n\n const fetchTopic = useCallback(\n async (topicId: string) => {\n setIsLoadingTopic(true);\n try {\n const response = await apiClient.getTopic(topicId);\n setSelectedTopic(response.topic);\n setReplies(response.replies);\n } finally {\n setIsLoadingTopic(false);\n }\n },\n [apiClient]\n );\n\n useEffect(() => {\n fetchTopics();\n }, [fetchTopics]);\n\n const handleTopicClick = useCallback(\n (topic: ForumTopic) => {\n setSelectedTopic(topic);\n setView('detail');\n fetchTopic(topic.id);\n },\n [fetchTopic]\n );\n\n const handleBack = useCallback(() => {\n setView('list');\n setSelectedTopic(null);\n setReplies([]);\n fetchTopics();\n }, [fetchTopics]);\n\n // Create topic\n const handleCloseCreateModal = useCallback(() => {\n setIsCreateModalOpen(false);\n setCreateContent('');\n setCreateImageUrl(undefined);\n setCreateCountsForGrade(undefined);\n }, []);\n\n const handleCreateTopic = useCallback(async () => {\n if (!createContent.trim()) return;\n setIsSubmitting(true);\n try {\n await apiClient.createTopic({\n content: createContent.trim(),\n ...(createImageUrl && { imageUrl: createImageUrl }),\n ...(isTeacher &&\n createCountsForGrade !== undefined && {\n countsForGrade: createCountsForGrade,\n }),\n });\n handleCloseCreateModal();\n await fetchTopics();\n } finally {\n setIsSubmitting(false);\n }\n }, [\n apiClient,\n createContent,\n createImageUrl,\n createCountsForGrade,\n isTeacher,\n handleCloseCreateModal,\n fetchTopics,\n ]);\n\n // Edit topic\n const handleOpenEditTopic = useCallback((topic: ForumTopic) => {\n setEditingTopic(topic);\n setEditTopicContent(topic.content);\n setEditTopicImageUrl(topic.imageUrl ?? undefined);\n }, []);\n\n const handleCloseEditTopic = useCallback(() => {\n setEditingTopic(null);\n setEditTopicContent('');\n setEditTopicImageUrl(undefined);\n }, []);\n\n const handleEditTopic = useCallback(async () => {\n if (!editingTopic || !editTopicContent.trim()) return;\n setIsSubmitting(true);\n try {\n await apiClient.updateTopic(editingTopic.id, {\n content: editTopicContent.trim(),\n imageUrl: editTopicImageUrl ?? null,\n });\n handleCloseEditTopic();\n if (view === 'list') {\n await fetchTopics();\n } else {\n await fetchTopic(editingTopic.id);\n }\n } finally {\n setIsSubmitting(false);\n }\n }, [\n apiClient,\n editingTopic,\n editTopicContent,\n editTopicImageUrl,\n handleCloseEditTopic,\n view,\n fetchTopics,\n fetchTopic,\n ]);\n\n const handleDeleteTopic = useCallback(\n async (topicId: string) => {\n setIsSubmitting(true);\n try {\n await apiClient.deleteTopic(topicId);\n if (view === 'detail') {\n handleBack();\n } else {\n await fetchTopics();\n }\n } finally {\n setIsSubmitting(false);\n setPendingDelete(null);\n }\n },\n [apiClient, view, handleBack, fetchTopics]\n );\n\n // Reply\n const handleCloseReplyModal = useCallback(() => {\n setIsReplyModalOpen(false);\n setReplyContent('');\n setReplyImageUrl(undefined);\n }, []);\n\n const handleCreateReply = useCallback(async () => {\n if (!selectedTopic || !replyContent.trim()) return;\n setIsSubmitting(true);\n try {\n await apiClient.createReply(selectedTopic.id, {\n content: replyContent.trim(),\n ...(replyImageUrl && { imageUrl: replyImageUrl }),\n });\n handleCloseReplyModal();\n await fetchTopic(selectedTopic.id);\n } finally {\n setIsSubmitting(false);\n }\n }, [\n apiClient,\n selectedTopic,\n replyContent,\n replyImageUrl,\n handleCloseReplyModal,\n fetchTopic,\n ]);\n\n // Edit reply\n const handleOpenEditReply = useCallback((reply: ForumReply) => {\n setEditingReply(reply);\n setEditReplyContent(reply.content);\n setEditReplyImageUrl(reply.imageUrl ?? undefined);\n }, []);\n\n const handleCloseEditReply = useCallback(() => {\n setEditingReply(null);\n setEditReplyContent('');\n setEditReplyImageUrl(undefined);\n }, []);\n\n const handleEditReply = useCallback(async () => {\n if (!editingReply || !editReplyContent.trim() || !selectedTopic) return;\n setIsSubmitting(true);\n try {\n await apiClient.updateReply(editingReply.id, {\n content: editReplyContent.trim(),\n imageUrl: editReplyImageUrl ?? null,\n });\n handleCloseEditReply();\n await fetchTopic(selectedTopic.id);\n } finally {\n setIsSubmitting(false);\n }\n }, [\n apiClient,\n editingReply,\n editReplyContent,\n editReplyImageUrl,\n handleCloseEditReply,\n selectedTopic,\n fetchTopic,\n ]);\n\n const handleDeleteReply = useCallback(\n async (replyId: string) => {\n if (!selectedTopic) return;\n setIsSubmitting(true);\n try {\n await apiClient.deleteReply(replyId);\n await fetchTopic(selectedTopic.id);\n } finally {\n setIsSubmitting(false);\n setPendingDelete(null);\n }\n },\n [apiClient, selectedTopic, fetchTopic]\n );\n\n const handleCloseEvaluateModal = useCallback(() => {\n setEvaluatingReplyId(null);\n setGradeValue(null);\n setIsEditingGrade(false);\n }, []);\n\n const handleOpenEvaluate = useCallback((reply: ForumReply) => {\n setEvaluatingReplyId(reply.id);\n setGradeValue(null);\n setIsEditingGrade(false);\n }, []);\n\n const handleOpenEditGrade = useCallback((reply: ForumReply) => {\n setEvaluatingReplyId(reply.id);\n setGradeValue(reply.grade ?? null);\n setIsEditingGrade(true);\n }, []);\n\n const isValidGrade =\n gradeValue !== null &&\n Number.isInteger(gradeValue) &&\n gradeValue >= 0 &&\n gradeValue <= 10;\n\n const handleSubmitEvaluation = useCallback(async () => {\n if (!evaluatingReplyId || !isValidGrade || !onEvaluateReply) return;\n setIsSubmitting(true);\n try {\n await onEvaluateReply(evaluatingReplyId, gradeValue);\n // Optimistically update grade in local state\n setReplies((prev) =>\n prev.map((r) =>\n r.id === evaluatingReplyId ? { ...r, grade: gradeValue } : r\n )\n );\n handleCloseEvaluateModal();\n } finally {\n setIsSubmitting(false);\n }\n }, [\n evaluatingReplyId,\n gradeValue,\n isValidGrade,\n onEvaluateReply,\n handleCloseEvaluateModal,\n ]);\n\n const handleConfirmDelete = useCallback(async () => {\n if (!pendingDelete) return;\n if (pendingDelete.type === 'topic') {\n await handleDeleteTopic(pendingDelete.id);\n } else {\n await handleDeleteReply(pendingDelete.id);\n }\n }, [pendingDelete, handleDeleteTopic, handleDeleteReply]);\n\n /* ---- Extracted render blocks (avoids nested ternaries — SonarQube) ---- */\n\n const topicListContent = (() => {\n if (isLoadingTopics) return <TopicListSkeleton />;\n\n if (topics.length === 0) {\n return (\n <div className=\"flex items-center justify-center py-16 border border-border-200 rounded-lg\">\n <Text size=\"md\" color=\"text-text-500\">\n Nenhum tópico ainda. Seja o primeiro a criar um post!\n </Text>\n </div>\n );\n }\n\n return (\n <div className=\"flex flex-col gap-4\">\n {topics.map((topic) => (\n <div\n key={topic.id}\n className=\"flex items-start bg-white border border-border-100 rounded-lg pt-4 pr-2 pb-4 pl-2 shadow-sm\"\n >\n {/* Clickable content area */}\n <button\n type=\"button\"\n className=\"flex flex-col gap-3 flex-1 min-w-0 text-left hover:opacity-90 transition-opacity\"\n onClick={() => handleTopicClick(topic)}\n >\n <AuthorMeta\n authorName={topic.authorName}\n authorPhoto={topic.authorPhoto}\n createdAt={topic.createdAt}\n />\n <Text size=\"sm\" className=\"leading-normal line-clamp-3 ml-10\">\n {stripHtml(topic.content)}\n </Text>\n <div className=\"flex items-center gap-1 text-text-600 ml-10\">\n <ChatCircleTextIcon size={16} />\n <Text size=\"xs\">{formatReplyCount(topic.replyCount)}</Text>\n </div>\n </button>\n\n {/* Actions menu — only for own posts */}\n {topic.userInstitutionId === currentUserId && (\n <PostActionsMenu\n onEdit={() => handleOpenEditTopic(topic)}\n onDelete={() =>\n setPendingDelete({ type: 'topic', id: topic.id })\n }\n />\n )}\n </div>\n ))}\n </div>\n );\n })();\n\n const topicDetailContent = (() => {\n if (isLoadingTopic) return <TopicDetailSkeleton />;\n\n if (!selectedTopic) return null;\n\n return (\n <>\n {/* Original post */}\n <div className=\"flex items-start gap-2\">\n <div className=\"flex flex-col gap-3 flex-1 min-w-0\">\n <AuthorMeta\n authorName={selectedTopic.authorName}\n authorPhoto={selectedTopic.authorPhoto}\n createdAt={selectedTopic.createdAt}\n />\n <div\n className=\"text-sm text-text-950 leading-normal ml-10 prose prose-sm max-w-none\"\n dangerouslySetInnerHTML={{\n __html: sanitizeHtmlForDisplay(selectedTopic.content),\n }}\n />\n {selectedTopic.imageUrl && (\n <img\n src={selectedTopic.imageUrl}\n alt=\"Imagem do post\"\n className=\"ml-10 max-w-lg rounded-lg\"\n />\n )}\n </div>\n {selectedTopic.userInstitutionId === currentUserId && (\n <PostActionsMenu\n onEdit={() => handleOpenEditTopic(selectedTopic)}\n onDelete={() =>\n setPendingDelete({ type: 'topic', id: selectedTopic.id })\n }\n />\n )}\n </div>\n\n {/* Replies header */}\n <div className=\"flex items-center justify-between\">\n <Text size=\"sm\" color=\"text-text-600\">\n {formatReplyCount(selectedTopic.replyCount)}\n </Text>\n <Button\n variant=\"solid\"\n action=\"primary\"\n size=\"medium\"\n onClick={() => setIsReplyModalOpen(true)}\n >\n Responder\n </Button>\n </div>\n\n {/* Replies list */}\n {replies.length > 0 && (\n <div className=\"flex flex-col divide-y divide-border-200\">\n {replies.map((reply) => (\n <div key={reply.id} className=\"flex items-start gap-2 py-4\">\n <div className=\"flex flex-col gap-3 flex-1 min-w-0\">\n <AuthorMeta\n authorName={reply.authorName}\n authorPhoto={reply.authorPhoto}\n createdAt={reply.createdAt}\n />\n <div\n className=\"text-sm text-text-950 leading-normal ml-10 prose prose-sm max-w-none\"\n dangerouslySetInnerHTML={{\n __html: sanitizeHtmlForDisplay(reply.content),\n }}\n />\n {reply.imageUrl && (\n <img\n src={reply.imageUrl}\n alt=\"Imagem da resposta\"\n className=\"ml-10 max-w-lg rounded-lg\"\n />\n )}\n {isTeacher &&\n selectedTopic?.countsForGrade &&\n onEvaluateReply &&\n (reply.grade == null ? (\n /* Not yet graded — show Avaliar button */\n <button\n type=\"button\"\n className=\"flex items-center gap-1.5 text-text-600 text-sm hover:text-text-950 transition-colors w-fit ml-10\"\n onClick={() => handleOpenEvaluate(reply)}\n >\n <CheckIcon size={16} />\n Avaliar\n </button>\n ) : (\n /* Grade already set — teacher sees it with edit pencil */\n <div className=\"flex items-center gap-1.5 ml-10\">\n <Text size=\"sm\" color=\"text-text-600\">\n Nota {reply.grade}\n </Text>\n <button\n type=\"button\"\n className=\"text-text-600 hover:text-text-950 transition-colors\"\n onClick={() => handleOpenEditGrade(reply)}\n >\n <PencilSimpleIcon size={14} />\n </button>\n </div>\n ))}\n {!isTeacher &&\n reply.grade != null &&\n reply.userInstitutionId === currentUserId &&\n selectedTopic?.countsForGrade && (\n <Text size=\"sm\" color=\"text-text-600\" className=\"ml-10\">\n Nota {reply.grade}\n </Text>\n )}\n </div>\n {reply.userInstitutionId === currentUserId && (\n <PostActionsMenu\n onEdit={() => handleOpenEditReply(reply)}\n onDelete={() =>\n setPendingDelete({ type: 'reply', id: reply.id })\n }\n />\n )}\n </div>\n ))}\n </div>\n )}\n </>\n );\n })();\n\n return (\n <div className={cn('flex flex-col gap-4', className)}>\n {view === 'list' ? (\n <>\n {/* Header */}\n <div className=\"flex items-center justify-between\">\n <Text as=\"h1\" size=\"2xl\" weight=\"bold\" color=\"text-text-950\">\n {title}\n </Text>\n <Button\n variant=\"solid\"\n action=\"primary\"\n size=\"medium\"\n onClick={() => setIsCreateModalOpen(true)}\n >\n Criar post\n </Button>\n </div>\n\n {/* Subtitle + topic list */}\n <div className=\"flex flex-col gap-3\">\n <Text as=\"h2\" size=\"lg\" weight=\"semibold\" color=\"text-text-950\">\n {subtitle}\n </Text>\n\n {topicListContent}\n </div>\n </>\n ) : (\n /* Detail view */\n <>\n <button\n type=\"button\"\n className=\"flex items-center gap-2 hover:opacity-80 transition-opacity w-fit\"\n onClick={handleBack}\n >\n <CaretLeftIcon size={24} className=\"text-text-950\" />\n <Text size=\"2xl\" weight=\"bold\">\n Voltar\n </Text>\n </button>\n\n {topicDetailContent}\n </>\n )}\n\n {/* Create post modal */}\n <PostContentModal\n isOpen={isCreateModalOpen}\n title=\"Criar post\"\n submitLabel=\"Criar post\"\n content={createContent}\n imageUrl={createImageUrl}\n isSubmitting={isSubmitting}\n onContentChange={setCreateContent}\n onClose={handleCloseCreateModal}\n onSubmit={handleCreateTopic}\n onUploadImage={onUploadImage}\n onImageUploaded={setCreateImageUrl}\n maxLength={10000}\n richText\n showGradeQuestion={isTeacher}\n countsForGrade={createCountsForGrade}\n onCountsForGradeChange={setCreateCountsForGrade}\n />\n\n {/* Reply modal */}\n <PostContentModal\n isOpen={isReplyModalOpen}\n title=\"Responder\"\n submitLabel=\"Responder\"\n content={replyContent}\n imageUrl={replyImageUrl}\n isSubmitting={isSubmitting}\n onContentChange={setReplyContent}\n onClose={handleCloseReplyModal}\n onSubmit={handleCreateReply}\n onUploadImage={onUploadImage}\n onImageUploaded={setReplyImageUrl}\n maxLength={5000}\n richText\n />\n\n {/* Edit topic modal */}\n <PostContentModal\n isOpen={!!editingTopic}\n title=\"Editar\"\n submitLabel=\"Salvar\"\n content={editTopicContent}\n imageUrl={editTopicImageUrl}\n isSubmitting={isSubmitting}\n onContentChange={setEditTopicContent}\n onClose={handleCloseEditTopic}\n onSubmit={handleEditTopic}\n onUploadImage={onUploadImage}\n onImageUploaded={setEditTopicImageUrl}\n maxLength={10000}\n richText\n />\n\n {/* Edit reply modal */}\n <PostContentModal\n isOpen={!!editingReply}\n title=\"Editar\"\n submitLabel=\"Salvar\"\n content={editReplyContent}\n imageUrl={editReplyImageUrl}\n isSubmitting={isSubmitting}\n onContentChange={setEditReplyContent}\n onClose={handleCloseEditReply}\n onSubmit={handleEditReply}\n onUploadImage={onUploadImage}\n onImageUploaded={setEditReplyImageUrl}\n maxLength={5000}\n richText\n />\n\n {/* Evaluate / edit grade modal (teacher only) */}\n <Modal\n isOpen={!!evaluatingReplyId}\n onClose={handleCloseEvaluateModal}\n title={\n isEditingGrade\n ? 'Edite a nota da atividade'\n : 'Defina a nota da atividade'\n }\n size=\"md\"\n footer={\n <div className=\"flex justify-end gap-2\">\n <Button\n variant=\"outline\"\n action=\"secondary\"\n size=\"medium\"\n onClick={handleCloseEvaluateModal}\n >\n Cancelar\n </Button>\n <Button\n variant=\"solid\"\n action=\"primary\"\n size=\"medium\"\n disabled={!isValidGrade || isSubmitting}\n onClick={handleSubmitEvaluation}\n >\n {isEditingGrade ? 'Salvar' : 'Avaliar'}\n </Button>\n </div>\n }\n >\n <Input\n type=\"number\"\n min={0}\n max={10}\n step={1}\n placeholder=\"Nota (0-10)\"\n value={gradeValue ?? ''}\n onChange={(e) => {\n const val = e.target.value;\n const parsed = Number(val);\n setGradeValue(val === '' || Number.isNaN(parsed) ? null : parsed);\n }}\n />\n </Modal>\n\n {/* Delete confirmation modal */}\n <Modal\n isOpen={!!pendingDelete}\n onClose={() => setPendingDelete(null)}\n title=\"Deseja deletar post?\"\n size=\"sm\"\n footer={\n <div className=\"flex justify-end gap-2\">\n <Button\n variant=\"outline\"\n action=\"secondary\"\n size=\"medium\"\n onClick={() => setPendingDelete(null)}\n >\n Cancelar\n </Button>\n <Button\n variant=\"solid\"\n action=\"negative\"\n size=\"medium\"\n disabled={isSubmitting}\n onClick={handleConfirmDelete}\n >\n Deletar\n </Button>\n </div>\n }\n >\n <Text size=\"md\" color=\"text-text-600\">\n Essa ação é permanente e não poderá ser desfeita.\n </Text>\n </Modal>\n </div>\n );\n}\n\nexport default Forum;\n"]}
|
|
1
|
+
{"version":3,"sources":["/home/runner/work/analytica-frontend-lib/analytica-frontend-lib/dist/chunk-DH6WK44A.js","../src/components/Forum/Forum.tsx"],"names":[],"mappings":"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","file":"/home/runner/work/analytica-frontend-lib/analytica-frontend-lib/dist/chunk-DH6WK44A.js","sourcesContent":[null,"import { useState, useEffect, useCallback } from 'react';\nimport {\n CaretLeftIcon,\n ChatCircleTextIcon,\n CheckIcon,\n DotsThreeVerticalIcon,\n ImageIcon,\n PencilSimpleIcon,\n TrashIcon,\n} from '@phosphor-icons/react';\nimport Text from '../Text/Text';\nimport Button from '../Button/Button';\nimport Input from '../Input/Input';\nimport TextArea from '../TextArea/TextArea';\nimport Modal from '../Modal/Modal';\nimport Radio from '../Radio/Radio';\nimport DropdownMenu, {\n DropdownMenuTrigger,\n DropdownMenuContent,\n DropdownMenuItem,\n} from '../DropdownMenu/DropdownMenu';\nimport { RichEditor } from '../RichEditor/RichEditor';\nimport ImageUpload from '../ImageUpload/ImageUpload';\nimport { SkeletonList, SkeletonCard } from '../Skeleton/Skeleton';\nimport { stripHtml, sanitizeHtmlForDisplay } from '../HtmlMathRenderer';\nimport { cn } from '../../utils/utils';\nimport type { ForumApiClient, ForumTopic, ForumReply } from '../../types/forum';\nimport { PROFILE_ROLES } from '../../types/chat';\n\n// ---------------------------------------------------------------------------\n// Helpers\n// ---------------------------------------------------------------------------\n\nconst MONTHS_PT = [\n 'Janeiro',\n 'Fevereiro',\n 'Março',\n 'Abril',\n 'Maio',\n 'Junho',\n 'Julho',\n 'Agosto',\n 'Setembro',\n 'Outubro',\n 'Novembro',\n 'Dezembro',\n];\n\nfunction formatForumDate(isoDate: string): string {\n const d = new Date(isoDate);\n const day = d.getDate();\n const month = MONTHS_PT[d.getMonth()];\n const year = d.getFullYear();\n const hours = d.getHours();\n const minutes = d.getMinutes().toString().padStart(2, '0');\n return `${day} ${month} ${year} • ${hours}:${minutes}`;\n}\n\nfunction getInitials(name: string): string {\n return name\n .split(' ')\n .slice(0, 2)\n .map((word) => word[0] ?? '')\n .join('')\n .toUpperCase();\n}\n\nfunction formatReplyCount(count: number): string {\n if (count === 1) return '1 resposta';\n return `${count} respostas`;\n}\n\n// ---------------------------------------------------------------------------\n// Sub-components\n// ---------------------------------------------------------------------------\n\ninterface AuthorAvatarProps {\n readonly name: string;\n readonly photoUrl: string | null;\n}\n\nfunction AuthorAvatar({ name, photoUrl }: AuthorAvatarProps) {\n if (photoUrl) {\n return (\n <img\n src={photoUrl}\n alt={name}\n className=\"w-8 h-8 rounded-full object-cover shrink-0\"\n />\n );\n }\n return (\n <div className=\"w-8 h-8 rounded-full bg-primary-600 flex items-center justify-center shrink-0\">\n <Text size=\"xs\" weight=\"semibold\" color=\"text-white\">\n {getInitials(name)}\n </Text>\n </div>\n );\n}\n\ninterface AuthorMetaProps {\n readonly authorName: string;\n readonly authorPhoto: string | null;\n readonly createdAt: string;\n}\n\nfunction AuthorMeta({ authorName, authorPhoto, createdAt }: AuthorMetaProps) {\n return (\n <div className=\"flex items-center gap-2\">\n <AuthorAvatar name={authorName} photoUrl={authorPhoto} />\n <div className=\"flex items-center gap-1\">\n <Text size=\"xs\" weight=\"semibold\" color=\"text-primary-700\">\n Postado por {authorName}\n </Text>\n <Text size=\"xs\" color=\"text-text-600\">\n • {formatForumDate(createdAt)}\n </Text>\n </div>\n </div>\n );\n}\n\ninterface PostActionsMenuProps {\n readonly onEdit: () => void;\n readonly onDelete: () => void;\n}\n\nfunction PostActionsMenu({ onEdit, onDelete }: PostActionsMenuProps) {\n return (\n <DropdownMenu>\n <DropdownMenuTrigger\n aria-label=\"Abrir ações do post\"\n className=\"p-1 rounded hover:bg-background-100 transition-colors shrink-0\"\n >\n <DotsThreeVerticalIcon size={20} className=\"text-text-600\" />\n </DropdownMenuTrigger>\n <DropdownMenuContent align=\"end\">\n <DropdownMenuItem\n iconLeft={<PencilSimpleIcon size={16} />}\n onClick={onEdit}\n >\n Editar\n </DropdownMenuItem>\n <DropdownMenuItem iconLeft={<TrashIcon size={16} />} onClick={onDelete}>\n Deletar\n </DropdownMenuItem>\n </DropdownMenuContent>\n </DropdownMenu>\n );\n}\n\ninterface PostContentModalProps {\n readonly isOpen: boolean;\n readonly title: string;\n readonly submitLabel: string;\n readonly content: string;\n readonly imageUrl: string | undefined;\n readonly isSubmitting: boolean;\n readonly onContentChange: (value: string) => void;\n readonly onClose: () => void;\n readonly onSubmit: () => void;\n readonly onUploadImage?: (file: File) => Promise<string>;\n readonly onImageUploaded: (url: string) => void;\n readonly maxLength: number;\n /** Use RichEditor with toolbar instead of plain TextArea */\n readonly richText?: boolean;\n /** Show \"Este fórum vale nota no boletim?\" question (professor only) */\n readonly showGradeQuestion?: boolean;\n readonly countsForGrade?: boolean;\n readonly onCountsForGradeChange?: (value: boolean) => void;\n}\n\nfunction PostContentModal({\n isOpen,\n title,\n submitLabel,\n content,\n imageUrl,\n isSubmitting,\n onContentChange,\n onClose,\n onSubmit,\n onUploadImage,\n onImageUploaded,\n maxLength,\n richText = false,\n showGradeQuestion = false,\n countsForGrade,\n onCountsForGradeChange,\n}: PostContentModalProps) {\n const handleImageSelect = useCallback(\n async (file: File) => {\n if (!onUploadImage) return;\n try {\n const url = await onUploadImage(file);\n onImageUploaded(url);\n } catch {\n // silently fail — consuming app should handle upload errors\n }\n },\n [onUploadImage, onImageUploaded]\n );\n\n // Determine if content is effectively empty or exceeds the limit\n const plainTextContent = richText ? stripHtml(content) : content;\n const isContentEmpty = !plainTextContent.trim();\n const exceedsMaxLength = plainTextContent.trim().length > maxLength;\n\n return (\n <Modal\n isOpen={isOpen}\n onClose={onClose}\n title={title}\n size=\"md\"\n footer={\n <div className=\"flex justify-end gap-2\">\n <Button\n variant=\"outline\"\n action=\"secondary\"\n size=\"medium\"\n onClick={onClose}\n >\n Cancelar\n </Button>\n <Button\n variant=\"solid\"\n action=\"primary\"\n size=\"medium\"\n disabled={isContentEmpty || exceedsMaxLength || isSubmitting}\n onClick={onSubmit}\n >\n {submitLabel}\n </Button>\n </div>\n }\n >\n <div className=\"flex flex-col gap-3\">\n {richText ? (\n // key forces remount (and thus reset) whenever the modal opens/closes\n <RichEditor\n key={String(isOpen)}\n content={content}\n onChange={(data) => onContentChange(data.html)}\n placeholder=\"Escreva o conteúdo do seu post aqui.\"\n />\n ) : (\n <TextArea\n placeholder=\"Escreva o conteúdo do seu post aqui.\"\n value={content}\n onChange={(e) => onContentChange(e.target.value)}\n autoResize\n minHeight={120}\n maxLength={maxLength}\n />\n )}\n\n <ImageUpload\n variant=\"compact\"\n onFileSelect={handleImageSelect}\n onRemoveFile={() => onImageUploaded('')}\n buttonIcon={<ImageIcon size={16} />}\n />\n {imageUrl && (\n <img\n src={imageUrl}\n alt=\"Preview da imagem\"\n className=\"max-h-40 rounded-lg object-contain\"\n />\n )}\n\n {showGradeQuestion && (\n <div className=\"flex flex-col gap-2\">\n <Text size=\"sm\" weight=\"medium\">\n Este fórum vale nota no boletim?\n </Text>\n <div className=\"flex items-center gap-4\">\n <Radio\n name=\"countsForGrade\"\n value=\"yes\"\n label=\"Sim\"\n size=\"small\"\n checked={countsForGrade === true}\n onChange={() => onCountsForGradeChange?.(true)}\n />\n <Radio\n name=\"countsForGrade\"\n value=\"no\"\n label=\"Não\"\n size=\"small\"\n checked={countsForGrade === false}\n onChange={() => onCountsForGradeChange?.(false)}\n />\n </div>\n </div>\n )}\n </div>\n </Modal>\n );\n}\n\n// ---------------------------------------------------------------------------\n// Skeleton\n// ---------------------------------------------------------------------------\n\nfunction TopicListSkeleton() {\n return (\n <SkeletonList items={3} showAvatar showTitle showDescription lines={2} />\n );\n}\n\nfunction TopicDetailSkeleton() {\n return (\n <SkeletonCard\n showAvatar\n showTitle\n showDescription\n showActions={false}\n lines={2}\n />\n );\n}\n\n// ---------------------------------------------------------------------------\n// Main component\n// ---------------------------------------------------------------------------\n\nexport type ForumProps = {\n /** API client with forum CRUD methods */\n readonly apiClient: ForumApiClient;\n /** Current user's userInstitutionId — used to identify own posts */\n readonly currentUserId: string;\n /** User role — enables professor-specific features (e.g. grade question on create) */\n readonly userRole?: PROFILE_ROLES.STUDENT | PROFILE_ROLES.TEACHER;\n /** Forum page title */\n readonly title?: string;\n /** Forum page subtitle */\n readonly subtitle?: string;\n /** Callback to upload an image file and return its hosted URL */\n readonly onUploadImage?: (file: File) => Promise<string>;\n /**\n * Callback to evaluate a reply (teacher only).\n * If provided, an \"Avaliar\" button appears on each reply when the topic counts for grade.\n */\n readonly onEvaluateReply?: (replyId: string, grade: number) => Promise<void>;\n /** Additional CSS classes */\n readonly className?: string;\n};\n\nexport function Forum({\n apiClient,\n currentUserId,\n userRole = PROFILE_ROLES.STUDENT,\n title = 'Fórum',\n subtitle = 'Espaço para troca de conhecimento',\n onUploadImage,\n onEvaluateReply,\n className,\n}: ForumProps) {\n const isTeacher = userRole === PROFILE_ROLES.TEACHER;\n const [view, setView] = useState<'list' | 'detail'>('list');\n const [topics, setTopics] = useState<ForumTopic[]>([]);\n const [isLoadingTopics, setIsLoadingTopics] = useState(false);\n const [selectedTopic, setSelectedTopic] = useState<ForumTopic | null>(null);\n const [replies, setReplies] = useState<ForumReply[]>([]);\n const [isLoadingTopic, setIsLoadingTopic] = useState(false);\n const [isSubmitting, setIsSubmitting] = useState(false);\n\n // Create post modal state\n const [isCreateModalOpen, setIsCreateModalOpen] = useState(false);\n const [createContent, setCreateContent] = useState('');\n const [createImageUrl, setCreateImageUrl] = useState<string | undefined>();\n const [createCountsForGrade, setCreateCountsForGrade] = useState<\n boolean | undefined\n >();\n\n // Reply modal state\n const [isReplyModalOpen, setIsReplyModalOpen] = useState(false);\n const [replyContent, setReplyContent] = useState('');\n const [replyImageUrl, setReplyImageUrl] = useState<string | undefined>();\n\n // Edit topic modal state\n const [editingTopic, setEditingTopic] = useState<ForumTopic | null>(null);\n const [editTopicContent, setEditTopicContent] = useState('');\n const [editTopicImageUrl, setEditTopicImageUrl] = useState<\n string | undefined\n >();\n\n // Edit reply modal state\n const [editingReply, setEditingReply] = useState<ForumReply | null>(null);\n const [editReplyContent, setEditReplyContent] = useState('');\n const [editReplyImageUrl, setEditReplyImageUrl] = useState<\n string | undefined\n >();\n\n // Evaluate reply modal state (teacher only)\n const [evaluatingReplyId, setEvaluatingReplyId] = useState<string | null>(\n null\n );\n const [gradeValue, setGradeValue] = useState<number | null>(null);\n const [isEditingGrade, setIsEditingGrade] = useState(false);\n\n // Delete confirmation state\n const [pendingDelete, setPendingDelete] = useState<\n { type: 'topic'; id: string } | { type: 'reply'; id: string } | null\n >(null);\n\n const fetchTopics = useCallback(async () => {\n setIsLoadingTopics(true);\n try {\n const response = await apiClient.getTopics({ limit: 50 });\n setTopics(response.topics);\n } finally {\n setIsLoadingTopics(false);\n }\n }, [apiClient]);\n\n const fetchTopic = useCallback(\n async (topicId: string) => {\n setIsLoadingTopic(true);\n try {\n const response = await apiClient.getTopic(topicId);\n setSelectedTopic(response.topic);\n setReplies(response.replies);\n } finally {\n setIsLoadingTopic(false);\n }\n },\n [apiClient]\n );\n\n useEffect(() => {\n fetchTopics();\n }, [fetchTopics]);\n\n const handleTopicClick = useCallback(\n (topic: ForumTopic) => {\n setSelectedTopic(topic);\n setView('detail');\n fetchTopic(topic.id);\n },\n [fetchTopic]\n );\n\n const handleBack = useCallback(() => {\n setView('list');\n setSelectedTopic(null);\n setReplies([]);\n fetchTopics();\n }, [fetchTopics]);\n\n // Create topic\n const handleCloseCreateModal = useCallback(() => {\n setIsCreateModalOpen(false);\n setCreateContent('');\n setCreateImageUrl(undefined);\n setCreateCountsForGrade(undefined);\n }, []);\n\n const handleCreateTopic = useCallback(async () => {\n if (!createContent.trim()) return;\n setIsSubmitting(true);\n try {\n await apiClient.createTopic({\n content: createContent.trim(),\n ...(createImageUrl && { imageUrl: createImageUrl }),\n ...(isTeacher &&\n createCountsForGrade !== undefined && {\n countsForGrade: createCountsForGrade,\n }),\n });\n handleCloseCreateModal();\n await fetchTopics();\n } finally {\n setIsSubmitting(false);\n }\n }, [\n apiClient,\n createContent,\n createImageUrl,\n createCountsForGrade,\n isTeacher,\n handleCloseCreateModal,\n fetchTopics,\n ]);\n\n // Edit topic\n const handleOpenEditTopic = useCallback((topic: ForumTopic) => {\n setEditingTopic(topic);\n setEditTopicContent(topic.content);\n setEditTopicImageUrl(topic.imageUrl ?? undefined);\n }, []);\n\n const handleCloseEditTopic = useCallback(() => {\n setEditingTopic(null);\n setEditTopicContent('');\n setEditTopicImageUrl(undefined);\n }, []);\n\n const handleEditTopic = useCallback(async () => {\n if (!editingTopic || !editTopicContent.trim()) return;\n setIsSubmitting(true);\n try {\n await apiClient.updateTopic(editingTopic.id, {\n content: editTopicContent.trim(),\n imageUrl: editTopicImageUrl ?? null,\n });\n handleCloseEditTopic();\n if (view === 'list') {\n await fetchTopics();\n } else {\n await fetchTopic(editingTopic.id);\n }\n } finally {\n setIsSubmitting(false);\n }\n }, [\n apiClient,\n editingTopic,\n editTopicContent,\n editTopicImageUrl,\n handleCloseEditTopic,\n view,\n fetchTopics,\n fetchTopic,\n ]);\n\n const handleDeleteTopic = useCallback(\n async (topicId: string) => {\n setIsSubmitting(true);\n try {\n await apiClient.deleteTopic(topicId);\n if (view === 'detail') {\n handleBack();\n } else {\n await fetchTopics();\n }\n } finally {\n setIsSubmitting(false);\n setPendingDelete(null);\n }\n },\n [apiClient, view, handleBack, fetchTopics]\n );\n\n // Reply\n const handleCloseReplyModal = useCallback(() => {\n setIsReplyModalOpen(false);\n setReplyContent('');\n setReplyImageUrl(undefined);\n }, []);\n\n const handleCreateReply = useCallback(async () => {\n if (!selectedTopic || !replyContent.trim()) return;\n setIsSubmitting(true);\n try {\n await apiClient.createReply(selectedTopic.id, {\n content: replyContent.trim(),\n ...(replyImageUrl && { imageUrl: replyImageUrl }),\n });\n handleCloseReplyModal();\n await fetchTopic(selectedTopic.id);\n } finally {\n setIsSubmitting(false);\n }\n }, [\n apiClient,\n selectedTopic,\n replyContent,\n replyImageUrl,\n handleCloseReplyModal,\n fetchTopic,\n ]);\n\n // Edit reply\n const handleOpenEditReply = useCallback((reply: ForumReply) => {\n setEditingReply(reply);\n setEditReplyContent(reply.content);\n setEditReplyImageUrl(reply.imageUrl ?? undefined);\n }, []);\n\n const handleCloseEditReply = useCallback(() => {\n setEditingReply(null);\n setEditReplyContent('');\n setEditReplyImageUrl(undefined);\n }, []);\n\n const handleEditReply = useCallback(async () => {\n if (!editingReply || !editReplyContent.trim() || !selectedTopic) return;\n setIsSubmitting(true);\n try {\n await apiClient.updateReply(editingReply.id, {\n content: editReplyContent.trim(),\n imageUrl: editReplyImageUrl ?? null,\n });\n handleCloseEditReply();\n await fetchTopic(selectedTopic.id);\n } finally {\n setIsSubmitting(false);\n }\n }, [\n apiClient,\n editingReply,\n editReplyContent,\n editReplyImageUrl,\n handleCloseEditReply,\n selectedTopic,\n fetchTopic,\n ]);\n\n const handleDeleteReply = useCallback(\n async (replyId: string) => {\n if (!selectedTopic) return;\n setIsSubmitting(true);\n try {\n await apiClient.deleteReply(replyId);\n await fetchTopic(selectedTopic.id);\n } finally {\n setIsSubmitting(false);\n setPendingDelete(null);\n }\n },\n [apiClient, selectedTopic, fetchTopic]\n );\n\n const handleCloseEvaluateModal = useCallback(() => {\n setEvaluatingReplyId(null);\n setGradeValue(null);\n setIsEditingGrade(false);\n }, []);\n\n const handleOpenEvaluate = useCallback((reply: ForumReply) => {\n setEvaluatingReplyId(reply.id);\n setGradeValue(null);\n setIsEditingGrade(false);\n }, []);\n\n const handleOpenEditGrade = useCallback((reply: ForumReply) => {\n setEvaluatingReplyId(reply.id);\n setGradeValue(reply.grade ?? null);\n setIsEditingGrade(true);\n }, []);\n\n const isValidGrade =\n gradeValue !== null &&\n Number.isInteger(gradeValue) &&\n gradeValue >= 0 &&\n gradeValue <= 10;\n\n const handleSubmitEvaluation = useCallback(async () => {\n if (!evaluatingReplyId || !isValidGrade || !onEvaluateReply) return;\n setIsSubmitting(true);\n try {\n await onEvaluateReply(evaluatingReplyId, gradeValue);\n // Optimistically update grade in local state\n setReplies((prev) =>\n prev.map((r) =>\n r.id === evaluatingReplyId ? { ...r, grade: gradeValue } : r\n )\n );\n handleCloseEvaluateModal();\n } finally {\n setIsSubmitting(false);\n }\n }, [\n evaluatingReplyId,\n gradeValue,\n isValidGrade,\n onEvaluateReply,\n handleCloseEvaluateModal,\n ]);\n\n const handleConfirmDelete = useCallback(async () => {\n if (!pendingDelete) return;\n if (pendingDelete.type === 'topic') {\n await handleDeleteTopic(pendingDelete.id);\n } else {\n await handleDeleteReply(pendingDelete.id);\n }\n }, [pendingDelete, handleDeleteTopic, handleDeleteReply]);\n\n /* ---- Extracted render blocks (avoids nested ternaries — SonarQube) ---- */\n\n const topicListContent = (() => {\n if (isLoadingTopics) return <TopicListSkeleton />;\n\n if (topics.length === 0) {\n return (\n <div className=\"flex items-center justify-center py-16 border border-border-200 rounded-lg\">\n <Text size=\"md\" color=\"text-text-500\">\n Nenhum tópico ainda. Seja o primeiro a criar um post!\n </Text>\n </div>\n );\n }\n\n return (\n <div className=\"flex flex-col gap-4\">\n {topics.map((topic) => (\n <div\n key={topic.id}\n className=\"flex items-start bg-white border border-border-100 rounded-lg pt-4 pr-2 pb-4 pl-2 shadow-sm\"\n >\n {/* Clickable content area */}\n <button\n type=\"button\"\n className=\"flex flex-col gap-3 flex-1 min-w-0 text-left hover:opacity-90 transition-opacity\"\n onClick={() => handleTopicClick(topic)}\n >\n <AuthorMeta\n authorName={topic.authorName}\n authorPhoto={topic.authorPhoto}\n createdAt={topic.createdAt}\n />\n <Text size=\"sm\" className=\"leading-normal line-clamp-3 ml-10\">\n {stripHtml(topic.content)}\n </Text>\n <div className=\"flex items-center gap-1 text-text-600 ml-10\">\n <ChatCircleTextIcon size={16} />\n <Text size=\"xs\">{formatReplyCount(topic.replyCount)}</Text>\n </div>\n </button>\n\n {/* Actions menu — only for own posts */}\n {topic.userInstitutionId === currentUserId && (\n <PostActionsMenu\n onEdit={() => handleOpenEditTopic(topic)}\n onDelete={() =>\n setPendingDelete({ type: 'topic', id: topic.id })\n }\n />\n )}\n </div>\n ))}\n </div>\n );\n })();\n\n const topicDetailContent = (() => {\n if (isLoadingTopic) return <TopicDetailSkeleton />;\n\n if (!selectedTopic) return null;\n\n return (\n <>\n {/* Original post */}\n <div className=\"flex items-start gap-2\">\n <div className=\"flex flex-col gap-3 flex-1 min-w-0\">\n <AuthorMeta\n authorName={selectedTopic.authorName}\n authorPhoto={selectedTopic.authorPhoto}\n createdAt={selectedTopic.createdAt}\n />\n <div\n className=\"text-sm text-text-950 leading-normal ml-10 prose prose-sm max-w-none\"\n dangerouslySetInnerHTML={{\n __html: sanitizeHtmlForDisplay(selectedTopic.content),\n }}\n />\n {selectedTopic.imageUrl && (\n <img\n src={selectedTopic.imageUrl}\n alt=\"Imagem do post\"\n className=\"ml-10 max-w-lg rounded-lg\"\n />\n )}\n </div>\n {selectedTopic.userInstitutionId === currentUserId && (\n <PostActionsMenu\n onEdit={() => handleOpenEditTopic(selectedTopic)}\n onDelete={() =>\n setPendingDelete({ type: 'topic', id: selectedTopic.id })\n }\n />\n )}\n </div>\n\n {/* Replies header */}\n <div className=\"flex items-center justify-between\">\n <Text size=\"sm\" color=\"text-text-600\">\n {formatReplyCount(selectedTopic.replyCount)}\n </Text>\n <Button\n variant=\"solid\"\n action=\"primary\"\n size=\"medium\"\n onClick={() => setIsReplyModalOpen(true)}\n >\n Responder\n </Button>\n </div>\n\n {/* Replies list */}\n {replies.length > 0 && (\n <div className=\"flex flex-col divide-y divide-border-200\">\n {replies.map((reply) => (\n <div key={reply.id} className=\"flex items-start gap-2 py-4\">\n <div className=\"flex flex-col gap-3 flex-1 min-w-0\">\n <AuthorMeta\n authorName={reply.authorName}\n authorPhoto={reply.authorPhoto}\n createdAt={reply.createdAt}\n />\n <div\n className=\"text-sm text-text-950 leading-normal ml-10 prose prose-sm max-w-none\"\n dangerouslySetInnerHTML={{\n __html: sanitizeHtmlForDisplay(reply.content),\n }}\n />\n {reply.imageUrl && (\n <img\n src={reply.imageUrl}\n alt=\"Imagem da resposta\"\n className=\"ml-10 max-w-lg rounded-lg\"\n />\n )}\n {isTeacher &&\n selectedTopic?.countsForGrade &&\n onEvaluateReply &&\n (reply.grade == null ? (\n /* Not yet graded — show Avaliar button */\n <button\n type=\"button\"\n className=\"flex items-center gap-1.5 text-text-600 text-sm hover:text-text-950 transition-colors w-fit ml-10\"\n onClick={() => handleOpenEvaluate(reply)}\n >\n <CheckIcon size={16} />\n Avaliar\n </button>\n ) : (\n /* Grade already set — teacher sees it with edit pencil */\n <div className=\"flex items-center gap-1.5 ml-10\">\n <Text size=\"sm\" color=\"text-text-600\">\n Nota {reply.grade}\n </Text>\n <button\n type=\"button\"\n className=\"text-text-600 hover:text-text-950 transition-colors\"\n onClick={() => handleOpenEditGrade(reply)}\n >\n <PencilSimpleIcon size={14} />\n </button>\n </div>\n ))}\n {!isTeacher &&\n reply.grade != null &&\n reply.userInstitutionId === currentUserId &&\n selectedTopic?.countsForGrade && (\n <Text size=\"sm\" color=\"text-text-600\" className=\"ml-10\">\n Nota {reply.grade}\n </Text>\n )}\n </div>\n {reply.userInstitutionId === currentUserId && (\n <PostActionsMenu\n onEdit={() => handleOpenEditReply(reply)}\n onDelete={() =>\n setPendingDelete({ type: 'reply', id: reply.id })\n }\n />\n )}\n </div>\n ))}\n </div>\n )}\n </>\n );\n })();\n\n return (\n <div className={cn('flex flex-col gap-4', className)}>\n {view === 'list' ? (\n <>\n {/* Header */}\n <div className=\"flex items-center justify-between\">\n <Text as=\"h1\" size=\"2xl\" weight=\"bold\" color=\"text-text-950\">\n {title}\n </Text>\n <Button\n variant=\"solid\"\n action=\"primary\"\n size=\"medium\"\n onClick={() => setIsCreateModalOpen(true)}\n >\n Criar post\n </Button>\n </div>\n\n {/* Subtitle + topic list */}\n <div className=\"flex flex-col gap-3\">\n <Text as=\"h2\" size=\"lg\" weight=\"semibold\" color=\"text-text-950\">\n {subtitle}\n </Text>\n\n {topicListContent}\n </div>\n </>\n ) : (\n /* Detail view */\n <>\n <button\n type=\"button\"\n className=\"flex items-center gap-2 hover:opacity-80 transition-opacity w-fit\"\n onClick={handleBack}\n >\n <CaretLeftIcon size={24} className=\"text-text-950\" />\n <Text size=\"2xl\" weight=\"bold\">\n Voltar\n </Text>\n </button>\n\n {topicDetailContent}\n </>\n )}\n\n {/* Create post modal */}\n <PostContentModal\n isOpen={isCreateModalOpen}\n title=\"Criar post\"\n submitLabel=\"Criar post\"\n content={createContent}\n imageUrl={createImageUrl}\n isSubmitting={isSubmitting}\n onContentChange={setCreateContent}\n onClose={handleCloseCreateModal}\n onSubmit={handleCreateTopic}\n onUploadImage={onUploadImage}\n onImageUploaded={setCreateImageUrl}\n maxLength={10000}\n richText\n showGradeQuestion={isTeacher}\n countsForGrade={createCountsForGrade}\n onCountsForGradeChange={setCreateCountsForGrade}\n />\n\n {/* Reply modal */}\n <PostContentModal\n isOpen={isReplyModalOpen}\n title=\"Responder\"\n submitLabel=\"Responder\"\n content={replyContent}\n imageUrl={replyImageUrl}\n isSubmitting={isSubmitting}\n onContentChange={setReplyContent}\n onClose={handleCloseReplyModal}\n onSubmit={handleCreateReply}\n onUploadImage={onUploadImage}\n onImageUploaded={setReplyImageUrl}\n maxLength={5000}\n richText\n />\n\n {/* Edit topic modal */}\n <PostContentModal\n isOpen={!!editingTopic}\n title=\"Editar\"\n submitLabel=\"Salvar\"\n content={editTopicContent}\n imageUrl={editTopicImageUrl}\n isSubmitting={isSubmitting}\n onContentChange={setEditTopicContent}\n onClose={handleCloseEditTopic}\n onSubmit={handleEditTopic}\n onUploadImage={onUploadImage}\n onImageUploaded={setEditTopicImageUrl}\n maxLength={10000}\n richText\n />\n\n {/* Edit reply modal */}\n <PostContentModal\n isOpen={!!editingReply}\n title=\"Editar\"\n submitLabel=\"Salvar\"\n content={editReplyContent}\n imageUrl={editReplyImageUrl}\n isSubmitting={isSubmitting}\n onContentChange={setEditReplyContent}\n onClose={handleCloseEditReply}\n onSubmit={handleEditReply}\n onUploadImage={onUploadImage}\n onImageUploaded={setEditReplyImageUrl}\n maxLength={5000}\n richText\n />\n\n {/* Evaluate / edit grade modal (teacher only) */}\n <Modal\n isOpen={!!evaluatingReplyId}\n onClose={handleCloseEvaluateModal}\n title={\n isEditingGrade\n ? 'Edite a nota da atividade'\n : 'Defina a nota da atividade'\n }\n size=\"md\"\n footer={\n <div className=\"flex justify-end gap-2\">\n <Button\n variant=\"outline\"\n action=\"secondary\"\n size=\"medium\"\n onClick={handleCloseEvaluateModal}\n >\n Cancelar\n </Button>\n <Button\n variant=\"solid\"\n action=\"primary\"\n size=\"medium\"\n disabled={!isValidGrade || isSubmitting}\n onClick={handleSubmitEvaluation}\n >\n {isEditingGrade ? 'Salvar' : 'Avaliar'}\n </Button>\n </div>\n }\n >\n <Input\n type=\"number\"\n min={0}\n max={10}\n step={1}\n placeholder=\"Nota (0-10)\"\n value={gradeValue ?? ''}\n onChange={(e) => {\n const val = e.target.value;\n const parsed = Number(val);\n setGradeValue(val === '' || Number.isNaN(parsed) ? null : parsed);\n }}\n />\n </Modal>\n\n {/* Delete confirmation modal */}\n <Modal\n isOpen={!!pendingDelete}\n onClose={() => setPendingDelete(null)}\n title=\"Deseja deletar post?\"\n size=\"sm\"\n footer={\n <div className=\"flex justify-end gap-2\">\n <Button\n variant=\"outline\"\n action=\"secondary\"\n size=\"medium\"\n onClick={() => setPendingDelete(null)}\n >\n Cancelar\n </Button>\n <Button\n variant=\"solid\"\n action=\"negative\"\n size=\"medium\"\n disabled={isSubmitting}\n onClick={handleConfirmDelete}\n >\n Deletar\n </Button>\n </div>\n }\n >\n <Text size=\"md\" color=\"text-text-600\">\n Essa ação é permanente e não poderá ser desfeita.\n </Text>\n </Modal>\n </div>\n );\n}\n\nexport default Forum;\n"]}
|
|
@@ -18,7 +18,7 @@ var _chunkBJMKBBN3js = require('./chunk-BJMKBBN3.js');
|
|
|
18
18
|
|
|
19
19
|
|
|
20
20
|
|
|
21
|
-
var
|
|
21
|
+
var _chunk2DPLSITCjs = require('./chunk-2DPLSITC.js');
|
|
22
22
|
|
|
23
23
|
|
|
24
24
|
var _chunkLS4EGCEJjs = require('./chunk-LS4EGCEJ.js');
|
|
@@ -120,25 +120,25 @@ var SingleNotificationCard = ({
|
|
|
120
120
|
/* @__PURE__ */ _jsxruntime.jsxs.call(void 0, "div", { className: "flex items-center gap-2 w-full", children: [
|
|
121
121
|
!isRead && /* @__PURE__ */ _jsxruntime.jsx.call(void 0, "div", { className: "w-[7px] h-[7px] bg-info-300 rounded-full flex-shrink-0" }),
|
|
122
122
|
/* @__PURE__ */ _jsxruntime.jsx.call(void 0, "h3", { className: "font-bold text-sm leading-4 text-text-950 flex-grow", children: title }),
|
|
123
|
-
/* @__PURE__ */ _jsxruntime.jsxs.call(void 0,
|
|
123
|
+
/* @__PURE__ */ _jsxruntime.jsxs.call(void 0, _chunk2DPLSITCjs.DropdownMenu_default, { children: [
|
|
124
124
|
/* @__PURE__ */ _jsxruntime.jsx.call(void 0,
|
|
125
|
-
|
|
125
|
+
_chunk2DPLSITCjs.DropdownMenuTrigger,
|
|
126
126
|
{
|
|
127
127
|
className: "flex-shrink-0 inline-flex items-center justify-center font-medium bg-transparent text-text-950 cursor-pointer hover:bg-info-50 w-6 h-6 rounded-lg",
|
|
128
128
|
"aria-label": "Menu de a\xE7\xF5es",
|
|
129
129
|
children: /* @__PURE__ */ _jsxruntime.jsx.call(void 0, _DotsThreeVertical.DotsThreeVerticalIcon, { size: 24 })
|
|
130
130
|
}
|
|
131
131
|
),
|
|
132
|
-
/* @__PURE__ */ _jsxruntime.jsxs.call(void 0,
|
|
132
|
+
/* @__PURE__ */ _jsxruntime.jsxs.call(void 0, _chunk2DPLSITCjs.DropdownMenuContent, { align: "end", className: "min-w-[160px]", children: [
|
|
133
133
|
!isRead && /* @__PURE__ */ _jsxruntime.jsx.call(void 0,
|
|
134
|
-
|
|
134
|
+
_chunk2DPLSITCjs.DropdownMenuItem,
|
|
135
135
|
{
|
|
136
136
|
onClick: handleMarkAsRead,
|
|
137
137
|
className: "text-text-950",
|
|
138
138
|
children: "Marcar como lida"
|
|
139
139
|
}
|
|
140
140
|
),
|
|
141
|
-
/* @__PURE__ */ _jsxruntime.jsx.call(void 0,
|
|
141
|
+
/* @__PURE__ */ _jsxruntime.jsx.call(void 0, _chunk2DPLSITCjs.DropdownMenuItem, { onClick: handleDelete, className: "text-error-600", children: "Deletar" })
|
|
142
142
|
] })
|
|
143
143
|
] })
|
|
144
144
|
] }),
|
|
@@ -419,11 +419,11 @@ var NotificationCenter = ({
|
|
|
419
419
|
}
|
|
420
420
|
return /* @__PURE__ */ _jsxruntime.jsxs.call(void 0, _jsxruntime.Fragment, { children: [
|
|
421
421
|
/* @__PURE__ */ _jsxruntime.jsxs.call(void 0,
|
|
422
|
-
|
|
422
|
+
_chunk2DPLSITCjs.DropdownMenu_default,
|
|
423
423
|
{
|
|
424
424
|
...typeof isActive === "boolean" ? { open: isActive, onOpenChange: handleOpenChange } : { onOpenChange: handleOpenChange },
|
|
425
425
|
children: [
|
|
426
|
-
/* @__PURE__ */ _jsxruntime.jsx.call(void 0,
|
|
426
|
+
/* @__PURE__ */ _jsxruntime.jsx.call(void 0, _chunk2DPLSITCjs.DropdownMenuTrigger, { className: "text-primary cursor-pointer", asChild: true, children: /* @__PURE__ */ _jsxruntime.jsx.call(void 0,
|
|
427
427
|
_chunkBVUPM6EYjs.IconButton_default,
|
|
428
428
|
{
|
|
429
429
|
active: isActive,
|
|
@@ -446,7 +446,7 @@ var NotificationCenter = ({
|
|
|
446
446
|
}
|
|
447
447
|
) }),
|
|
448
448
|
/* @__PURE__ */ _jsxruntime.jsx.call(void 0,
|
|
449
|
-
|
|
449
|
+
_chunk2DPLSITCjs.DropdownMenuContent,
|
|
450
450
|
{
|
|
451
451
|
className: "min-w-[320px] max-w-[400px] max-h-[500px] overflow-hidden",
|
|
452
452
|
side: "bottom",
|
|
@@ -630,4 +630,4 @@ var NotificationCard_default = NotificationCard;
|
|
|
630
630
|
|
|
631
631
|
|
|
632
632
|
exports.LegacyNotificationCard = LegacyNotificationCard; exports.NotificationCard_default = NotificationCard_default;
|
|
633
|
-
//# sourceMappingURL=chunk-
|
|
633
|
+
//# sourceMappingURL=chunk-DOIJGYYP.js.map
|