From 3c5432a32b692f93c8be814207d137b8f050e1c7 Mon Sep 17 00:00:00 2001 From: rgrgogu Date: Thu, 13 Aug 2026 20:00:06 +0800 Subject: [PATCH] bing Signed-off-by: rgrgogu --- src/components/generic/AvatarUploadDialog.jsx | 2 +- src/components/generic/DashboardGrid.jsx | 6 +- src/components/generic/DeadlinePicker.jsx | 116 ++++++----- src/components/generic/Profile.jsx | 3 +- src/contexts/AdminLibraryContext.jsx | 50 ----- src/contexts/ClientLibraryContext.jsx | 31 --- src/contexts/ProfileProvider.jsx | 3 +- src/data/adminTiles.data.js | 1 - src/hooks/usePurchaseCheckout.js | 71 ------- .../courses/CourseReadingProgressList.jsx | 5 +- .../products/ProductPricingCard.jsx | 173 ---------------- src/modules/admin/pages/AdminDashboard.jsx | 2 +- .../admin/pages/activity/ActivityFeed.jsx | 3 +- .../library/lessons/EditLibraryLesson.jsx | 17 +- .../pages/library/units/EditLibraryUnit.jsx | 17 +- src/modules/admin/pages/users/EditUser.jsx | 3 +- src/modules/admin/pages/users/ViewUser.jsx | 3 +- src/modules/auth/pages/Intro.jsx | 3 +- .../client/components/LessonUpsellModal.jsx | 26 +-- .../client/components/LockedContentPanel.jsx | 32 +-- .../client/components/UnitUpsellModal.jsx | 28 +-- src/modules/client/layout/ClientLayout.jsx | 4 +- src/modules/client/pages/EditProfile.jsx | 3 +- src/modules/client/pages/LessonCheckout.jsx | 190 ------------------ src/modules/client/pages/LessonDetails.jsx | 1 - src/modules/client/pages/PlanList.jsx | 145 +------------ src/modules/client/pages/UnitCheckout.jsx | 190 ------------------ src/modules/client/pages/UnitDetails.jsx | 1 - src/modules/client/routes/ClientRoutes.jsx | 4 - 29 files changed, 104 insertions(+), 1029 deletions(-) delete mode 100644 src/hooks/usePurchaseCheckout.js delete mode 100644 src/modules/admin/components/products/ProductPricingCard.jsx delete mode 100644 src/modules/client/pages/LessonCheckout.jsx delete mode 100644 src/modules/client/pages/UnitCheckout.jsx diff --git a/src/components/generic/AvatarUploadDialog.jsx b/src/components/generic/AvatarUploadDialog.jsx index b0544c5..1facf4b 100644 --- a/src/components/generic/AvatarUploadDialog.jsx +++ b/src/components/generic/AvatarUploadDialog.jsx @@ -23,7 +23,7 @@ function loadImage(src) { }) } -async function cropToBlob(imageSrc, pixels, outputSize = 512) { +async function cropToBlob(imageSrc, pixels, outputSize = 200) { const img = await loadImage(imageSrc) const canvas = document.createElement('canvas') canvas.width = outputSize diff --git a/src/components/generic/DashboardGrid.jsx b/src/components/generic/DashboardGrid.jsx index fa941f2..ebf01e3 100644 --- a/src/components/generic/DashboardGrid.jsx +++ b/src/components/generic/DashboardGrid.jsx @@ -11,8 +11,8 @@ export default function DashboardGrid({ sections = [] }) { } return ( -
-
+
+
{sections.map(({ title, description, tiles }) => (
@@ -25,7 +25,7 @@ export default function DashboardGrid({ sections = [] }) {
{/* Tiles */} -
+
{tiles.map(({ key, label, icon: Icon, link }) => ( { onChange(buildISO(date, timeStr)); - setOpen(false); }; const handleTimeChange = (e) => { @@ -61,62 +62,67 @@ export default function DeadlinePicker({ value, onChange, disabled = false }) { const handleClear = () => onChange(null); return ( -
- - {/* ── Date picker ──────────────────────────────────────────────── */} -
- - - - - - + + + + + + + - - -
- - {/* ── Time input ───────────────────────────────────────────────── */} -
- - -
- - {/* ── Clear ────────────────────────────────────────────────────── */} - {value && ( - - )} - -
+ + + + + Time + + + + + + + + + {value && ( + + )} + + + + ); } \ No newline at end of file diff --git a/src/components/generic/Profile.jsx b/src/components/generic/Profile.jsx index fa12429..2d3df4d 100644 --- a/src/components/generic/Profile.jsx +++ b/src/components/generic/Profile.jsx @@ -3,6 +3,7 @@ import { useProfile } from '@/contexts/ProfileProvider' import { useState, useEffect } from 'react' import { Camera, Loader2, Phone, MapPin, Shield, Trophy, Activity, Plus, Trash2, Pencil, Home, Building2, Edit2, Check, X } from 'lucide-react' import AvatarUploadDialog from '@/components/generic/AvatarUploadDialog' +import { resolveAssetSrc } from '@/utils/media.util' import { Button } from '@/components/ui/button' import { Input } from '@/components/ui/input' @@ -233,7 +234,7 @@ export default function ProfilePage() { const fullName = info?.name?.full_name ?? user?.email ?? 'User' const initials = ((info?.name?.given_name?.[0] ?? '') + (info?.name?.last_name?.[0] ?? '')).toUpperCase() || user?.email?.[0]?.toUpperCase() || 'U' - const avatarUrl = info?.avatar?.url ?? null + const avatarUrl = resolveAssetSrc(info?.avatar) const avatarColor = AVATAR_COLORS[user?.acc_type] ?? 'bg-muted text-muted-foreground' // Load fresh profile data on mount diff --git a/src/contexts/AdminLibraryContext.jsx b/src/contexts/AdminLibraryContext.jsx index b812a2a..e973efc 100644 --- a/src/contexts/AdminLibraryContext.jsx +++ b/src/contexts/AdminLibraryContext.jsx @@ -448,54 +448,6 @@ export function LibraryProvider({ children }) { [request], ); - // ─── Product listings (individual purchase, parity with course products) ── - - const fetchUnitProduct = useCallback( - (unitId) => request(async () => { - const { data } = await api.get(`/admin/products/units/${unitId}/product`); - return data.data ?? null; - }), [request], - ); - - const saveUnitProduct = useCallback( - (unitId, payload) => request(async () => { - const { data } = await api.put(`/admin/products/units/${unitId}/product`, payload); - toast("Product listing saved."); - return data.data ?? null; - }), [request], - ); - - const removeUnitProduct = useCallback( - (unitId) => request(async () => { - await api.delete(`/admin/products/units/${unitId}/product`); - toast("Product listing removed."); - return true; - }), [request], - ); - - const fetchLessonProduct = useCallback( - (lessonId) => request(async () => { - const { data } = await api.get(`/admin/products/lessons/${lessonId}/product`); - return data.data ?? null; - }), [request], - ); - - const saveLessonProduct = useCallback( - (lessonId, payload) => request(async () => { - const { data } = await api.put(`/admin/products/lessons/${lessonId}/product`, payload); - toast("Product listing saved."); - return data.data ?? null; - }), [request], - ); - - const removeLessonProduct = useCallback( - (lessonId) => request(async () => { - await api.delete(`/admin/products/lessons/${lessonId}/product`); - toast("Product listing removed."); - return true; - }), [request], - ); - // ─── Value ──────────────────────────────────────────────────────────────── const value = { // shared table state @@ -511,7 +463,6 @@ export function LibraryProvider({ children }) { fetchUnitFieldValues, attachLessonsToUnit, detachLessonFromUnit, detachUnitFromCourse, attachUnitToCourses, reorderUnitLessons, attachLessonToUnits, - fetchUnitProduct, saveUnitProduct, removeUnitProduct, // lesson library lessons, lesson, lessonsFlat, @@ -521,7 +472,6 @@ export function LibraryProvider({ children }) { permanentlyDeleteLesson, permanentlyDeleteLessons, fetchLessonPermanentDeleteImpact, fetchLessonFieldValues, - fetchLessonProduct, saveLessonProduct, removeLessonProduct, }; return {children}; diff --git a/src/contexts/ClientLibraryContext.jsx b/src/contexts/ClientLibraryContext.jsx index af0c81c..fcc124b 100644 --- a/src/contexts/ClientLibraryContext.jsx +++ b/src/contexts/ClientLibraryContext.jsx @@ -36,12 +36,6 @@ export function ClientLibraryProvider({ children }) { const [quiz, setQuiz] = useState(null); const [quizLoading, setQuizLoading] = useState(false); - // ── Checkout info (unit/lesson individual-purchase page) — deliberately not - // gated by canAccessUnit/canAccessLesson like unitDetail/lesson above, since - // this is exactly what a locked-and-unpurchased learner needs to see. - const [checkoutInfo, setCheckoutInfo] = useState(null); - const [checkoutInfoLoading, setCheckoutInfoLoading] = useState(false); - // ─── Actions ──────────────────────────────────────────────────────────── const getUnits = useCallback(async () => { @@ -228,26 +222,6 @@ export function ClientLibraryProvider({ children }) { } }, []); - const getUnitCheckoutInfo = useCallback(async (uuid) => { - setCheckoutInfoLoading(true); - try { - const { data } = await api.get(`/client/units/${uuid}/checkout-info`); - setCheckoutInfo(data.data ?? null); - } catch (err) { - toast(err?.response?.data?.message ?? "Could not load unit."); - } finally { setCheckoutInfoLoading(false); } - }, []); - - const getLessonCheckoutInfo = useCallback(async (uuid) => { - setCheckoutInfoLoading(true); - try { - const { data } = await api.get(`/client/lessons/${uuid}/checkout-info`); - setCheckoutInfo(data.data ?? null); - } catch (err) { - toast(err?.response?.data?.message ?? "Could not load lesson."); - } finally { setCheckoutInfoLoading(false); } - }, []); - // ─── Resets ───────────────────────────────────────────────────────────── const resetUnitDetail = useCallback(() => { @@ -257,7 +231,6 @@ export function ClientLibraryProvider({ children }) { }, []); const resetLesson = useCallback(() => setLesson(null), []); const resetQuiz = useCallback(() => setQuiz(null), []); - const resetCheckoutInfo = useCallback(() => setCheckoutInfo(null), []); // ─── Value ────────────────────────────────────────────────────────────── @@ -267,7 +240,6 @@ export function ClientLibraryProvider({ children }) { unitDetail, unitDetailLoading, unitBlocked, unitBlockedInfo, lesson, lessonLoading, quiz, quizLoading, - checkoutInfo, checkoutInfoLoading, getUnits, getLessons, @@ -279,13 +251,10 @@ export function ClientLibraryProvider({ children }) { upsertLessonProgress, upsertWatchProgress, markComplete, - getUnitCheckoutInfo, - getLessonCheckoutInfo, resetUnitDetail, resetLesson, resetQuiz, - resetCheckoutInfo, }; return ( diff --git a/src/contexts/ProfileProvider.jsx b/src/contexts/ProfileProvider.jsx index 3f7540d..fffa8ed 100644 --- a/src/contexts/ProfileProvider.jsx +++ b/src/contexts/ProfileProvider.jsx @@ -2,6 +2,7 @@ import { createContext, useCallback, useContext, useState } from "react"; import api from "@/utils/api.util"; import { toast } from "sonner"; import { useAuth } from "@/contexts/AuthContext"; +import { resolveAssetSrc } from "@/utils/media.util"; const ProfileContext = createContext(null); @@ -143,7 +144,7 @@ export function ProfileProvider({ children, apiBase = '/client' }) { const fullName = pi?.name?.full_name ?? ""; const givenName = pi?.name?.given_name ?? ""; const lastName = pi?.name?.last_name ?? ""; - const avatarUrl = pi?.avatar?.url ?? ""; + const avatarUrl = resolveAssetSrc(pi?.avatar) ?? ""; const occupation = pi?.occupation ?? ""; // ─── Reset helpers ───────────────────────────────────────────────────────── diff --git a/src/data/adminTiles.data.js b/src/data/adminTiles.data.js index 3fce239..58d21b1 100644 --- a/src/data/adminTiles.data.js +++ b/src/data/adminTiles.data.js @@ -32,7 +32,6 @@ export const ADMIN_SECTIONS = [ { key: "units", label: "Units", icon: BookCheck, link: "/admin/units" }, { key: "lessons", label: "Lessons", icon: FileText, link: "/admin/lessons" }, { key: "tasks", label: "Tasks", icon: ListCheck, link: "/admin/taskList" }, - ], }, { diff --git a/src/hooks/usePurchaseCheckout.js b/src/hooks/usePurchaseCheckout.js deleted file mode 100644 index a08f436..0000000 --- a/src/hooks/usePurchaseCheckout.js +++ /dev/null @@ -1,71 +0,0 @@ -import { useEffect, useRef, useState } from "react"; -import { useNavigate, useSearchParams } from "react-router-dom"; -import { toast } from "sonner"; -import { usePurchases } from "./usePurchases"; - -/** - * Shared PayPal order/capture/cancel flow for a standalone checkout page - * (Unit or Lesson) — mirrors CourseCheckout.jsx's logic exactly, including - * the cancel-vs-capture race-condition guard: a real PayPal cancel redirect - * carries BOTH `cancelled=true` (our own cancelUrl) and `token=` - * (PayPal always appends its own token to whatever return/cancel URL it's - * given) — without the `wasCancelled` check below, both the capture effect - * and the cancel effect would fire on the same load, racing a capture call - * against the cancel and surfacing a confusing "not found" error instead of - * a clean cancellation message. - * - * @param {string} targetId - uuid used to reload the page's own detail state - * @param {(id: string) => void} loadTarget - memoized loader, called on mount and again after a successful capture - * @param {string} checkoutPath - this page's own path, used to strip query params after a cancel - */ -export function usePurchaseCheckout({ targetId, loadTarget, checkoutPath }) { - const navigate = useNavigate(); - const [searchParams] = useSearchParams(); - const { purchaseLoading, createOrder, captureOrder, cancelOrder } = usePurchases(); - - const returnToken = searchParams.get("token"); - const wasCancelled = searchParams.get("cancelled") === "true"; - - const [capturing, setCapturing] = useState(false); - const capturingRef = useRef(false); - - useEffect(() => { - loadTarget(targetId); - }, [targetId]); // eslint-disable-line react-hooks/exhaustive-deps - - useEffect(() => { - if (!returnToken || wasCancelled || capturingRef.current) return; - capturingRef.current = true; - setCapturing(true); - captureOrder(returnToken).then((result) => { - if (result) { - loadTarget(targetId); - navigate(checkoutPath, { replace: true }); - } else { - setCapturing(false); - capturingRef.current = false; - } - }); - }, [returnToken]); // eslint-disable-line react-hooks/exhaustive-deps - - useEffect(() => { - if (!wasCancelled) return; - const orderId = searchParams.get("token"); - if (orderId) cancelOrder(orderId); - // Deferred to a new macrotask, same reasoning as CourseCheckout.jsx: this - // page renders before in the layout, so a toast fired - // synchronously here races the Toaster's own mount effect and is - // silently dropped. - setTimeout(() => toast("Payment was cancelled."), 0); - navigate(checkoutPath, { replace: true }); - }, [wasCancelled]); // eslint-disable-line react-hooks/exhaustive-deps - - const buyNow = async (productId) => { - const order = await createOrder(productId); - if (!order) return; - if (!order.approval_url) { toast("Could not get PayPal approval URL."); return; } - window.location.href = order.approval_url; - }; - - return { capturing, purchaseLoading, buyNow }; -} diff --git a/src/modules/admin/components/courses/CourseReadingProgressList.jsx b/src/modules/admin/components/courses/CourseReadingProgressList.jsx index c1c6e36..a3f4696 100644 --- a/src/modules/admin/components/courses/CourseReadingProgressList.jsx +++ b/src/modules/admin/components/courses/CourseReadingProgressList.jsx @@ -15,6 +15,7 @@ import { import { TablePagination } from '@/components/generic/Table/TablePagination'; import { useAdminCourseReadingProgress } from '@/contexts/AdminCourseReadingProgressContext'; import { useDateFormat } from '@/hooks/useDateFormat'; +import { streamUrl } from '@/utils/media.util'; // ─── Helpers ────────────────────────────────────────────────────────────────── @@ -97,7 +98,7 @@ function UserDetailDialog({ open, onOpenChange, entry, courseId }) {
- {entry && } + {entry && }
{entry?.user.full_name ?? No name} @@ -205,7 +206,7 @@ function UserCard({ entry, onOpen }) { onClick={() => onOpen(entry)} className="bg-background w-full text-left border rounded-lg p-4 flex items-center gap-3 hover:bg-accent/10 transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring" > - +
diff --git a/src/modules/admin/components/products/ProductPricingCard.jsx b/src/modules/admin/components/products/ProductPricingCard.jsx deleted file mode 100644 index f8fc658..0000000 --- a/src/modules/admin/components/products/ProductPricingCard.jsx +++ /dev/null @@ -1,173 +0,0 @@ -import { useEffect, useState } from "react"; -import { Save } from "lucide-react"; - -import { Button } from "@/components/ui/button"; -import { Input } from "@/components/ui/input"; -import { Label } from "@/components/ui/label"; -import { Spinner } from "@/components/ui/spinner"; -import { Switch } from "@/components/ui/switch"; - -const EMPTY_FORM = { name: "", price: "", currency: "USD", access_days: "", is_active: true }; - -/** - * Self-contained product-listing editor for individual-purchase pricing — - * fetches on mount, saves via its own button (same convention as - * CompletionRequirementBuilder: fetchFn/saveFn/removeFn + args resolve the - * target server-side, so this one component covers Course/Unit/Lesson). - */ -export default function ProductPricingCard({ label = "this content", fetchFn, saveFn, removeFn, args = [] }) { - const [product, setProduct] = useState(null); - const [form, setForm] = useState(EMPTY_FORM); - const [dirty, setDirty] = useState(false); - const [loading, setLoading] = useState(true); - const [saving, setSaving] = useState(false); - - useEffect(() => { - let active = true; - fetchFn(...args).then((prod) => { - if (!active) return; - if (prod) { - setProduct(prod); - setForm({ - name: prod.name ?? "", - price: prod.price ?? "", - currency: prod.currency ?? "USD", - access_days: prod.access_days ?? "", - is_active: prod.is_active ?? true, - }); - } - setLoading(false); - setDirty(false); - }); - return () => { active = false; }; - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [...args]); - - const handleChange = (field, value) => { - setForm((prev) => ({ ...prev, [field]: value })); - setDirty(true); - }; - - const handleSave = async () => { - if (!form.price) return; - setSaving(true); - const saved = await saveFn(...args, { - name: form.name || null, - price: Number(form.price), - currency: form.currency || "USD", - access_days: form.access_days ? Number(form.access_days) : null, - is_active: form.is_active, - }); - if (saved) { setProduct(saved); setDirty(false); } - setSaving(false); - }; - - const handleRemove = async () => { - setSaving(true); - await removeFn(...args); - setProduct(null); - setForm(EMPTY_FORM); - setDirty(false); - setSaving(false); - }; - - if (loading) { - return ( -
- -
- ); - } - - return ( -
-

- Allow learners to purchase {label} individually via PayPal, as an alternative to a tier plan. -

- -
-
- - handleChange("name", e.target.value)} - /> -

Defaults to the title if left blank.

-
-
- - handleChange("price", e.target.value)} - /> -
-
- - handleChange("currency", e.target.value.toUpperCase())} - /> -
-
- - handleChange("access_days", e.target.value)} - /> -
-
- -
-
- -

Show a "Buy" button to learners.

-
- handleChange("is_active", v)} - /> -
- -
- {product && ( - - )} - -
-
- ); -} diff --git a/src/modules/admin/pages/AdminDashboard.jsx b/src/modules/admin/pages/AdminDashboard.jsx index e283fb4..893b39c 100644 --- a/src/modules/admin/pages/AdminDashboard.jsx +++ b/src/modules/admin/pages/AdminDashboard.jsx @@ -37,7 +37,7 @@ export default function AdminDashboard() {
{/* ── Sections — same array, one DashboardGrid per entry ── */} -
+
{ADMIN_SECTIONS.map((s) => (
{s.tiles.length > 0 ? ( diff --git a/src/modules/admin/pages/activity/ActivityFeed.jsx b/src/modules/admin/pages/activity/ActivityFeed.jsx index 8e5b951..f902bac 100644 --- a/src/modules/admin/pages/activity/ActivityFeed.jsx +++ b/src/modules/admin/pages/activity/ActivityFeed.jsx @@ -16,6 +16,7 @@ import { ACTION_CONFIG, getActionBadge } from "@/data/activity.data"; import { timeAgo } from "@/utils/timestamp.util"; import { fmtISO } from "@/utils/datetime.util"; import { useDateFormat } from "@/hooks/useDateFormat"; +import { streamUrl } from "@/utils/media.util"; const BREADCRUMB = [ { label: "Home", icon: , to: "/admin" }, @@ -211,7 +212,7 @@ function ActivityRow({ row, onViewUser }) {
- + {initials(row.full_name, row.email)} diff --git a/src/modules/admin/pages/library/lessons/EditLibraryLesson.jsx b/src/modules/admin/pages/library/lessons/EditLibraryLesson.jsx index d19ca58..e8165e7 100644 --- a/src/modules/admin/pages/library/lessons/EditLibraryLesson.jsx +++ b/src/modules/admin/pages/library/lessons/EditLibraryLesson.jsx @@ -20,7 +20,6 @@ import { } from "@/components/ui/select"; import { useUnsavedChangesGuard } from "@/hooks/useUnsavedChangesGuard"; import CompletionRequirementBuilder from "@/modules/admin/components/courses/CompletionRequirementBuilder"; -import ProductPricingCard from "@/modules/admin/components/products/ProductPricingCard"; const schema = z.object({ title: z.string().min(1, "Title is required."), @@ -36,7 +35,7 @@ function FieldError({ message }) { export default function EditLibraryLesson() { const navigate = useNavigate(); const { lessonId } = useParams(); - const { fetchLesson, updateLesson, lesson, loading, fetchLessonProduct, saveLessonProduct, removeLessonProduct } = useLibrary(); + const { fetchLesson, updateLesson, lesson, loading } = useLibrary(); const { fetchLessonRequirements, syncLessonRequirements } = useCourses(); const { user } = useAuth(); @@ -153,20 +152,6 @@ export default function EditLibraryLesson() { args={[null, null, lessonId]} />
- -
-
-

Pricing

-

Optional individual-purchase listing for this lesson.

-
- -
diff --git a/src/modules/admin/pages/library/units/EditLibraryUnit.jsx b/src/modules/admin/pages/library/units/EditLibraryUnit.jsx index 9b712a0..fbb6f53 100644 --- a/src/modules/admin/pages/library/units/EditLibraryUnit.jsx +++ b/src/modules/admin/pages/library/units/EditLibraryUnit.jsx @@ -11,7 +11,6 @@ import { useAuth } from "@/contexts/AuthContext"; import { PageMeta } from "@/contexts/MetadataContext"; import api from "@/utils/api.util"; import CompletionRequirementBuilder from "@/modules/admin/components/courses/CompletionRequirementBuilder"; -import ProductPricingCard from "@/modules/admin/components/products/ProductPricingCard"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; @@ -36,7 +35,7 @@ function FieldError({ message }) { export default function EditLibraryUnit() { const navigate = useNavigate(); const { unitId } = useParams(); - const { fetchUnit, updateUnit, unit, loading, fetchUnitProduct, saveUnitProduct, removeUnitProduct } = useLibrary(); + const { fetchUnit, updateUnit, unit, loading } = useLibrary(); const { fetchUnitRequirements, syncUnitRequirements } = useCourses(); const { user } = useAuth(); @@ -153,20 +152,6 @@ export default function EditLibraryUnit() { args={[null, unitId]} />
- -
-
-

Pricing

-

Optional individual-purchase listing for this unit.

-
- -
diff --git a/src/modules/admin/pages/users/EditUser.jsx b/src/modules/admin/pages/users/EditUser.jsx index ad8b51f..f45ab3a 100644 --- a/src/modules/admin/pages/users/EditUser.jsx +++ b/src/modules/admin/pages/users/EditUser.jsx @@ -18,6 +18,7 @@ import { } from "@/components/ui/select"; import { ArrowLeft, Save, Plus, Trash2, UserCircle2 } from "lucide-react"; import { useUnsavedChangesGuard } from "@/hooks/useUnsavedChangesGuard"; +import { resolveAssetSrc } from "@/utils/media.util"; // ─── Schema ─────────────────────────────────────────────────────────────────── const addressSchema = z.object({ @@ -106,7 +107,7 @@ export default function EditUser() { const info = user.personal_info ?? {}; const name = info.name ?? {}; - setAvatarPreview(info.avatar?.url ?? null); + setAvatarPreview(resolveAssetSrc(info.avatar) ?? null); reset({ acc_type: user.acc_type ?? "user", diff --git a/src/modules/admin/pages/users/ViewUser.jsx b/src/modules/admin/pages/users/ViewUser.jsx index 4c478d1..f351a94 100644 --- a/src/modules/admin/pages/users/ViewUser.jsx +++ b/src/modules/admin/pages/users/ViewUser.jsx @@ -18,6 +18,7 @@ import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip import { BanUserDialog } from "@/components/generic/Dialogs/BanUserDialog"; import { UnbanDialog } from "@/components/generic/Dialogs/UnbanDialog"; import { useDateFormat } from "@/hooks/useDateFormat"; +import { resolveAssetSrc } from "@/utils/media.util"; // ─── Helper ─────────────────────────────────────────────────────────────────── const StatusBadge = ({ value }) => ( @@ -86,7 +87,7 @@ export default function ViewUser() {
- + {(name.full_name ?? user.email ?? "?") .split(" ").map((n) => n[0]).slice(0, 2).join("").toUpperCase()} diff --git a/src/modules/auth/pages/Intro.jsx b/src/modules/auth/pages/Intro.jsx index e8c0ec7..68db983 100644 --- a/src/modules/auth/pages/Intro.jsx +++ b/src/modules/auth/pages/Intro.jsx @@ -18,6 +18,7 @@ import { Input } from '@/components/ui/input' import { PhoneInput } from '@/components/ui/phone-input' import { Label } from '@/components/ui/label' import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar' +import { resolveAssetSrc } from '@/utils/media.util' import { Loader2 } from 'lucide-react' import { toast } from 'sonner' import { Toaster } from '@/components/ui/sonner' @@ -59,7 +60,7 @@ export default function IntroPage() { // ── Derived ──────────────────────────────────────────────────────────────── - const avatarUrl = pi.avatar?.url ?? '' + const avatarUrl = resolveAssetSrc(pi.avatar) ?? '' const initials = getInitials(givenName, lastName) const displayEmail = user?.email ?? '' diff --git a/src/modules/client/components/LessonUpsellModal.jsx b/src/modules/client/components/LessonUpsellModal.jsx index d33ee88..b24e4c7 100644 --- a/src/modules/client/components/LessonUpsellModal.jsx +++ b/src/modules/client/components/LessonUpsellModal.jsx @@ -1,25 +1,19 @@ // LessonUpsellModal — shown when a learner clicks a locked standalone Lesson. // Mirrors UnitUpsellModal.jsx: unlocks via any course reachable through its // attached Units (aggregated across all of them, since a Lesson can sit in -// more than one), with an optional direct "Buy" listing if the lesson itself -// has an active product, plus a generic "View Plans" fallback. +// more than one), plus a generic "View Plans" fallback. // Shared by LessonsList and Dashboard. import { useNavigate } from "react-router-dom"; -import { LockIcon, ShoppingCart, GraduationCap, Check } from "lucide-react"; +import { LockIcon, Check } from "lucide-react"; import ResponsiveModal from "@/components/generic/ResponsiveModal"; import { Badge } from "@/components/ui/badge"; import { Button } from "@/components/ui/button"; -import { useDateFormat } from "@/hooks/useDateFormat"; import { resolveTierBadge, cheapestTierSlug } from "@/utils/tierBadge.util"; export default function LessonUpsellModal({ open, onOpenChange, lesson, tierMap = {} }) { const navigate = useNavigate(); - const { fmtCurrency } = useDateFormat(); const courses = lesson?.courses ?? []; - const purchasable = lesson?.product?.is_active && !lesson?.has_purchased; - const awaitingStarterSet = purchasable && lesson?.purchase_eligible === false; - const canBuy = purchasable && !awaitingStarterSet; const slug = cheapestTierSlug([lesson?.subscription, ...courses.map((c) => c.subscription)], tierMap); const { rank, label, cls, panel } = resolveTierBadge(slug, tierMap); @@ -33,27 +27,13 @@ export default function LessonUpsellModal({ open, onOpenChange, lesson, tierMap footer={ <> - {canBuy && ( - - )} - } >
- {awaitingStarterSet && ( -
- -

- Complete your plan's starter content to unlock individual purchases like this one. -

-
- )} - {rank > 0 && (
diff --git a/src/modules/client/components/LockedContentPanel.jsx b/src/modules/client/components/LockedContentPanel.jsx index ab4a0a6..1c85147 100644 --- a/src/modules/client/components/LockedContentPanel.jsx +++ b/src/modules/client/components/LockedContentPanel.jsx @@ -4,25 +4,20 @@ // renders in place of the page body itself. Shared by UnitDetails and // LessonDetails. // -// `item` (optional) is the unit/lesson's own subscription/product info, from -// the 403 body's `item` field — lets a deep-link show the same own-tier -// badge + direct "Buy" option the browse-list upsell modals already do, -// instead of only ever pointing at an attached course. +// `item` (optional) is the unit/lesson's own subscription info, from the 403 +// body's `item` field — lets a deep-link show the same own-tier badge the +// browse-list upsell modals already do, instead of only ever pointing at an +// attached course. import { useNavigate } from "react-router-dom"; -import { LockIcon, Zap, ShoppingCart, GraduationCap } from "lucide-react"; +import { LockIcon, Zap } from "lucide-react"; import { Button } from "@/components/ui/button"; -import { useDateFormat } from "@/hooks/useDateFormat"; import { resolveTierBadge } from "@/utils/tierBadge.util"; -export default function LockedContentPanel({ course, item, tierMap = {}, checkoutPath }) { +export default function LockedContentPanel({ course, item, tierMap = {} }) { const navigate = useNavigate(); - const { fmtCurrency } = useDateFormat(); const tier = course?.subscription ? tierMap[course.subscription] : null; const ownTier = item?.subscription ? resolveTierBadge(item.subscription, tierMap) : null; - const purchasable = item?.product?.is_active && !item?.has_purchased && checkoutPath; - const awaitingStarterSet = purchasable && item?.purchase_eligible === false; - const canBuy = purchasable && !awaitingStarterSet; return (
@@ -33,27 +28,14 @@ export default function LockedContentPanel({ course, item, tierMap = {}, checkou

Premium / Exclusive Content

{ownTier - ? `This content requires the ${ownTier.label} plan${canBuy ? ", or you can purchase it individually" : ""}.` + ? `This content requires the ${ownTier.label} plan.` : course ? `This content is part of "${course.title}"${tier?.name ? ` (${tier.name} plan)` : ""}. Upgrade your plan or view the course to unlock it.` : "Upgrade your plan to access this content."}

- {awaitingStarterSet && ( -
- -

- Complete your plan's starter content to unlock individual purchases like this one. -

-
- )}
- {canBuy && ( - - )} diff --git a/src/modules/client/components/UnitUpsellModal.jsx b/src/modules/client/components/UnitUpsellModal.jsx index ed543cf..3914340 100644 --- a/src/modules/client/components/UnitUpsellModal.jsx +++ b/src/modules/client/components/UnitUpsellModal.jsx @@ -1,25 +1,17 @@ // UnitUpsellModal — shown when a learner clicks a locked standalone Unit. -// Unlocks via any course it's attached to (links to view/buy the course), with -// an optional direct "Buy" listing if the unit itself has an active product -// (same PayPal flow as Courses). +// Unlocks via any course it's attached to (links to view/buy the course). // Shared by UnitsList, UnitDetails, and Dashboard. import { useNavigate } from "react-router-dom"; -import { LockIcon, ShoppingCart, GraduationCap, Check } from "lucide-react"; +import { LockIcon, Check } from "lucide-react"; import ResponsiveModal from "@/components/generic/ResponsiveModal"; import { Badge } from "@/components/ui/badge"; import { Button } from "@/components/ui/button"; -import { useDateFormat } from "@/hooks/useDateFormat"; import { resolveTierBadge, cheapestTierSlug } from "@/utils/tierBadge.util"; export default function UnitUpsellModal({ open, onOpenChange, unit, tierMap = {} }) { const navigate = useNavigate(); - const { fmtCurrency } = useDateFormat(); const courses = unit?.courses ?? []; - const purchasable = unit?.product?.is_active && !unit?.has_purchased; - // purchase_eligible is undefined for callers that haven't fetched it yet — treat as eligible (no regression). - const awaitingStarterSet = purchasable && unit?.purchase_eligible === false; - const canBuy = purchasable && !awaitingStarterSet; const slug = cheapestTierSlug([unit?.subscription, ...courses.map((c) => c.subscription)], tierMap); const { rank, label, cls, panel } = resolveTierBadge(slug, tierMap); @@ -33,27 +25,13 @@ export default function UnitUpsellModal({ open, onOpenChange, unit, tierMap = {} footer={ <> - {canBuy && ( - - )} - } >
- {awaitingStarterSet && ( -
- -

- Complete your plan's starter content to unlock individual purchases like this one. -

-
- )} - {rank > 0 && (
diff --git a/src/modules/client/layout/ClientLayout.jsx b/src/modules/client/layout/ClientLayout.jsx index 1aed1f4..c0eb802 100644 --- a/src/modules/client/layout/ClientLayout.jsx +++ b/src/modules/client/layout/ClientLayout.jsx @@ -31,6 +31,7 @@ import { useProfile } from "@/contexts/ProfileProvider" import { useClientTiers } from "@/contexts/ClientTiersProvider" import { resolveTierBadge } from "@/utils/tierBadge.util" import { useGroup } from "@/contexts/ClientGroupContext" +import { resolveAssetSrc } from "@/utils/media.util" import { useEffect, useRef, useState } from "react" import { useDateFormat } from "@/hooks/useDateFormat" import { AVATAR_COLORS } from "@/data/profile.data" @@ -216,7 +217,8 @@ function ClientNav() { const given = user?.personal_info?.name?.given_name ?? "" const last = user?.personal_info?.name?.last_name ?? "" const fullName = given && last ? `${given} ${last}` : (user?.email ?? "") - const avatarUrl = user?.personal_info?.avatar?.url ?? user?.personal_info?.avatar ?? "" + const avatarUrl = resolveAssetSrc(user?.personal_info?.avatar) + ?? (typeof user?.personal_info?.avatar === 'string' ? user.personal_info.avatar : "") const email = user?.email ?? "" const avatarColor = AVATAR_COLORS[user?.acc_type] ?? 'bg-muted text-muted-foreground' diff --git a/src/modules/client/pages/EditProfile.jsx b/src/modules/client/pages/EditProfile.jsx index ad69ba0..5216ffa 100644 --- a/src/modules/client/pages/EditProfile.jsx +++ b/src/modules/client/pages/EditProfile.jsx @@ -16,6 +16,7 @@ import { import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"; import { Camera, Loader2, Plus, Trash2, ArrowLeft } from "lucide-react"; import AvatarUploadDialog from "@/components/generic/AvatarUploadDialog"; +import { resolveAssetSrc } from "@/utils/media.util"; // ─── Helpers ────────────────────────────────────────────────────────────────── @@ -76,7 +77,7 @@ const EditProfile = () => { setOccupation(pi.occupation ?? ""); setPhones(pi.phone_number?.length ? pi.phone_number : [emptyPhone()]); setAddresses(pi.addresses?.length ? pi.addresses : [emptyAddress()]); - setAvatarPreview(pi.avatar?.url ?? ""); + setAvatarPreview(resolveAssetSrc(pi.avatar) ?? ""); }, [profile]); // ── Phone helpers ────────────────────────────────────────────────────────── diff --git a/src/modules/client/pages/LessonCheckout.jsx b/src/modules/client/pages/LessonCheckout.jsx deleted file mode 100644 index c69ea76..0000000 --- a/src/modules/client/pages/LessonCheckout.jsx +++ /dev/null @@ -1,190 +0,0 @@ -import { useNavigate, useParams } from "react-router-dom"; -import { ArrowLeft, FileText, CalendarDays, House, Loader2, ShieldCheck } from "lucide-react"; -import AppBreadcrumb from "@/components/generic/Breadcrumb/AppBreadcrumb"; -import { Button } from "@/components/ui/button"; -import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; -import { Separator } from "@/components/ui/separator"; -import { Skeleton } from "@/components/ui/skeleton"; -import { useLibrary } from "@/contexts/ClientLibraryContext"; -import { useDateFormat } from "@/hooks/useDateFormat"; -import { usePurchaseCheckout } from "@/hooks/usePurchaseCheckout"; - -function formatAccess(days) { - if (!days) return "Lifetime access"; - if (days % 365 === 0) return `${days / 365} year${days / 365 > 1 ? "s" : ""} access`; - if (days % 30 === 0) return `${days / 30} month${days / 30 > 1 ? "s" : ""} access`; - return `${days} day access`; -} - -const PageSkeleton = () => ( -
-
- -
-
-
-
-
-
-); - -export default function LessonCheckout() { - const navigate = useNavigate(); - const { uuid } = useParams(); - const { fmtCurrency } = useDateFormat(); - const { checkoutInfo: lesson, checkoutInfoLoading: lessonLoading, getLessonCheckoutInfo } = useLibrary(); - - const { capturing, purchaseLoading, buyNow } = usePurchaseCheckout({ - targetId: uuid, - loadTarget: getLessonCheckoutInfo, - checkoutPath: `/lessons/${uuid}/checkout`, - }); - - if (capturing) { - return ( -
-
- -

Confirming your payment…

-
-
- ); - } - - if (lessonLoading) return ; - - const product = lesson?.product ?? null; - - if (!product) { - return ( -
-
- - - -
-

Not available for purchase

-

- This lesson doesn't have an individual purchase option. -

-
- -
-
-
-
- ); - } - - const breadcrumbItems = [ - { label: "Home", icon: , to: "/dashboard" }, - { label: "Lessons", to: "/lessons" }, - { label: lesson?.title ?? "Lesson", to: `/lessons/${uuid}` }, - { label: "Checkout" }, - ]; - - return ( -
-
-
- - {/* ── Left ── */} -
- - - - - Lesson Details - Review what you're about to purchase. - - -
-
- -
-
-

{lesson?.title}

- {lesson?.description && ( -

{lesson.description}

- )} -
-
- - - -
-
- - {formatAccess(product.access_days)} -
-
-
-
-
- - {/* ── Right / Summary ── */} -
- - - Price Summary - Payment processed through PayPal. - - -
- Lesson Price - {fmtCurrency(product.price, product.currency)} -
- - - -
- Total - {fmtCurrency(product.price, product.currency)} -
- - {lesson?.has_purchased ? ( - - ) : lesson?.purchase_eligible === false ? ( -
- -

- Complete your plan's starter content to unlock this purchase. -

-
- ) : ( - - )} - -
-

- - Secure payment powered by PayPal -

-

Access starts after successful payment capture.

-
-
-
-
- -
-
-
- ); -} diff --git a/src/modules/client/pages/LessonDetails.jsx b/src/modules/client/pages/LessonDetails.jsx index 35c0df6..7f2d780 100644 --- a/src/modules/client/pages/LessonDetails.jsx +++ b/src/modules/client/pages/LessonDetails.jsx @@ -183,7 +183,6 @@ const LessonDetails = () => { course={unitBlockedInfo?.course} item={unitBlockedInfo?.item} tierMap={tierMap} - checkoutPath={unitBlockedInfo?.item?.uuid ? `/lessons/${unitBlockedInfo.item.uuid}/checkout` : null} /> ); } diff --git a/src/modules/client/pages/PlanList.jsx b/src/modules/client/pages/PlanList.jsx index a1fa14c..b9b6803 100644 --- a/src/modules/client/pages/PlanList.jsx +++ b/src/modules/client/pages/PlanList.jsx @@ -107,148 +107,9 @@ const PlanCard = ({ plan, myTier, tierMap, onSelect, onView, onRefund, refundSec return ( <> - onView(plan)} - > - -
- {plan.label} -
- {isCurrent && ( - Current Plan - )} - {isBlockedByOverlap && ( - - - Already Included in Your Plan - - )} - - - {tierLabel} - -
-
- {plan.description && ( -

{plan.description}

- )} - - - {fmtCurrency(plan.price, plan.currency)} - - {duration && ( - / {duration} - )} - -
- - - - {features.length > 0 && ( -
-

- What's included -

-
    - {features.slice(0, 4).map((f, i) => ( -
  • - - {f.text} -
  • - ))} -
-
- )} - - {bundle ? ( -
-

- - Bundle -

-
    - {previewItems.map((item) => ( -
  • - -
    - {item.title} -
    - {item.level && ( - {item.level} - )} - {formatCourseDuration(item.duration_seconds) && ( - - - {formatCourseDuration(item.duration_seconds)} - - )} -
    -
    -
  • - ))} -
- {extraCount > 0 && ( - setCoursesOpen(true)} - // DIALOG DISABLED - variant="secondary" - - > - +{extraCount} more {bundle.noun.toLowerCase()}{extraCount !== 1 ? "s" : ""} - - )} -
- ) : !plan.description ? ( -

- Access to all free course content. -

- ) : null} -
- - {hasFooterAction && ( - <> - - - {isCurrent && refundSecsLeft > 0 ? ( - - ) : !plan.is_active ? ( - - ) : isBlockedByOverlap ? ( - - ) : plan.tier !== "free" ? ( - // Already holds this tier (from another plan, past the refund - // window) — repurchasing extends the existing grant's expiry - // rather than being blocked, so still offer a way to buy. - - ) : null} - - - )} -
+
+ askodasodkas +
{/* ── Not Available Dialog ──────────────────────────────────────── */} diff --git a/src/modules/client/pages/UnitCheckout.jsx b/src/modules/client/pages/UnitCheckout.jsx deleted file mode 100644 index 09c6235..0000000 --- a/src/modules/client/pages/UnitCheckout.jsx +++ /dev/null @@ -1,190 +0,0 @@ -import { useNavigate, useParams } from "react-router-dom"; -import { ArrowLeft, BookCheck, CalendarDays, House, Loader2, ShieldCheck } from "lucide-react"; -import AppBreadcrumb from "@/components/generic/Breadcrumb/AppBreadcrumb"; -import { Button } from "@/components/ui/button"; -import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; -import { Separator } from "@/components/ui/separator"; -import { Skeleton } from "@/components/ui/skeleton"; -import { useLibrary } from "@/contexts/ClientLibraryContext"; -import { useDateFormat } from "@/hooks/useDateFormat"; -import { usePurchaseCheckout } from "@/hooks/usePurchaseCheckout"; - -function formatAccess(days) { - if (!days) return "Lifetime access"; - if (days % 365 === 0) return `${days / 365} year${days / 365 > 1 ? "s" : ""} access`; - if (days % 30 === 0) return `${days / 30} month${days / 30 > 1 ? "s" : ""} access`; - return `${days} day access`; -} - -const PageSkeleton = () => ( -
-
- -
-
-
-
-
-
-); - -export default function UnitCheckout() { - const navigate = useNavigate(); - const { uuid } = useParams(); - const { fmtCurrency } = useDateFormat(); - const { checkoutInfo: unit, checkoutInfoLoading: unitLoading, getUnitCheckoutInfo } = useLibrary(); - - const { capturing, purchaseLoading, buyNow } = usePurchaseCheckout({ - targetId: uuid, - loadTarget: getUnitCheckoutInfo, - checkoutPath: `/units/${uuid}/checkout`, - }); - - if (capturing) { - return ( -
-
- -

Confirming your payment…

-
-
- ); - } - - if (unitLoading) return ; - - const product = unit?.product ?? null; - - if (!product) { - return ( -
-
- - - -
-

Not available for purchase

-

- This unit doesn't have an individual purchase option. -

-
- -
-
-
-
- ); - } - - const breadcrumbItems = [ - { label: "Home", icon: , to: "/dashboard" }, - { label: "Units", to: "/units" }, - { label: unit?.title ?? "Unit", to: `/units/${uuid}` }, - { label: "Checkout" }, - ]; - - return ( -
-
-
- - {/* ── Left ── */} -
- - - - - Unit Details - Review what you're about to purchase. - - -
-
- -
-
-

{unit?.title}

- {unit?.description && ( -

{unit.description}

- )} -
-
- - - -
-
- - {formatAccess(product.access_days)} -
-
-
-
-
- - {/* ── Right / Summary ── */} -
- - - Price Summary - Payment processed through PayPal. - - -
- Unit Price - {fmtCurrency(product.price, product.currency)} -
- - - -
- Total - {fmtCurrency(product.price, product.currency)} -
- - {unit?.has_purchased ? ( - - ) : unit?.purchase_eligible === false ? ( -
- -

- Complete your plan's starter content to unlock this purchase. -

-
- ) : ( - - )} - -
-

- - Secure payment powered by PayPal -

-

Access starts after successful payment capture.

-
-
-
-
- -
-
-
- ); -} diff --git a/src/modules/client/pages/UnitDetails.jsx b/src/modules/client/pages/UnitDetails.jsx index 2cac3c3..03d0a54 100644 --- a/src/modules/client/pages/UnitDetails.jsx +++ b/src/modules/client/pages/UnitDetails.jsx @@ -145,7 +145,6 @@ const UnitDetails = () => { course={unitBlockedInfo?.course} item={unitBlockedInfo?.item} tierMap={tierMap} - checkoutPath={unitBlockedInfo?.item?.uuid ? `/units/${unitBlockedInfo.item.uuid}/checkout` : null} /> ); } diff --git a/src/modules/client/routes/ClientRoutes.jsx b/src/modules/client/routes/ClientRoutes.jsx index b65629f..15bcb62 100644 --- a/src/modules/client/routes/ClientRoutes.jsx +++ b/src/modules/client/routes/ClientRoutes.jsx @@ -21,8 +21,6 @@ import PlanList from '../pages/PlanList' import ViewPlan from '../pages/ViewPlan' import ViewTask from '../pages/ViewTask' import CourseCheckout from '../pages/CourseCheckout' -import UnitCheckout from '../pages/UnitCheckout' -import LessonCheckout from '../pages/LessonCheckout' import MyCertificates from '../pages/MyCertificates' import MyAchievements from '../pages/MyAchievements' import MyCompletedContent from '../pages/MyCompletedContent' @@ -108,7 +106,6 @@ export const ClientRoutes = { children: [ { index: true, element: }, { path: 'read', element: , handle: { showFooter: false } }, - { path: 'checkout', element: }, ], }, ], @@ -123,7 +120,6 @@ export const ClientRoutes = { path: ':uuid', element: , children: [ { index: true, element: }, - { path: 'checkout', element: }, ], }, ],