Skip to content

835-fix: Stale data on the website #843

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 24 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 11 commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
224911c
fix: 835 - to evaluate is course stale on the client
Quiddlee Apr 6, 2025
0c156e3
feat: 835 - implement to filter stale mentorship courses
Quiddlee Apr 6, 2025
12dc5c8
refactor: 835 - move mentorship stale calculation to the separate method
Quiddlee Apr 6, 2025
fe5f8c4
feat: 835 - implement to evaluate course availability on the client
Quiddlee Apr 7, 2025
80f6eaa
feat: 835 - implement to disable registration link if the course is s…
Quiddlee Apr 7, 2025
fd466c5
fix: 835 - hero course test
Quiddlee Apr 7, 2025
816f2e2
fix: 835 - registration link label when disabled
Quiddlee Apr 7, 2025
c2fd57a
refactor: 835 - remove unnecessary course sorting
Quiddlee Apr 7, 2025
ab878d2
refactor: 835 - move header all courses to the client component
Quiddlee Apr 7, 2025
2de9ce5
fix: 835 - move all static parts to server component
Quiddlee Apr 7, 2025
6413da6
fix: 835 - move all static parts to server component
Quiddlee Apr 7, 2025
5ffacbb
refactor: 835 - encapsulate course menu items in reusable component
Quiddlee Apr 14, 2025
6dc48b6
refactor: 835 - implement to reuse fresh courses component in as many…
Quiddlee Apr 14, 2025
ec6cd2b
refactor: 835 - replace default export with named export
Quiddlee Apr 14, 2025
01d9171
refactor: 835 - remove variable reassignment
Quiddlee Apr 14, 2025
d90c937
refactor: 835 - add explicit types
Quiddlee Apr 14, 2025
9927125
refactor: 835 - rename variable
Quiddlee Apr 14, 2025
cba7bad
chore: 835 - resolve merge conflicts
Quiddlee Apr 19, 2025
c6d3310
chore: 835 - resolve merge conflicts
Quiddlee May 8, 2025
4719ecb
chore: 835 - resolve merge conflict
Quiddlee May 21, 2025
6e96876
chore: 835 - resolve merge conflicts
Quiddlee May 21, 2025
2a14c70
fix: 835 - to evaluate date on the client
Quiddlee May 21, 2025
6ea440b
fix: 835 - upcoming courses view
Quiddlee May 22, 2025
7e24a3c
fix: 835 - course card normal view
Quiddlee May 22, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 30 additions & 0 deletions src/core/base-layout/components/footer/all-courses.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
'use client';

import type { Course } from '@/entities/course';
import { getActualData } from '@/shared/helpers/getActualData';
import { SchoolMenu } from '@/widgets/school-menu';

type AllCoursesProps = {
courses: Course[];
};

const AllCourses = ({ courses }: AllCoursesProps) => {
const actualCourses = getActualData({
data: courses,
filterStale: false,
sort: false,
});

return actualCourses.map((course) => (
<SchoolMenu.Item
key={course.id}
icon={course.iconFooter}
title={course.title}
description={course.startDate}
url={course.detailsUrl}
color="light"
/>
));
};

export default AllCourses;
Copy link
Collaborator

@ansivgit ansivgit Apr 10, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why do we need to use default export here and in the Header?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

replaced with named export

