feat: add creators, dates and overall ux improvements

This commit is contained in:
swve 2025-05-25 22:05:30 +02:00
parent 988534a42c
commit a73b6ae57e
3 changed files with 103 additions and 6 deletions

View file

@ -5,7 +5,7 @@ import { BookOpenCheck, Check, CheckCircle, ChevronDown, ChevronLeft, ChevronRig
import { markActivityAsComplete, unmarkActivityAsComplete } from '@services/courses/activity' import { markActivityAsComplete, unmarkActivityAsComplete } from '@services/courses/activity'
import { usePathname, useRouter } from 'next/navigation' import { usePathname, useRouter } from 'next/navigation'
import AuthenticatedClientElement from '@components/Security/AuthenticatedClientElement' import AuthenticatedClientElement from '@components/Security/AuthenticatedClientElement'
import { getCourseThumbnailMediaDirectory } from '@services/media/media' import { getCourseThumbnailMediaDirectory, getUserAvatarMediaDirectory } from '@services/media/media'
import { useOrg } from '@components/Contexts/OrgContext' import { useOrg } from '@components/Contexts/OrgContext'
import { CourseProvider } from '@components/Contexts/CourseContext' import { CourseProvider } from '@components/Contexts/CourseContext'
import { useLHSession } from '@components/Contexts/LHSessionContext' import { useLHSession } from '@components/Contexts/LHSessionContext'
@ -31,6 +31,7 @@ import MiniInfoTooltip from '@components/Objects/MiniInfoTooltip'
import GeneralWrapperStyled from '@components/Objects/StyledElements/Wrappers/GeneralWrapper' import GeneralWrapperStyled from '@components/Objects/StyledElements/Wrappers/GeneralWrapper'
import ActivityIndicators from '@components/Pages/Courses/ActivityIndicators' import ActivityIndicators from '@components/Pages/Courses/ActivityIndicators'
import { revalidateTags } from '@services/utils/ts/requests' import { revalidateTags } from '@services/utils/ts/requests'
import UserAvatar from '@components/Objects/UserAvatar'
// Lazy load heavy components // Lazy load heavy components
const Canva = lazy(() => import('@components/Objects/Activities/DynamicCanva/DynamicCanva')) const Canva = lazy(() => import('@components/Objects/Activities/DynamicCanva/DynamicCanva'))
@ -132,6 +133,26 @@ function ActivityActions({ activity, activityid, course, orgslug, assignment, sh
); );
} }
function getRelativeTime(date: Date): string {
const now = new Date();
const diff = now.getTime() - date.getTime();
const seconds = Math.floor(diff / 1000);
const minutes = Math.floor(seconds / 60);
const hours = Math.floor(minutes / 60);
const days = Math.floor(hours / 24);
const weeks = Math.floor(days / 7);
const months = Math.floor(days / 30);
const years = Math.floor(days / 365);
if (years > 0) return `${years} year${years > 1 ? 's' : ''} ago`;
if (months > 0) return `${months} month${months > 1 ? 's' : ''} ago`;
if (weeks > 0) return `${weeks} week${weeks > 1 ? 's' : ''} ago`;
if (days > 0) return `${days} day${days > 1 ? 's' : ''} ago`;
if (hours > 0) return `${hours} hour${hours > 1 ? 's' : ''} ago`;
if (minutes > 0) return `${minutes} minute${minutes > 1 ? 's' : ''} ago`;
return 'just now';
}
function ActivityClient(props: ActivityClientProps) { function ActivityClient(props: ActivityClientProps) {
const activityid = props.activityid const activityid = props.activityid
const courseuuid = props.courseuuid const courseuuid = props.courseuuid
@ -508,7 +529,7 @@ function ActivityClient(props: ActivityClientProps) {
</div> </div>
<div className="flex flex-col -space-y-1"> <div className="flex flex-col -space-y-1">
<p className="font-bold text-gray-700 text-md">Course </p> <p className="font-bold text-gray-700 text-md">Course </p>
<h1 className="font-bold text-gray-950 text-2xl first-letter:uppercase"> <h1 className="font-bold text-gray-950 text-3xl first-letter:uppercase">
{course.name} {course.name}
</h1> </h1>
</div> </div>
@ -550,6 +571,80 @@ function ActivityClient(props: ActivityClientProps) {
<h1 className="font-bold text-gray-950 text-2xl first-letter:uppercase"> <h1 className="font-bold text-gray-950 text-2xl first-letter:uppercase">
{activity.name} {activity.name}
</h1> </h1>
{/* Authors and Dates Section */}
<div className="flex flex-wrap items-center gap-3 mt-2">
{/* Avatars */}
{course.authors && course.authors.length > 0 && (
<div className="flex -space-x-3">
{course.authors.filter((a: any) => a.authorship_status === 'ACTIVE').slice(0, 3).map((author: any, idx: number) => (
<div key={author.user.user_uuid} className="relative z-[${10-idx}]">
<UserAvatar
border="border-2"
rounded="rounded-full"
avatar_url={author.user.avatar_image ? getUserAvatarMediaDirectory(author.user.user_uuid, author.user.avatar_image) : ''}
predefined_avatar={author.user.avatar_image ? undefined : 'empty'}
width={26}
showProfilePopup={true}
userId={author.user.id}
/>
</div>
))}
{course.authors.filter((a: any) => a.authorship_status === 'ACTIVE').length > 3 && (
<div className="flex items-center justify-center bg-neutral-100 text-neutral-600 font-medium rounded-full border-2 border-white shadow-sm w-9 h-9 text-xs z-0">
+{course.authors.filter((a: any) => a.authorship_status === 'ACTIVE').length - 3}
</div>
)}
</div>
)}
{/* Author names */}
{course.authors && course.authors.length > 0 && (
<div className="text-xs text-gray-700 font-medium flex items-center gap-1">
{course.authors.filter((a: any) => a.authorship_status === 'ACTIVE').length > 1 && (
<span>Co-created by </span>
)}
{course.authors.filter((a: any) => a.authorship_status === 'ACTIVE').slice(0, 2).map((author: any, idx: number, arr: any[]) => (
<span key={author.user.user_uuid}>
{author.user.first_name && author.user.last_name
? `${author.user.first_name} ${author.user.last_name}`
: `@${author.user.username}`}
{idx === 0 && arr.length > 1 ? ' & ' : ''}
</span>
))}
{course.authors.filter((a: any) => a.authorship_status === 'ACTIVE').length > 2 && (
<ToolTip
content={
<div className="p-2">
{course.authors
.filter((a: any) => a.authorship_status === 'ACTIVE')
.slice(2)
.map((author: any) => (
<div key={author.user.user_uuid} className="text-white text-sm py-1">
{author.user.first_name && author.user.last_name
? `${author.user.first_name} ${author.user.last_name}`
: `@${author.user.username}`}
</div>
))}
</div>
}
>
<div className="bg-gray-100 hover:bg-gray-200 text-gray-600 px-2 py-0.5 rounded-md cursor-pointer text-xs font-medium transition-colors duration-200">
+{course.authors.filter((a: any) => a.authorship_status === 'ACTIVE').length - 2}
</div>
</ToolTip>
)}
</div>
)}
{/* Dates */}
<div className="flex items-center text-xs text-gray-500 gap-2">
<span>
Created on {new Date(course.creation_date).toLocaleDateString(undefined, { year: 'numeric', month: 'long', day: 'numeric' })}
</span>
<span className="mx-1"></span>
<span>
Last updated {getRelativeTime(new Date(course.updated_at || course.last_updated || course.creation_date))}
</span>
</div>
</div>
</div> </div>
</div> </div>
<div className="flex space-x-2 items-center"> <div className="flex space-x-2 items-center">
@ -598,7 +693,7 @@ function ActivityClient(props: ActivityClientProps) {
<div className={`p-7 drop-shadow-xs rounded-lg ${bgColor} relative`}> <div className={`p-7 drop-shadow-xs rounded-lg ${bgColor} relative`}>
<button <button
onClick={() => setIsFocusMode(true)} onClick={() => setIsFocusMode(true)}
className="absolute top-4 right-4 bg-white/80 hover:bg-white nice-shadow p-2 rounded-full cursor-pointer transition-all duration-200 group overflow-hidden" className="absolute top-4 right-4 bg-white/80 hover:bg-white nice-shadow p-2 rounded-full cursor-pointer transition-all duration-200 group overflow-hidden z-50 pointer-events-auto"
title="Enter focus mode" title="Enter focus mode"
> >
<div className="flex items-center"> <div className="flex items-center">

View file

@ -77,7 +77,7 @@ export default function ActivityChapterDropdown(props: ActivityChapterDropdownPr
</button> </button>
{isOpen && ( {isOpen && (
<div className={`absolute z-50 mt-2 ${isMobile ? 'left-0 w-[90vw] sm:w-72' : 'left-0 w-72'} max-h-[70vh] cursor-pointer overflow-y-auto bg-white rounded-lg shadow-xl border border-gray-200 py-1 animate-in fade-in duration-200`}> <div className={`absolute z-50 mt-2 ${isMobile ? 'right-0 w-[90vw] sm:w-72' : 'right-0 w-72'} max-h-[70vh] cursor-pointer overflow-y-auto bg-white rounded-lg shadow-xl border border-gray-200 py-1 animate-in fade-in duration-200`}>
<div className="px-3 py-1.5 border-b border-gray-100 flex justify-between items-center"> <div className="px-3 py-1.5 border-b border-gray-100 flex justify-between items-center">
<h3 className="text-sm font-semibold text-gray-800">Course Content</h3> <h3 className="text-sm font-semibold text-gray-800">Course Content</h3>
<button <button

View file

@ -4,6 +4,7 @@ import React, { useMemo, memo, useState } from 'react'
import ToolTip from '@components/Objects/StyledElements/Tooltip/Tooltip' import ToolTip from '@components/Objects/StyledElements/Tooltip/Tooltip'
import { getUriWithOrg } from '@services/config/config' import { getUriWithOrg } from '@services/config/config'
import Link from 'next/link' import Link from 'next/link'
import { useRouter } from 'next/navigation'
interface Props { interface Props {
course: any course: any
@ -100,6 +101,7 @@ function ActivityIndicators(props: Props) {
const orgslug = props.orgslug const orgslug = props.orgslug
const courseid = props.course_uuid.replace('course_', '') const courseid = props.course_uuid.replace('course_', '')
const enableNavigation = props.enableNavigation || false const enableNavigation = props.enableNavigation || false
const router = useRouter()
const [currentIndex, setCurrentIndex] = useState(0) const [currentIndex, setCurrentIndex] = useState(0)
@ -161,7 +163,7 @@ function ActivityIndicators(props: Props) {
if (currentActivityIndex > 0) { if (currentActivityIndex > 0) {
const prevActivity = allActivities[currentActivityIndex - 1] const prevActivity = allActivities[currentActivityIndex - 1]
const activityId = prevActivity.activity_uuid.replace('activity_', '') const activityId = prevActivity.activity_uuid.replace('activity_', '')
window.location.href = getUriWithOrg(orgslug, '') + `/course/${courseid}/activity/${activityId}` router.push(getUriWithOrg(orgslug, '') + `/course/${courseid}/activity/${activityId}`)
} }
} }
@ -169,7 +171,7 @@ function ActivityIndicators(props: Props) {
if (currentActivityIndex < allActivities.length - 1) { if (currentActivityIndex < allActivities.length - 1) {
const nextActivity = allActivities[currentActivityIndex + 1] const nextActivity = allActivities[currentActivityIndex + 1]
const activityId = nextActivity.activity_uuid.replace('activity_', '') const activityId = nextActivity.activity_uuid.replace('activity_', '')
window.location.href = getUriWithOrg(orgslug, '') + `/course/${courseid}/activity/${activityId}` router.push(getUriWithOrg(orgslug, '') + `/course/${courseid}/activity/${activityId}`)
} }
} }