From bac7168b1ea214a39642967932787e87330d4c4a Mon Sep 17 00:00:00 2001 From: Kenneth Obsequio Date: Sun, 28 Jun 2026 11:30:01 +0800 Subject: [PATCH] push pushy Signed-off-by: Kenneth Obsequio --- src/App.jsx | 13 +- src/components/generic/AssetPickerSheet.jsx | 245 +++-- .../generic/ClientNotificationBell.jsx | 24 +- .../generic/Dialogs/BanUserDialog.jsx | 230 +++++ .../generic/Dialogs/UnbanDialog.jsx | 93 ++ src/components/generic/NotificationBell.jsx | 15 +- src/components/generic/Sheet/AddSheet.jsx | 57 +- src/components/generic/Sheet/FilterSheet.jsx | 10 +- src/components/generic/UserMenu.jsx | 64 +- src/contexts/AdminTierCategoriesContext.jsx | 76 ++ src/contexts/AdminTierPoliciesContext.jsx | 60 ++ src/contexts/AdminTiersContext.jsx | 25 - src/contexts/AdminUserContext.jsx | 83 ++ src/contexts/AuthContext.jsx | 3 +- .../ClientCourseReadingProgressContext.jsx | 32 +- src/contexts/ClientCoursesContext.jsx | 7 + src/contexts/ClientTaskProgressContext.jsx | 24 + src/contexts/ClientTiersProvider.jsx | 35 + src/contexts/DateTimePreferenceContext.jsx | 32 + src/hooks/useDateFormat.js | 37 + .../courses/CourseReadingProgressList.jsx | 13 +- .../admin/components/courses/UnitsTable.jsx | 2 +- .../admin/components/tiers/CoursePicker.jsx | 146 +++ .../admin/components/tiers/PaymentsTable.jsx | 16 +- .../admin/components/tiers/TierPlansTable.jsx | 40 +- .../admin/components/users/UserTable.jsx | 78 +- .../config/tiers/payments/columns.config.jsx | 59 +- .../config/tiers/plans/toolbar.config.jsx | 13 +- .../user_groups/view/rowActions.config.jsx | 19 +- .../user_groups/view/selection.config.jsx | 30 +- .../admin/config/users/rowActions.config.jsx | 33 +- .../admin/config/users/selection.config.jsx | 35 +- .../admin/pages/activity/ActivityFeed.jsx | 15 +- .../admin/pages/activity/UserActivityPage.jsx | 7 +- .../advertisements/AdvertisementList.jsx | 14 +- .../advertisements/ViewAdvertisement.jsx | 17 +- .../admin/pages/assets/ViewAudioAsset.jsx | 6 +- .../admin/pages/assets/ViewDocumentAsset.jsx | 6 +- .../admin/pages/assets/ViewImageAsset.jsx | 6 +- .../admin/pages/assets/ViewVideoAsset.jsx | 6 +- src/modules/admin/pages/courses/AddCourse.jsx | 18 +- .../admin/pages/courses/CourseAssessment.jsx | 30 +- .../admin/pages/courses/EditCourse.jsx | 24 +- .../admin/pages/courses/ViewAssessment.jsx | 33 +- .../admin/pages/courses/ViewCourse.jsx | 9 +- .../pages/courses/lessons/LessonsList.jsx | 6 +- .../pages/courses/lessons/ViewLesson.jsx | 9 +- .../admin/pages/courses/units/EditUnit.jsx | 2 +- .../units/{UnitQuiz.jsx => ModifyQuiz.jsx} | 32 +- .../admin/pages/courses/units/UnitsList.jsx | 8 +- .../pages/courses/units/ViewUnitQuiz.jsx | 21 +- .../admin/pages/task_list/CreateTaskList.jsx | 2 + .../admin/pages/task_list/ViewTaskList.jsx | 8 +- .../admin/pages/task_list/task/EditTask.jsx | 2 +- .../admin/pages/task_list/task/ViewTask.jsx | 8 +- src/modules/admin/pages/tiers/AddPlan.jsx | 395 +++----- src/modules/admin/pages/tiers/EditPlan.jsx | 179 +--- .../admin/pages/tiers/EditTierCategory.jsx | 353 +++++++ src/modules/admin/pages/tiers/PlanPolicy.jsx | 298 ++++++ .../admin/pages/tiers/SystemBadges.jsx | 222 +++++ .../admin/pages/tiers/TierCategories.jsx | 176 ++++ .../admin/pages/tiers/UserTierList.jsx | 59 +- src/modules/admin/pages/tiers/ViewPayment.jsx | 25 +- src/modules/admin/pages/tiers/ViewPlan.jsx | 76 +- .../admin/pages/user_groups/ViewGroup.jsx | 220 ++++- src/modules/admin/pages/users/ViewUser.jsx | 176 +++- src/modules/admin/routes/AdminRoutes.jsx | 37 +- src/modules/auth/components/LoginForm.jsx | 11 + src/modules/auth/pages/OAuthCallback.jsx | 83 +- src/modules/auth/routes/AuthRoutes.jsx | 2 + .../components/blocks/CourseCompleteBlock.jsx | 11 +- .../client/components/blocks/FileUpload.jsx | 67 +- .../client/components/blocks/QuizBlock.jsx | 906 ++++++++++++++---- .../client/components/blocks/ReadCourse.jsx | 50 +- .../client/components/blocks/ReadLesson.jsx | 25 +- .../client/components/blocks/ReadUnit.jsx | 45 +- .../client/components/blocks/VisitLink.jsx | 126 ++- src/modules/client/layout/ClientLayout.jsx | 23 +- src/modules/client/pages/AccountSettings.jsx | 91 +- src/modules/client/pages/Checkout.jsx | 17 +- src/modules/client/pages/CourseCheckout.jsx | 14 +- src/modules/client/pages/CourseDetails.jsx | 35 +- src/modules/client/pages/CourseList.jsx | 229 ++--- src/modules/client/pages/Dashboard.jsx | 283 +++--- src/modules/client/pages/MyAchievements.jsx | 6 +- src/modules/client/pages/MyCertificates.jsx | 6 +- src/modules/client/pages/PlanList.jsx | 98 +- src/modules/client/pages/Profile.jsx | 124 ++- src/modules/client/pages/UnitList.jsx | 453 +++++++-- src/modules/client/pages/ViewPlan.jsx | 14 +- src/modules/client/pages/ViewRequirement.jsx | 710 ++++++++------ src/modules/client/pages/ViewTask.jsx | 16 +- src/modules/public/pages/Suspended.jsx | 66 ++ .../staff/components/MemberDetailTabs.jsx | 15 +- .../staff/components/TaskListDetail.jsx | 16 +- src/modules/staff/components/TaskListsTab.jsx | 12 +- src/modules/staff/pages/GroupDetailPage.jsx | 15 +- src/utils/datetime.util.js | 87 ++ src/utils/tierBadge.util.js | 24 + src/utils/tierColors.js | 100 ++ 100 files changed, 5958 insertions(+), 1976 deletions(-) create mode 100644 src/components/generic/Dialogs/BanUserDialog.jsx create mode 100644 src/components/generic/Dialogs/UnbanDialog.jsx create mode 100644 src/contexts/AdminTierCategoriesContext.jsx create mode 100644 src/contexts/AdminTierPoliciesContext.jsx create mode 100644 src/contexts/DateTimePreferenceContext.jsx create mode 100644 src/hooks/useDateFormat.js create mode 100644 src/modules/admin/components/tiers/CoursePicker.jsx rename src/modules/admin/pages/courses/units/{UnitQuiz.jsx => ModifyQuiz.jsx} (95%) create mode 100644 src/modules/admin/pages/tiers/EditTierCategory.jsx create mode 100644 src/modules/admin/pages/tiers/PlanPolicy.jsx create mode 100644 src/modules/admin/pages/tiers/SystemBadges.jsx create mode 100644 src/modules/admin/pages/tiers/TierCategories.jsx create mode 100644 src/modules/public/pages/Suspended.jsx create mode 100644 src/utils/datetime.util.js create mode 100644 src/utils/tierBadge.util.js create mode 100644 src/utils/tierColors.js diff --git a/src/App.jsx b/src/App.jsx index d921180..2a5639f 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -1,6 +1,7 @@ import { useEffect } from 'react'; import { AuthProvider, useAuth, decodeToken } from './contexts/AuthContext'; import { ThemeProvider } from './contexts/ThemeContext'; +import { DateTimePreferenceProvider } from './contexts/DateTimePreferenceContext'; import { Helmet, HelmetProvider } from "react-helmet-async"; import { TooltipProvider } from './components/ui/tooltip'; import { setAuthInterceptor } from './utils/api.util'; @@ -78,11 +79,13 @@ export default function App() { - - - - - + + + + + + + ); diff --git a/src/components/generic/AssetPickerSheet.jsx b/src/components/generic/AssetPickerSheet.jsx index 52442b8..b12f994 100644 --- a/src/components/generic/AssetPickerSheet.jsx +++ b/src/components/generic/AssetPickerSheet.jsx @@ -1,25 +1,32 @@ -import { useEffect, useState, useCallback } from "react"; -import { Search, CheckCircle2 } from "lucide-react"; +import { useEffect, useState, useCallback, useRef } from "react"; +import { Search, CheckCircle2, SlidersHorizontal, X } from "lucide-react"; -import { Sheet, SheetContent, SheetHeader, SheetTitle, SheetDescription, } from "@/components/ui/sheet"; +import { Sheet, SheetContent, SheetHeader, SheetTitle, SheetDescription } from "@/components/ui/sheet"; import { Input } from "@/components/ui/input"; import { Button } from "@/components/ui/button"; +import { Badge } from "@/components/ui/badge"; import { Spinner } from "@/components/ui/spinner"; +import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover"; import { useAssets } from "@/contexts/AdminAssetsContext"; +import api from "@/utils/api.util"; -function EmptyState({ fileType }) { - return ( -
-