12 changes: 2 additions & 10 deletions src/core/base-layout/components/footer/desktop-view.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { AboutList } from './about-list';
import AllCourses from '@/core/base-layout/components/footer/all-courses';
import { getCourses } from '@/entities/course/api/course-api';
import { SchoolMenu } from '@/widgets/school-menu';
import { schoolMenuStaticLinks } from 'data';
Expand All @@ -25,16 +26,7 @@ export const DesktopView = async () => {

<div className="right">
<SchoolMenu heading="all courses" color="light">
{courses.map((course) => (
<SchoolMenu.Item
key={course.id}
icon={course.iconFooter}
title={course.title}
description={course.startDate}
url={course.detailsUrl}
color="light"
/>
))}
<AllCourses courses={courses} />
</SchoolMenu>
</div>
</div>
Expand Down
29 changes: 29 additions & 0 deletions src/core/base-layout/components/header/all-courses.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
'use client';

import { Course } from '@/entities/course';
import { getActualData } from '@/shared/helpers/getActualData';
import { SchoolMenu } from '@/widgets/school-menu';

type AllCoursesProps = {
courses: Course[];
};

const AllCourses = ({ courses }: AllCoursesProps) => {
Copy link
Collaborator

@ansivgit ansivgit Apr 10, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As I can see AllCourses component in Header, Footer & mobile-view is almost the same. Could we move it to separate component & reuse it?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

const actualCourses = getActualData({
data: courses,
filterStale: false,
sort: false,
});

return actualCourses.map((course) => (
<SchoolMenu.Item
key={course.id}
icon={course.iconSmall}
title={course.title}
description={course.startDate}
url={course.detailsUrl}
/>
));
};

export default AllCourses;
11 changes: 2 additions & 9 deletions src/core/base-layout/components/header/header.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { usePathname } from 'next/navigation';

import { BurgerMenu } from './burger/burger';
import { NavItem } from './nav-item/nav-item';
import AllCourses from '@/core/base-layout/components/header/all-courses';
import { ANCHORS, ROUTES } from '@/core/const';
import { Course } from '@/entities/course';
import { Logo } from '@/shared/ui/logo';
Expand Down Expand Up @@ -87,15 +88,7 @@ export const Header = ({ courses }: HeaderProps) => {
</NavItem>
<NavItem label="Courses" href={ROUTES.COURSES}>
<SchoolMenu>
{courses.map((course) => (
<SchoolMenu.Item
key={course.id}
icon={course.iconSmall}
title={course.title}
description={course.startDate}
url={course.detailsUrl}
/>
))}
<AllCourses courses={courses} />
</SchoolMenu>
</NavItem>
<NavItem label="Community" href={ROUTES.COMMUNITY}>
Expand Down
55 changes: 52 additions & 3 deletions src/shared/helpers/getActualData.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,18 +11,36 @@ type GetActualDataParams<T extends DataType> = {
data: T;
staleAfter?: number;
filterStale?: boolean;
isMentorship?: boolean;
sort?: boolean;
};

type GetActualDataType = <T extends DataType>(params: GetActualDataParams<T>) => T;

export const getActualData: GetActualDataType = ({ data, staleAfter, filterStale = true }) => {
let dataWithTBD = mapStaleAsTBD(data, staleAfter);
export const getActualData: GetActualDataType = ({
data,
staleAfter,
filterStale = true,
isMentorship = false,
sort = true,
}) => {
let dataWithTBD;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We could avoid variable reassignment. I suggest several changes below. The same in the mapStaleAsTBD function

Suggested change
let dataWithTBD;
let dataWithTBD = [];

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done


if (isMentorship) {
dataWithTBD = mapMentorshipStaleAsTBD(data);
} else {
dataWithTBD = mapStaleAsTBD(data, staleAfter);
}
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
if (isMentorship) {
dataWithTBD = mapMentorshipStaleAsTBD(data);
} else {
dataWithTBD = mapStaleAsTBD(data, staleAfter);
}
if (isMentorship) {
dataWithTBD.push(mapMentorshipStaleAsTBD(data));
} else {
dataWithTBD.push(mapStaleAsTBD(data, staleAfter));
}

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

IMO this refactoring should be made in separate task, since it's require to change the functions inner and types logic


if (filterStale) {
dataWithTBD = filterStaleData(dataWithTBD);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could we avoid variable reassignment?

}

return sortData(dataWithTBD);
if (sort) {
dataWithTBD = sortData(dataWithTBD);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
dataWithTBD = sortData(dataWithTBD);
sortData(dataWithTBD);

}

return dataWithTBD;
};

const mapStaleAsTBD = <T extends DataType>(data: T, staleAfter?: number): T =>
Expand All @@ -45,6 +63,37 @@ const mapStaleAsTBD = <T extends DataType>(data: T, staleAfter?: number): T =>
};
}) as T;

