Skip to content

Commit b364e75

Browse files
committed
refactor(app): roadmap detail view
1 parent 90a03fb commit b364e75

File tree

8 files changed

+219
-144
lines changed

8 files changed

+219
-144
lines changed

src/app/learn/[type]/List.js

Lines changed: 20 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -16,20 +16,33 @@
1616

1717
'use client';
1818

19+
import clsx from 'clsx';
20+
1921
import ChallengeListView from '#/domain/challenge/views/challenge-list';
2022
import CourseListView from '#/domain/course/views/course-list';
2123
import RoadmapListView from '#/domain/roadmap/views/roadmap-list';
2224
import { useOpenFilter } from '#/state/application/hooks';
2325

24-
const viewWidgetMap = {
25-
courses: CourseListView,
26-
challenges: ChallengeListView,
27-
career_path: RoadmapListView,
26+
const viewMap = {
27+
courses: {
28+
widget: CourseListView,
29+
className: 'mb-9 mt-6 gap-5 md:grid-cols-3',
30+
},
31+
challenges: {
32+
widget: ChallengeListView,
33+
},
34+
career_path: {
35+
widget: RoadmapListView,
36+
},
2837
};
2938

3039
export function List({ type, data }) {
3140
const openFilter = useOpenFilter();
32-
const ListViewWidget = viewWidgetMap[type];
41+
const view = viewMap[type];
42+
43+
if (!view) {
44+
return null;
45+
}
3346

3447
const otherClassNames = {
3548
'lg:grid-cols-2': openFilter,
@@ -39,12 +52,11 @@ export function List({ type, data }) {
3952
'3xl:grid-cols-4': openFilter,
4053
'3xl:grid-cols-5': !openFilter,
4154
};
55+
const ListViewWidget = view.widget;
4256

4357
return (
4458
<div>
45-
{ListViewWidget && (
46-
<ListViewWidget className={otherClassNames} data={data.list} total={data.count} />
47-
)}
59+
<ListViewWidget className={clsx(view.className, otherClassNames)} data={data.list} total={data.count} />
4860
</div>
4961
);
5062
}

src/app/learn/[type]/[id]/GrowPath.js

Lines changed: 9 additions & 129 deletions
Original file line numberDiff line numberDiff line change
@@ -15,145 +15,25 @@
1515
*/
1616

1717
'use client';
18-
import clsx from 'clsx';
19-
import { useSession } from 'next-auth/react';
20-
import Image from 'next/image';
21-
import { usePathname, useRouter } from 'next/navigation';
22-
import { useState } from 'react';
23-
import { toast } from 'react-toastify';
2418

25-
import { Button } from '@/components/Button';
26-
import { ClockIcon } from '@/components/icon/outlined';
27-
import { CheckCircleIcon } from '@/components/icon/solid';
2819
import { Share } from '@/components/Share';
29-
import { formatTime } from '@/utils/date';
3020

31-
import { useMediaUrl } from '#/state/application/hooks';
21+
import RoadmapDetailView from '#/domain/roadmap/views/roadmap-detail';
3222

33-
import { CourseCard } from '../CourseCard';
3423
import { growPathEnrollAction } from './actions';
3524
import { Back } from './Back';
36-
import { Title } from './Summary';
3725

38-
function Steps({ data, permission, id }) {
39-
// console.log(data)
40-
// console.log(permission)
41-
const mediaUrl = useMediaUrl();
42-
const { status } = useSession();
43-
const router = useRouter();
44-
const pathname = usePathname();
45-
const [loading, setLoading] = useState(false);
46-
47-
const enroll = async id => {
48-
if (status !== 'authenticated') {
49-
router.push(`/signin?from=${pathname}`);
50-
}
51-
setLoading(true);
52-
const res = await growPathEnrollAction(id);
53-
if (res) {
54-
toast.error(res.message);
55-
}
56-
setLoading(false);
57-
};
58-
59-
return data && data.length > 0 ? (
26+
function GrowPath({ params, data, permission }) {
27+
const headerExtra = (
6028
<>
61-
{data.map((i, k) =>
62-
<div key={`GrowPath-Steps-${i.step}`} className={clsx('border-gray-600 mb-9 pb-9', { 'border-b': i.step < data.length })}>
63-
<h2 className="text-base mb-1 flex items-center">
64-
{i.step !==1 && !data[k-1]?.is_finish && <svg className="mr-2 relative top-[-1px]" width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
65-
<g clipPath="url(#clip0_1069_4201)">
66-
<path d="M10.9375 5.25001H10.6094C10.5492 5.25001 10.5 5.2008 10.5 5.14064V3.52736C10.5 1.59553 8.91952 -0.00681413 6.98632 2.1792e-05C5.05996 0.00685772 3.5 1.57092 3.5 3.50002V5.14064C3.5 5.20079 3.45078 5.25001 3.39062 5.25001H3.0625C2.33789 5.25001 1.75 5.8379 1.75 6.56251V12.6875C1.75 13.4121 2.33789 14 3.0625 14H10.9375C11.6621 14 12.25 13.4121 12.25 12.6875V6.56251C12.25 5.8379 11.6621 5.25001 10.9375 5.25001ZM7.78886 10.2361C7.57011 10.4016 7.43749 10.6559 7.43749 10.9307V11.8125C7.43749 12.0518 7.24335 12.2473 7.00546 12.25C6.7621 12.2527 6.56249 12.0477 6.56249 11.8029V10.9375C6.56249 10.6613 6.43124 10.4029 6.20976 10.2361C5.89257 9.99688 5.68749 9.61544 5.68749 9.18751C5.68749 8.47384 6.26718 7.88458 6.98085 7.87501C7.71503 7.86407 8.31249 8.45606 8.31249 9.18751C8.31249 9.61544 8.10741 9.99688 7.78886 10.2361ZM9.62499 5.14064C9.62499 5.20079 9.57577 5.25001 9.51561 5.25001H4.48437C4.42421 5.25001 4.375 5.2008 4.375 5.14064V3.50002C4.375 2.79865 4.64843 2.13967 5.14335 1.64338C5.63964 1.14846 6.29863 0.87502 6.99999 0.87502C7.70136 0.87502 8.36034 1.14846 8.85663 1.64338C9.35155 2.13967 9.62499 2.79865 9.62499 3.50002V5.14064Z" fill="#1A1A1A"/>
67-
<path d="M6.5625 9.1875C6.5625 9.30353 6.60859 9.41481 6.69064 9.49686C6.77269 9.5789 6.88397 9.625 7 9.625C7.11603 9.625 7.22731 9.5789 7.30936 9.49686C7.3914 9.41481 7.4375 9.30353 7.4375 9.1875C7.4375 9.07147 7.3914 8.96019 7.30936 8.87814C7.22731 8.79609 7.11603 8.75 7 8.75C6.88397 8.75 6.77269 8.79609 6.69064 8.87814C6.60859 8.96019 6.5625 9.07147 6.5625 9.1875Z" fill="#1A1A1A"/>
68-
</g>
69-
<defs>
70-
<clipPath id="clip0_1069_4201">
71-
<rect width="14" height="14" fill="white"/>
72-
</clipPath>
73-
</defs>
74-
</svg>}
75-
76-
{i.step === 1 ? 'Start' : `Step ${i.step}`}
77-
</h2>
78-
<div className="flex justify-between items-center">
79-
<h2 className="text-2xl mb-1 flex items-center">{i.title}</h2>
80-
{i.step === 1 &&
81-
(
82-
permission.course_user_permission_status === 0 ?
83-
<Button loading={loading} className="w-[196px] !font-bold" onClick={() => enroll(i.gp_id)}>Enroll now</Button> :
84-
<Button className="w-[196px] !font-bold bg-[rgba(26,26,26,0.06)] border-0 text-gray hover:bg-[rgba(26,26,26,0.06)]">
85-
<CheckCircleIcon className="h-4 w-4" />
86-
Completed
87-
</Button>
88-
)
89-
}
90-
</div>
91-
<p className="max-w-[672px] mt-2">{i.info}</p>
92-
{
93-
i.step === 1 && i.extra_data?.length > 0 && <div className={'mt-6 grid gap-4 grid-cols-3'}>
94-
{i.extra_data.map(j => <CourseCard data={j} target={'_blank'} from={`from=career_path&fromid=${id}`} key={`GrowPath-Steps-card-${j.base.course_series_id}`} />)}
95-
</div>
96-
}
97-
{i.img !== '' && <div className="mt-4">
98-
<Image
99-
width={222}
100-
height={222}
101-
src={mediaUrl + i.img}
102-
alt=""
103-
className="h-[222px] w-[222px] rounded-lg object-cover"
104-
/>
105-
</div>
106-
}
107-
</div>
108-
)}
29+
<Back params={params} />
30+
<Share img={data?.img} title={data?.title} type={params.type} id={params.id} />
10931
</>
110-
) : null;
111-
}
32+
);
11233

113-
export function GrowPath({params, data, permission}) {
114-
const mediaUrl = useMediaUrl();
11534
return (
116-
<div className="">
117-
<div className="w-full bg-black text-white">
118-
<div className="mx-auto p-6 max-w-[1080px]">
119-
<div className="flex justify-between">
120-
<Back params={params} />
121-
<Share img={data?.img} title={data?.title} type={params.type} id={params.id} />
122-
</div>
123-
<Title data={data} type={'GrowPath'} />
124-
<div className="flex items-center">
125-
<div className="flex items-center">
126-
<ClockIcon className="h-[18px] w-[18px] mr-2 text-base" />
127-
{formatTime(data?.created_at * 1000)}
128-
</div>
129-
<span className="opacity-40"></span>
130-
<div>
131-
<div className="flex items-center justify-between py-6 text-base">
132-
<div suppressHydrationWarning className="flex [&>img]:ml-[-8px] [&>img]:rounded-full [&>img]:border [&>img]:border-white [&>img:first-child]:ml-0">
133-
{mediaUrl &&
134-
data?.enrool_users?.slice(0, 10)
135-
.map(i => (
136-
<Image
137-
key={`courses-enrool-users-${i.user_nick_name}`}
138-
width={24}
139-
height={24}
140-
src={mediaUrl + i.user_avatar}
141-
alt=""
142-
className="h-6 w-6 object-cover"
143-
/>
144-
))}
145-
{data?.enrool_users?.length > 10 && <span className="ml-[-8px] w-6 h-6 inline-block rounded-full bg-white text-center leading-4">...</span>}
146-
</div>
147-
<p className="ml-2">{data?.builder_num} Builders</p>
148-
</div>
149-
</div>
150-
</div>
151-
<p>{data.info}</p>
152-
</div>
153-
</div>
154-
<div className="mx-auto p-6 max-w-[1080px]">
155-
<Steps data={data?.step_list} permission={permission} id={params.id} />
156-
</div>
157-
</div>
35+
<RoadmapDetailView id={params.id} data={data} permission={permission} headerExtra={headerExtra} executeEnroll={growPathEnrollAction} />
15836
);
15937
}
38+
39+
export default GrowPath;

src/app/learn/[type]/[id]/page.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ import { Chapters } from '../../Chapters';
2222
import { Speaker } from '../../Speaker';
2323
import { Author } from './Author';
2424
import { Back } from './Back';
25-
import { GrowPath } from './GrowPath';
25+
import GrowPath from './GrowPath';
2626
import { LearnInfo } from './LearnInfo';
2727
import { LearnRightCard } from './RightCard';
2828
import { Summary, Title } from './Summary';

src/domain/course/views/course-list/CourseItem.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ import CourseProgress from './CourseProgress';
3030

3131
const typeStyle = 'inline-block mb-1 text-xs border border-gray-600 rounded-md leading-[14px] rounded-md px-2 py-1 opacity-60 mr-1';
3232

33-
function CourseItem({ data, target, from }) {
33+
function CourseItem({ data, from }) {
3434
const configs = useConfig();
3535
const tags = useMemo(() => {
3636
const _filters = configs && configs.find(f => f.config_id === 1);
@@ -71,7 +71,7 @@ function CourseItem({ data, target, from }) {
7171

7272
return (
7373
<Link
74-
target={target ? target : '_self'}
74+
target={from ? '_blank' : '_self'}
7575
href={`/learn/courses/${data.base.course_series_id}${from ? ('?'+from) : ''}`}
7676
className="flex overflow-hidden flex-col group relative cursor-pointer rounded-2xl bg-white shadow-lg transition-shadow hover:shadow-lg md:shadow-none"
7777
>

src/domain/course/views/course-list/CourseList.js

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -17,18 +17,21 @@
1717
import clsx from 'clsx';
1818

1919
import { OPagination } from '@/components/Pagination';
20+
import { isInteger } from '@/utils';
2021

2122
import CourseItem from './CourseItem';
2223

23-
function CourseListView({ className, data = [], total = 0 }) {
24+
function CourseListView({ className, data = [], total, source }) {
2425
return (
2526
<>
2627
<div
27-
className={clsx('mb-9 mt-6 grid gap-5 md:grid-cols-3', className)}
28+
className={clsx('grid', className)}
2829
>
29-
{data.map(item => <CourseItem data={item} key={`course-${item.base.course_series_id}`} />)}
30+
{data.map(item => (
31+
<CourseItem key={`course-${item.base.course_series_id}`} data={item} from={source} />
32+
))}
3033
</div>
31-
<OPagination total={total} />
34+
{isInteger(total) && <OPagination total={total} />}
3235
</>
3336
);
3437
}
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
/**
2+
* Copyright 2024 OpenBuild
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
import ContentEditable from 'react-contenteditable';
18+
19+
import { ClockIcon } from '@/components/icon/outlined';
20+
import { HTMLDecode } from '@/utils';
21+
import { formatTime } from '@/utils/date';
22+
23+
import { BuilderListWidget } from '../../../course';
24+
import StepList from './StepList';
25+
26+
function RoadmapDetailView({ data, permission, headerExtra, executeEnroll }) {
27+
return (
28+
<div className="">
29+
<div className="w-full bg-black text-white">
30+
<div className="mx-auto p-6 max-w-[1080px]">
31+
{headerExtra && <div className="flex justify-between">{headerExtra}</div>}
32+
<h3 className="my-6 text-[36px] font-bold leading-9 md:text-[36px] md:leading-[48px]">
33+
<ContentEditable html={HTMLDecode(data?.title)} disabled />
34+
</h3>
35+
<div className="flex items-center">
36+
<div className="flex items-center">
37+
<ClockIcon className="h-[18px] w-[18px] mr-2 text-base" />
38+
{formatTime(data?.created_at * 1000)}
39+
</div>
40+
<span className="opacity-40"></span>
41+
<BuilderListWidget
42+
className="justify-between py-6 !text-base"
43+
dataSource={data?.enrool_users}
44+
total={data?.builder_num}
45+
dataKeys={{ id: 'user_nick_name', avatar: 'user_avatar' }}
46+
>
47+
<p className="ml-2">{data?.builder_num} Builders</p>
48+
</BuilderListWidget>
49+
</div>
50+
<p>{data.info}</p>
51+
</div>
52+
</div>
53+
<div className="mx-auto p-6 max-w-[1080px]">
54+
<StepList data={data?.step_list} permission={permission} roadmapId={data.id} executeEnroll={executeEnroll} />
55+
</div>
56+
</div>
57+
);
58+
}
59+
60+
export default RoadmapDetailView;

0 commit comments

Comments
 (0)