Skip to content
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

Enhance Quiz Rankings and UI Optimization & Fix Challenge Dialog Flickering Issue #87

Merged
merged 8 commits into from
Jan 24, 2025
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,7 @@
"sha256": "^0.2.0",
"siwe": "^2.1.4",
"slick-carousel": "^1.8.1",
"striptags": "3.2.0",
"survey-analytics": "^1.9.109",
"survey-core": "^1.9.90",
"survey-creator-react": "^1.9.90",
Expand Down
4 changes: 3 additions & 1 deletion src/app/quiz/List.js
Original file line number Diff line number Diff line change
Expand Up @@ -30,10 +30,12 @@ import { fetchTeamList } from '#/domain/quiz/repository';
import { ReactSelect } from '@/components/Select/ReactSelect';
import { SearchIcon } from '@/components/Icons';
import Input from '@/components/Input';
import { markdownToPlainText } from '@/utils/markdown';
import useMounted from '@/hooks/useMounted';

function List({ data }) {
const mediaUrl = useMediaUrl();

return (
<Link href={`/quiz/${data.id}`} className="p-6 bg-white flex max-md:flex-col gap-4 md:gap-9 mb-4 rounded-xl transition-all hover:shadow-[0_4px_24px_rgba(0,0,0,0.08)]">
<div className="relative">
Expand All @@ -43,7 +45,7 @@ function List({ data }) {
<div className="flex flex-col justify-between flex-1">
<div>
<h3 className="text-2xl mb-2">{data?.title}</h3>
<p className="text-base md:mb-2 opacity-60 md:line-clamp-2">{data?.describe}</p>
<p className="text-base md:mb-2 opacity-60 md:line-clamp-2">{markdownToPlainText(data?.describe)}</p>
{data?.reward_text && <div className="flex w-fit pr-2 items-center h-6 bg-[rgba(239,78,22,0.1)] rounded-full max-md:mt-4">
<div className="w-6 h-6 rounded-full flex items-center justify-center bg-[#EF4E16] mr-2">
<Image width={16} height={16} src={TrophiesSvg} alt="Trophies" />
Expand Down
72 changes: 72 additions & 0 deletions src/app/quiz/[id]/RankList.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
/**
* Copyright 2024 OpenBuild
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

'use client';

import Image from 'next/image';
import Rank1Icon from 'public/images/svg/rank-1.svg';
import Rank2Icon from 'public/images/svg/rank-2.svg';
import Rank3Icon from 'public/images/svg/rank-3.svg';
import { useMediaUrl } from '#/state/application/hooks';

export default function RankList({ rank, list }) {
const mediaUrl = useMediaUrl();

return (
<div className="flex flex-col h-full">
Copy link
Member

Choose a reason for hiding this comment

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

这层 div 多余的吧?

<div className="border border-gray-600 rounded flex flex-col h-full overflow-hidden">
<h6 className="h-12 bg-gray-1000 text-center leading-[48px] relative flex-shrink-0">
Quiz Scoreboard
{rank > 0 && (
<p className="absolute right-6 top-[14px] text-sm font-normal">
<span className="opacity-60">My ranking: </span>
{rank}
</p>
)}
</h6>
<ul className="p-4 overflow-y-auto flex-grow">
{list?.map((i, k) => (
<li key={`QuizScoreboard-${k}`} className="flex items-center justify-between mb-4 last:mb-0">
<div className="flex items-center">
{k === 0 && <Image alt="" src={Rank1Icon} className="mr-2 w-5" />}
{k === 1 && <Image alt="" src={Rank2Icon} className="mr-2 w-5" />}
{k === 2 && <Image alt="" src={Rank3Icon} className="mr-2 w-5" />}
{k > 2 && <span className="inline-block w-5 text-center mr-2 text-xs opacity-40">{k + 1}</span>}
{mediaUrl && i?.user?.user_avatar ? (
<Image
className="h-6 w-6 rounded object-cover mr-2"
height={24}
width={24}
alt={'user_avatar'}
src={`${mediaUrl}${i.user.user_avatar}`}
/>
) : (
<span className="h-6 w-6 rounded-full bg-gray-200 mr-2 flex items-center justify-center text-xs">
{i?.user?.user_nick_name?.[0]?.toUpperCase() || 'U'}
</span>
)}
<p className="text-[12px] max-md:leading-[20px] md:text-sm">
<a href={`/u/${i?.user?.user_handle}`}>{i?.user?.user_nick_name}</a>
</p>
</div>
<p className="max-md:text-[12px] max-md:leading-[24px]">{i?.score}</p>
</li>
))}
</ul>
</div>
</div>
);
}
52 changes: 52 additions & 0 deletions src/app/quiz/[id]/RankListModal.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
/**
* Copyright 2024 OpenBuild
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

import { Modal } from '@/components/Modal';
import RankList from './RankList';
import { useState } from 'react';
import { fetchRankList } from '#/domain/quiz/repository';
import Loader from '@/components/Loader';
import { ModalCloseIcon } from '@/components/Icons';
import useMounted from '@/hooks/useMounted';

export function RankListModal({ quizId, shown, onClose, rank }) {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);

useMounted(() => {
setLoading(true);
fetchRankList({ quizId })
.then(res => {
setData(res?.data?.list?.rank);
})
.finally(() => {
setLoading(false);
});
});

return (
<Modal isOpen={shown} closeModal={onClose} container mode="640">
<ModalCloseIcon
onClick={onClose}
className="absolute top-[-48px] md:top-[-32px] right-0 md:right-[-32px] cursor-pointer"
/>
<div className="md:h-[600px] h-[400px] flex flex-col overflow-y-auto rounded-inherit overflow-hidden">
{loading && <Loader classname="mr-2" />}
<RankList rank={rank} list={data} />
</div>
</Modal>
);
}
10 changes: 5 additions & 5 deletions src/app/quiz/[id]/Record.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,14 +25,14 @@ import { formatTime, fromUtcOffset, formatTimeMeridiem } from '@/utils/date';
import { NoData } from '@/components/NoData';
import Link from 'next/link';

export function Record({id, openModal, closeModal}) {
export function Record({quizId, shown, onClose}) {
const mediaUrl = useMediaUrl();
const { data } = useSWR(openModal ? `/ts/v1/quiz/${id}/answer` : null, fetcher);
const { data } = useSWR(shown ? `/ts/v1/quiz/${quizId}/answer` : null, fetcher);

return (
<Modal isOpen={openModal} closeModal={closeModal} container mode="640">
<Modal isOpen={shown} onClose={onClose} container mode="640">
<div >
<ModalCloseIcon onClick={closeModal} className="absolute top-[-48px] md:top-[-32px] right-0 md:right-[-32px] cursor-pointer" />
<ModalCloseIcon onClick={onClose} className="absolute top-[-48px] md:top-[-32px] right-0 md:right-[-32px] cursor-pointer" />
<div>
<h3 className="text-center py-4 border-b border-gray-600">
Challenge Record
Expand Down Expand Up @@ -85,7 +85,7 @@ export function Record({id, openModal, closeModal}) {
))}
</div>
</div>
{data?.length === 0 && <div className="pb-12"><NoData /></div>}
{(!data|| data?.length === 0) && <div className="pb-12"><NoData /></div>}
</div>
</div>
</Modal>
Expand Down
68 changes: 20 additions & 48 deletions src/app/quiz/[id]/page.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,9 +22,6 @@
import { ArrowUturnLeftIcon } from '@heroicons/react/24/solid';
import { HistoryIcon } from '@/components/Icons';
import { Button } from '@/components/Button';
import Rank1Icon from 'public/images/svg/rank-1.svg';
import Rank2Icon from 'public/images/svg/rank-2.svg';
import Rank3Icon from 'public/images/svg/rank-3.svg';
import { useState } from 'react';
import useSWR from 'swr';
import { fetcher } from '@/utils/request';
Expand All @@ -33,10 +30,14 @@
import { useRouter } from 'next/navigation';
import { Record } from './Record';
import QuizLimiterWidget from '#/domain/quiz/widgets/quiz-limiter';
import RankList from './RankList';
import { RankListModal } from './RankListModal';
import { OViewer } from '@/components/MarkDown';

export default function Quiz({params}) {
const mediaUrl = useMediaUrl();
const [openModal, setOpenModal] = useState(false);
const [openChallenge, setOpenChallenge] = useState(false);
const [openRankList, setOpenRankList] = useState(false);
const [checkLimit, setCheckLimit] = useState(false);
const { data } = useSWR(`/ts/v1/quiz/${params.id}/index`, fetcher);
const { data: coursesList } = useSWR(`v1/learn/course/opencourse?skip=0&take=2&order=default&quiz_bind_id=${params.id}`, fetcher);
Expand All @@ -62,7 +63,7 @@
if (status !== 'authenticated') {
router.push(`/signin?from=/quiz/${params.id}`);
} else {
setOpenModal(true);
setOpenChallenge(true);
}
}} className="cursor-pointer transition-all flex text-sm items-center opacity-80 rounded py-2 px-3 border border-gray-1100 text-black hover:border-gray">
<HistoryIcon className="mr-2" />Challenge Record
Expand All @@ -84,55 +85,26 @@
</div>
<div className="max-w-[800px] mx-auto bg-white rounded-xl p-6 md:px-9 md:pt-10 md:pb-6 relative z-[2] md:top-[-155px]">
<h5 className="text-lg mb-4 md:mb-3">Quiz Describe</h5>
<p dangerouslySetInnerHTML={{__html: data?.describe.replace('\n', '<br>')}}>
{/* {data?.describe} */}
</p>
<OViewer value={data?.describe} />
<Button
onClick={() => setCheckLimit(true)}
className="mt-4 md:mt-6 mb-9 md:mb-10 !font-bold px-[64px] !text-base max-md:w-full">
Challenge now
</Button>
<div>
<div className="border border-gray-600 rounded">
<h6 className="h-12 bg-gray-1000 text-center leading-[48px] rounded-t relative">
Quiz Scoreboard
{data?.my_rank > 0 && <p className="absolute right-6 top-[14px] text-sm font-normal">
<span className="opacity-60">My ranking: </span>
{data?.my_rank}
</p>}
</h6>
<ul className="p-4">
{data?.rank?.map((i, k) => (
<li key={`QuizScoreboard-${k}`} className="flex items-center justify-between mb-4 last:mb-0">
<div className="flex items-center">
{k === 0 && <Image alt="" src={Rank1Icon} className="mr-2 w-5" />}
{k === 1 && <Image alt="" src={Rank2Icon} className="mr-2 w-5" />}
{k === 2 && <Image alt="" src={Rank3Icon} className="mr-2 w-5" />}
{k > 2 && <span className="inline-block w-5 text-center mr-2 text-xs opacity-40">{k + 1}</span>}
<Image
className="h-6 w-6 rounded object-cover mr-2"
height={24}
width={24}
alt={'user_avatar'}
src={mediaUrl + i?.user?.user_avatar}
/>
<p className="text-[12px] max-md:leading-[20px] md:text-sm"><a href={`/u/${i?.user?.user_handle}`}>{i?.user?.user_nick_name}</a></p>
</div>
<p className="max-md:text-[12px] max-md:leading-[24px]">{i.score}</p>
</li>
))}
</ul>
</div>
<p className="text-sm text-center mt-6"><strong>{data?.user_num}</strong> builders have participated</p>
</div>
</div>
<div className="max-w-[800px] max-md:mt-9 mx-6 md:mx-auto relative md:top-[-105px] max-md:pb-14">
<h3 className="text-[18px] max-md:leading-[24px] md:text-lg mb-6">Related courses</h3>
<div className="grid gap-y-6 md:gap-4 md:grid-cols-2">
{coursesList?.list?.map(i => <CourseCard data={i} key={`open-courses-${i.base.course_series_id}`} />)}
</div>
<RankList rank={data?.my_rank} list={data?.rank}/>
<p className="text-sm text-center mt-6 cursor-pointer" onClick={()=>{setOpenRankList(true)}}><strong>{data?.user_num}</strong> builders have participated</p>

Check warning on line 95 in src/app/quiz/[id]/page.js

View workflow job for this annotation

GitHub Actions / lint

Missing semicolon
</div>
<Record id={params.id} openModal={openModal} closeModal={() => setOpenModal(false)} />
{
coursesList?.count > 0 && (
<div className="max-w-[800px] max-md:mt-9 mx-6 md:mx-auto relative md:top-[-105px] max-md:pb-14">
<h3 className="text-[18px] max-md:leading-[24px] md:text-lg mb-6">Related courses</h3>
<div className="grid gap-y-6 md:gap-4 md:grid-cols-2">
{coursesList?.list?.map(i => <CourseCard data={i} key={`open-courses-${i.base.course_series_id}`} />)}
</div>
</div>)
}
<RankListModal quizId={params.id} shown={openRankList} onClose={() => setOpenRankList(false)} rank={data?.my_rank}/>
<Record quizId={params.id} shown={openChallenge} onClose={() => setOpenChallenge(false)} />
</QuizLimiterWidget>
);
}
6 changes: 5 additions & 1 deletion src/domain/quiz/repository.js
Original file line number Diff line number Diff line change
Expand Up @@ -49,4 +49,8 @@ async function fetchTeamList(){
return httpClient.get('/quiz/team');
}

export { updateRespondentContacts, fetchPublishedQuizList, fetchAnsweredQuizList, fetchAnsweredResult, fetchTeamList };
async function fetchRankList({ quizId }){
return httpClient.get(`/quiz/${quizId}/users`);
}

export { updateRespondentContacts, fetchPublishedQuizList, fetchAnsweredQuizList, fetchAnsweredResult, fetchTeamList, fetchRankList };
10 changes: 9 additions & 1 deletion src/shared/utils/markdown.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import breaks from '@bytemd/plugin-breaks';
import math from '@bytemd/plugin-math';
import mermaid from '@bytemd/plugin-mermaid';
import gemoji from '@bytemd/plugin-gemoji';
import striptags from 'striptags';

const plugins = [gfm(), breaks(), highlight(), math(), mermaid(), gemoji()];

Expand Down Expand Up @@ -53,4 +54,11 @@ function renderHtml(markdownContent) {
return getProcessor({ sanitize, plugins }).processSync(markdownContent).toString();
}

export { getPlugins, sanitize, renderMarkdown, renderHtml };
function markdownToPlainText(markdownContent) {
const html = renderHtml(markdownContent);
const plainText = striptags(html);

return plainText.trim().replace(/[\r\n]+/g, ' ');
}

export { getPlugins, sanitize, renderMarkdown, renderHtml, markdownToPlainText };
Loading