const mapMentorshipStaleAsTBD = <T extends DataType>(data: T): T => {
if ('eventType' in data) {
return data;
}

return (data as Course[]).map((item) => {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I suppose if we assign the code below to a variable and then return it, it will be more readable.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

const date = item.personalMentoringStartDate;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
const date = item.personalMentoringStartDate;
const date: string | null = item.personalMentoringStartDate;

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done


if (!date) {
return item;
}

const daysBeforeStale = dayJS(item.personalMentoringEndDate).diff(
item.personalMentoringStartDate,
'd',
);

const startDate = getCourseDate(date, daysBeforeStale);

if (startDate === TO_BE_DETERMINED) {
return {
...item,
personalMentoringStartDate: null,
personalMentoringEndDate: null,
};
}

return item;
}) as T;
};

const filterStaleData = <T extends DataType>(data: T): T =>
data.filter((item) => {
const date = isCourse(item) ? item.startDate : item.date;
Expand Down
28 changes: 28 additions & 0 deletions src/shared/ui/short-info-panel/course-start-label.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
'use client';

import { PropsWithChildren } from 'react';

import { calculateFreshDate } from '@/shared/helpers/getCourseDate';
import { DateSimple } from '@/shared/ui/date-simple';

type CourseStartLabelProps = PropsWithChildren & {
startDate: string | null;
registrationEndDate: string | null;
label: string | undefined;
};

export const CourseStartLabel = ({
startDate,
registrationEndDate,
label,
children,
}: CourseStartLabelProps) => {
const freshDate =
startDate && registrationEndDate ? calculateFreshDate(startDate, registrationEndDate) : null;

return (
<DateSimple label={label} startDate={freshDate}>
{children}
</DateSimple>
);
};
14 changes: 5 additions & 9 deletions src/shared/ui/short-info-panel/short-info-panel.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,10 @@
import classNames from 'classnames/bind';
import Image from 'next/image';

import { DateSimple } from '../date-simple';
import micIcon from '@/shared/assets/icons/mic.svg';
import { LABELS } from '@/shared/constants';
import { calculateFreshDate } from '@/shared/helpers/getCourseDate';
import { Language } from '@/shared/types';
import { CourseStartLabel } from '@/shared/ui/short-info-panel/course-start-label';

import styles from './short-info-panel.module.scss';

Expand Down Expand Up @@ -34,20 +33,17 @@ export const ShortInfoPanel = ({

return (
<section className={cx('info', { margin: withMargin })}>
<DateSimple
<CourseStartLabel
startDate={startDate}
registrationEndDate={registrationEndDate}
label={label}
startDate={
startDate && registrationEndDate
? calculateFreshDate(startDate, registrationEndDate)
: null
}
>
{onlyLanguage && (
<span className={cx('language')} data-testid="course-language">
{courseLanguage}
</span>
)}
</DateSimple>
</CourseStartLabel>
{!onlyLanguage && (
<p className={cx('additional-info')}>
<Image className={cx('icon')} src={micIcon} alt="microphone-icon" />
Expand Down
30 changes: 30 additions & 0 deletions src/views/mentorship/ui/mentorship-courses/course-items.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
'use client';

import React from 'react';

import { Course, CourseCard } from '@/entities/course';
import { getActualData } from '@/shared/helpers/getActualData';
import {
transformCoursesToMentorship,
} from '@/views/mentorship/helpers/transformCoursesToMentorship';

type CourseItems = {
courses: Course[];
className: string;
};

const CourseItems = ({ courses, className }: CourseItems) => {
const actualCourses = getActualData({
data: courses,
filterStale: false,
isMentorship: true,
sort: false,
});
const coursesWithMentorship = transformCoursesToMentorship(actualCourses);

return coursesWithMentorship.map((course) => (
<CourseCard key={course.id} className={className} {...course} showMentoringStartDate={true} />
));
};

export default CourseItems;
16 changes: 3 additions & 13 deletions src/views/mentorship/ui/mentorship-courses/mentorship-courses.tsx
Original file line number Diff line number Diff line change
@@ -1,32 +1,22 @@
import classNames from 'classnames/bind';

import { CourseCard } from '@/entities/course';
import { getCourses } from '@/entities/course/api/course-api';
import { WidgetTitle } from '@/shared/ui/widget-title';
import {
transformCoursesToMentorship,
} from '@/views/mentorship/helpers/transformCoursesToMentorship';
import CourseItems from '@/views/mentorship/ui/mentorship-courses/course-items';

import styles from './mentorship-courses.module.scss';

const cx = classNames.bind(styles);

export const MentorshipCourses = async () => {
const coursesWithMentorship = transformCoursesToMentorship(await getCourses());
const courses = await getCourses();

return (
<section className={cx('mentorship-courses', 'container')}>
<div className={cx('content')}>
<WidgetTitle>Courses That Need Mentors</WidgetTitle>
<div className={cx('courses-list')}>
{coursesWithMentorship.map((course) => (
<CourseCard
key={course.id}
className={cx('mentorship-course-card')}
{...course}
showMentoringStartDate={true}
/>
))}
<CourseItems courses={courses} className={cx('mentorship-course-card')} />
</div>
</div>
</section>
Expand Down
23 changes: 23 additions & 0 deletions src/widgets/courses/ui/course-items.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
'use client';

import { Course, CourseCard } from '@/entities/course';
import { getActualData } from '@/shared/helpers/getActualData';

type CourseItemsProps = {
courses: Course[];
};

const CourseItems = ({ courses }: CourseItemsProps) => {
const actualCourses = getActualData({
data: courses,
filterStale: false,
});

return (
actualCourses.map((course) =>
<CourseCard size="sm" key={course.id} {...course} />,
)
);
};

export default CourseItems;
14 changes: 2 additions & 12 deletions src/widgets/courses/ui/courses.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,8 @@
import classNames from 'classnames/bind';

import { type Course, CourseCard } from '@/entities/course';
import { getCourses } from '@/entities/course/api/course-api';
import { getActualData } from '@/shared/helpers/getActualData';
import { WidgetTitle } from '@/shared/ui/widget-title';
import CourseItems from '@/widgets/courses/ui/course-items';

import styles from './courses.module.scss';

Expand All @@ -12,21 +11,12 @@ const cx = classNames.bind(styles);
export const Courses = async () => {
const courses = await getCourses();

const sortParams = {
data: courses,
filterStale: false,
};

const sortedCourses: Course[] = getActualData(sortParams);

return (
<section className={cx('container')} data-testid="all-courses">
<div className={cx('content', 'courses-content')}>
<WidgetTitle>All courses</WidgetTitle>
<div className={cx('courses-list')}>
{sortedCourses.map((course) => {
return <CourseCard size="sm" key={course.id} {...course} />;
})}
<CourseItems courses={courses} />
</div>
</div>
</section>
Expand Down
16 changes: 16 additions & 0 deletions src/widgets/hero-course/ui/availability-status.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
'use client';

import { dayJS } from '@/shared/helpers/dayJS';
import { SectionLabel } from '@/shared/ui/section-label';
import { getCourseStatus } from '@/widgets/hero-course/helpers/get-course-status';

type AvailabilityStatusProps = {
startDate: string;
registrationEndDate: string;
};

export const AvailabilityStatus = ({ startDate, registrationEndDate }: AvailabilityStatusProps) => {
const status = getCourseStatus(startDate, dayJS(registrationEndDate).diff(startDate, 'd'));

return <SectionLabel data-testid="course-label">{status}</SectionLabel>;
};
Loading
Loading