- No {fileType} assets found. -

-
- ); -} +const STREAM_BASE = `${import.meta.env.VITE_API_URL}/client/media/stream`; +const DEBOUNCE_MS = 400; -function AssetCard({ asset, selected, onSelect }) { - const thumb = asset.thumbnail_url ?? asset.file_url; +const EXT_OPTIONS = { + image: ["svg", "png", "jpg", "jpeg", "webp", "gif"], + video: ["mp4", "mov", "webm", "avi"], + audio: ["mp3", "wav", "ogg", "m4a"], + document: ["pdf", "docx", "xlsx", "pptx"], +}; + +// ─── Asset Card ─────────────────────────────────────────────────────────────── +// streamSrc is resolved at the sheet level (batch token request) — no per-card fetch. + +function AssetCard({ asset, streamSrc, selected, onSelect }) { + const directThumb = asset.thumbnail_url ?? asset.file_url; + const thumb = streamSrc ?? directThumb; return ( + + {extOptions.length > 0 && ( + + + + + +
+

File type

+ {hasActiveFilters && ( + + )} +
+
+ {extOptions.map((ext) => ( + + ))} +
+
+
+ )} + + {hasActiveFilters && ( +
+ {[...activeExts].map((ext) => ( + + {ext} + + + ))} +
+ )} {/* ── Grid ── */} @@ -156,6 +290,7 @@ export function AssetPickerSheet({ open, onOpenChange, fileType, onSelect }) { @@ -194,4 +329,4 @@ export function AssetPickerSheet({ open, onOpenChange, fileType, onSelect }) { ); -} \ No newline at end of file +} diff --git a/src/components/generic/ClientNotificationBell.jsx b/src/components/generic/ClientNotificationBell.jsx index 63e9048..777ecfa 100644 --- a/src/components/generic/ClientNotificationBell.jsx +++ b/src/components/generic/ClientNotificationBell.jsx @@ -7,6 +7,7 @@ import { ScrollArea } from "@/components/ui/scroll-area"; import { Separator } from "@/components/ui/separator"; import { useClientNotifications } from "@/contexts/ClientNotificationContext"; import { cn } from "@/lib/utils"; +import { useDateFormat } from "@/hooks/useDateFormat"; const TYPE_ICON = { achievement: Trophy, @@ -31,20 +32,10 @@ function timeAgo(dateStr) { return `${Math.floor(h / 24)}d ago`; } -function formatDate(dateStr) { - return new Date(dateStr).toLocaleString("en-US", { - month: "long", - day: "numeric", - year: "numeric", - hour: "numeric", - minute: "2-digit", - hour12: true, - }); -} - export default function ClientNotificationBell() { const { notifications, unseenCount, loading, fetchNotifications, markSeen, markAllSeen } = useClientNotifications(); + const { fmtDateTime } = useDateFormat(); const [selected, setSelected] = useState(null); const [copiedCode, setCopiedCode] = useState(false); @@ -79,15 +70,16 @@ export default function ClientNotificationBell() { -
+
Notifications {unseenCount > 0 && ( - + )}
@@ -186,7 +178,7 @@ export default function ClientNotificationBell() {
{selected?.type} - {selected ? formatDate(selected.createdAt) : ""} + {selected ? fmtDateTime(selected.createdAt) : ""}
diff --git a/src/components/generic/Dialogs/BanUserDialog.jsx b/src/components/generic/Dialogs/BanUserDialog.jsx new file mode 100644 index 0000000..46fadef --- /dev/null +++ b/src/components/generic/Dialogs/BanUserDialog.jsx @@ -0,0 +1,230 @@ +// ─── components/Dialogs/BanUserDialog.jsx ───────────────────────────────────── +import { useState } from "react"; +import { + Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription, DialogFooter, +} from "@/components/ui/dialog"; +import { Button } from "@/components/ui/button"; +import { Label } from "@/components/ui/label"; +import { Textarea } from "@/components/ui/textarea"; +import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group"; +import { Spinner } from "@/components/ui/spinner"; +import { Calendar } from "@/components/ui/calendar"; +import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover"; +import { Card, CardContent, CardFooter } from "@/components/ui/card"; +import { Field, FieldGroup, FieldLabel } from "@/components/ui/field"; +import { InputGroup, InputGroupAddon, InputGroupInput } from "@/components/ui/input-group"; +import { CalendarIcon, Clock2Icon } from "lucide-react"; +import { cn } from "@/lib/utils"; +import { fmtDate } from "@/utils/datetime.util"; + +const EMPTY = { reason: "", ban_type: "temporary", expires_at: "", _date: null, _time: "12:00" }; + +/** + * Ban dialog — single or bulk. + * + * Single: r.name} ... /> + * Bulk: + * + * @param {Function} onBan (payload) => Promise — called with { reason, ban_type, expires_at? } + * For bulk, caller merges ids on top. + */ +export function BanUserDialog({ + open, + onOpenChange, + entity, + ids, + entityLabel = "User", + getName, + onBan, + loading, + onSuccess, +}) { + const [form, setForm] = useState(EMPTY); + const [errors, setErrors] = useState({}); + const [calOpen, setCalOpen] = useState(false); + + const mergeDateTime = (date, time) => { + if (!date) return ""; + const [h, m] = (time || "12:00").split(":").map(Number); + const d = new Date(date); + d.setHours(h, m, 0, 0); + return d.toISOString(); + }; + + const isBulk = Array.isArray(ids) && ids.length > 0; + const count = isBulk ? ids.length : 1; + const displayName = isBulk + ? `${count} selected ${entityLabel.toLowerCase()}${count !== 1 ? "s" : ""}` + : (getName?.(entity) ?? entity?.personal_info?.name?.full_name ?? entity?.email ?? "this user"); + + const validate = () => { + const e = {}; + if (!form.reason.trim()) e.reason = "Reason is required."; + if (form.ban_type === "temporary") { + if (!form.expires_at) e.expires_at = "Expiry date is required."; + else if (new Date(form.expires_at) <= new Date()) e.expires_at = "Expiry must be in the future."; + } + setErrors(e); + return Object.keys(e).length === 0; + }; + + const handleSubmit = async () => { + if (!validate()) return; + const payload = { + reason: form.reason.trim(), + ban_type: form.ban_type, + ...(form.ban_type === "temporary" ? { expires_at: form.expires_at } : {}), + }; + const res = await onBan(payload); + if (res) { + setForm(EMPTY); + setErrors({}); + onOpenChange(false); + onSuccess?.(); + } + }; + + const handleOpenChange = (v) => { + if (!v) { setForm(EMPTY); setErrors({}); } + onOpenChange(v); + }; + + + return ( + + + + + Ban {isBulk ? `${count} ${entityLabel}${count !== 1 ? "s" : ""}` : entityLabel} + + + You are about to ban{" "} + {displayName}. + They will be logged out immediately and blocked from accessing the platform. + + + +
+ {/* Reason */} +
+ +