Merge pull request #11 from turbolnk-com/develop

♻️ refactor(ui): Optimize document display
This commit is contained in:
web@ppanel 2025-02-25 18:35:05 +07:00 committed by GitHub
commit c2b858e3fe
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 101 additions and 95 deletions

View File

@ -0,0 +1,35 @@
import { cn } from "@workspace/ui/lib/utils";
import { motion } from "framer-motion";
export const CloseIcon = ({ className }: { className?: string }) => {
return (
<motion.svg
initial={{
opacity: 0,
}}
animate={{
opacity: 1,
}}
exit={{
opacity: 0,
transition: {
duration: 0.05,
},
}}
xmlns='http://www.w3.org/2000/svg'
width='24'
height='24'
viewBox='0 0 24 24'
fill='none'
stroke='currentColor'
strokeWidth='2'
strokeLinecap='round'
strokeLinejoin='round'
className={cn('h-4 w-4', className)}
>
<path stroke='none' d='M0 0h24v24H0z' fill='none' />
<path d='M18 6l-12 12' />
<path d='M6 6l12 12' />
</motion.svg>
);
};

View File

@ -11,6 +11,7 @@ import { formatDate } from '@workspace/ui/utils';
import { AnimatePresence, motion } from 'framer-motion'; import { AnimatePresence, motion } from 'framer-motion';
import { useTranslations } from 'next-intl'; import { useTranslations } from 'next-intl';
import { RefObject, useEffect, useId, useRef, useState } from 'react'; import { RefObject, useEffect, useId, useRef, useState } from 'react';
import { CloseIcon } from './close-icon';
export function DocumentButton({ items }: { items: API.Document[] }) { export function DocumentButton({ items }: { items: API.Document[] }) {
const t = useTranslations('document'); const t = useTranslations('document');
@ -78,7 +79,7 @@ export function DocumentButton({ items }: { items: API.Document[] }) {
duration: 0.05, duration: 0.05,
}, },
}} }}
className='bg-foreground absolute right-2 top-2 flex h-6 w-6 items-center justify-center rounded-full' className='bg-foreground absolute right-2 top-2 flex h-6 w-6 items-center justify-center rounded-full text-white dark:text-black'
onClick={() => setActive(null)} onClick={() => setActive(null)}
> >
<CloseIcon /> <CloseIcon />
@ -104,7 +105,7 @@ export function DocumentButton({ items }: { items: API.Document[] }) {
<div className='flex flex-row items-center gap-4'> <div className='flex flex-row items-center gap-4'>
<motion.div layoutId={`image-${item.id}-${id}`}> <motion.div layoutId={`image-${item.id}-${id}`}>
<Avatar className='size-12'> <Avatar className='size-12'>
<AvatarFallback className='bg-primary'>{item.title.split('')[0]}</AvatarFallback> <AvatarFallback className='bg-primary/80 text-white'>{item.title.split('')[0]}</AvatarFallback>
</Avatar> </Avatar>
</motion.div> </motion.div>
<div className=''> <div className=''>
@ -136,36 +137,3 @@ export function DocumentButton({ items }: { items: API.Document[] }) {
</> </>
); );
} }
export const CloseIcon = () => {
return (
<motion.svg
initial={{
opacity: 0,
}}
animate={{
opacity: 1,
}}
exit={{
opacity: 0,
transition: {
duration: 0.05,
},
}}
xmlns='http://www.w3.org/2000/svg'
width='24'
height='24'
viewBox='0 0 24 24'
fill='none'
stroke='currentColor'
strokeWidth='2'
strokeLinecap='round'
strokeLinejoin='round'
className='h-4 w-4 text-black'
>
<path stroke='none' d='M0 0h24v24H0z' fill='none' />
<path d='M18 6l-12 12' />
<path d='M6 6l12 12' />
</motion.svg>
);
};

View File

@ -2,7 +2,7 @@
import { getTutorial } from '@/utils/tutorial'; import { getTutorial } from '@/utils/tutorial';
import { useQuery } from '@tanstack/react-query'; import { useQuery } from '@tanstack/react-query';
import { Avatar, AvatarFallback } from '@workspace/ui/components/avatar'; import { Avatar, AvatarFallback, AvatarImage } from '@workspace/ui/components/avatar';
import { buttonVariants } from '@workspace/ui/components/button'; import { buttonVariants } from '@workspace/ui/components/button';
import { Markdown } from '@workspace/ui/custom-components/markdown'; import { Markdown } from '@workspace/ui/custom-components/markdown';
import { useOutsideClick } from '@workspace/ui/hooks/use-outside-click'; import { useOutsideClick } from '@workspace/ui/hooks/use-outside-click';
@ -10,10 +10,14 @@ import { cn } from '@workspace/ui/lib/utils';
import { AnimatePresence, motion } from 'framer-motion'; import { AnimatePresence, motion } from 'framer-motion';
import { useTranslations } from 'next-intl'; import { useTranslations } from 'next-intl';
import { RefObject, useEffect, useId, useRef, useState } from 'react'; import { RefObject, useEffect, useId, useRef, useState } from 'react';
import { CloseIcon } from './close-icon';
import { formatDate } from '@workspace/ui/utils';
interface Item { interface Item {
path: string; path: string;
title: string; title: string;
updated_at?: string;
icon?: string;
} }
export function TutorialButton({ items }: { items: Item[] }) { export function TutorialButton({ items }: { items: Item[] }) {
const t = useTranslations('document'); const t = useTranslations('document');
@ -80,7 +84,7 @@ export function TutorialButton({ items }: { items: Item[] }) {
duration: 0.05, duration: 0.05,
}, },
}} }}
className='bg-foreground absolute right-2 top-2 flex h-6 w-6 items-center justify-center rounded-full' className='bg-foreground absolute right-2 top-2 flex h-6 w-6 items-center justify-center rounded-full text-white dark:text-black'
onClick={() => setActive(null)} onClick={() => setActive(null)}
> >
<CloseIcon /> <CloseIcon />
@ -105,7 +109,7 @@ export function TutorialButton({ items }: { items: Item[] }) {
}, },
}} }}
> >
{data || ''} {data?.content || ''}
</Markdown> </Markdown>
</motion.div> </motion.div>
</div> </div>
@ -122,19 +126,22 @@ export function TutorialButton({ items }: { items: Item[] }) {
<div className='flex flex-row items-center gap-4'> <div className='flex flex-row items-center gap-4'>
<motion.div layoutId={`image-${item.title}-${id}`}> <motion.div layoutId={`image-${item.title}-${id}`}>
<Avatar className='size-12'> <Avatar className='size-12'>
<AvatarFallback className='bg-primary'>{item.title.split('')[0]}</AvatarFallback> <AvatarImage alt={item.title ?? ''} src={item.icon ?? ''} />
<AvatarFallback className='bg-primary/80 text-white'>{item.title.split('')[0]}</AvatarFallback>
</Avatar> </Avatar>
</motion.div> </motion.div>
<div className=''> <div className=''>
<motion.h3 layoutId={`title-${item.title}-${id}`} className='font-medium'> <motion.h3 layoutId={`title-${item.title}-${id}`} className='font-medium'>
{item.title} {item.title}
</motion.h3> </motion.h3>
{/* <motion.p {item.updated_at && (
layoutId={`description-${item.title}-${id}`} <motion.p
className='text-center text-neutral-600 md:text-left dark:text-neutral-400' layoutId={`description-${item.title}-${id}`}
> className='text-center text-neutral-600 md:text-left dark:text-neutral-400'
{formatDate(item.updated_at)} >
</motion.p> */} {formatDate(new Date(item.updated_at), false)}
</motion.p>
)}
</div> </div>
</div> </div>
<motion.button <motion.button
@ -154,36 +161,3 @@ export function TutorialButton({ items }: { items: Item[] }) {
</> </>
); );
} }
export const CloseIcon = () => {
return (
<motion.svg
initial={{
opacity: 0,
}}
animate={{
opacity: 1,
}}
exit={{
opacity: 0,
transition: {
duration: 0.05,
},
}}
xmlns='http://www.w3.org/2000/svg'
width='24'
height='24'
viewBox='0 0 24 24'
fill='none'
stroke='currentColor'
strokeWidth='2'
strokeLinecap='round'
strokeLinejoin='round'
className='h-4 w-4 text-black'
>
<path stroke='none' d='M0 0h24v24H0z' fill='none' />
<path d='M18 6l-12 12' />
<path d='M6 6l12 12' />
</motion.svg>
);
};

View File

@ -43,7 +43,6 @@ const languages = {
export default function LanguageSwitch() { export default function LanguageSwitch() {
const locale = useLocale(); const locale = useLocale();
const country = getCountry(locale); const country = getCountry(locale);
const t = useTranslations('language');
const router = useRouter(); const router = useRouter();
const handleLanguageChange = (value: string) => { const handleLanguageChange = (value: string) => {

View File

@ -18,6 +18,7 @@
"ahooks": "^3.8.4", "ahooks": "^3.8.4",
"axios": "^1.7.9", "axios": "^1.7.9",
"framer-motion": "^11.16.1", "framer-motion": "^11.16.1",
"gray-matter": "^4.0.3",
"lucide-react": "^0.469.0", "lucide-react": "^0.469.0",
"next": "^15.1.4", "next": "^15.1.4",
"next-intl": "^3.26.3", "next-intl": "^3.26.3",

View File

@ -1,6 +1,9 @@
import matter from 'gray-matter';
const BASE_URL = 'https://cdn.jsdelivr.net/gh/perfect-panel/ppanel-tutorial'; const BASE_URL = 'https://cdn.jsdelivr.net/gh/perfect-panel/ppanel-tutorial';
async function getVersion() { async function getVersion() {
// API rate limit: 60 requests per hour
const response = await fetch( const response = await fetch(
'https://api.github.com/repos/perfect-panel/ppanel-tutorial/commits', 'https://api.github.com/repos/perfect-panel/ppanel-tutorial/commits',
); );
@ -8,17 +11,33 @@ async function getVersion() {
return json[0].sha; return json[0].sha;
} }
export async function getTutorial(path: string): Promise<string> { async function getVersionPath() {
const version = await getVersion(); return getVersion()
.then(version => `${BASE_URL}@${version}`)
.catch(error => {
console.warn('Error fetching the version:', error);
return BASE_URL;
});
}
export async function getTutorial(path: string): Promise<{
config?: Record<string, unknown>;
content: string;
}> {
const versionPath = await getVersionPath();
try { try {
const url = `${BASE_URL}@${version}/${path}`; const url = `${versionPath}/${path}`;
const response = await fetch(url); const response = await fetch(url);
if (!response.ok) { if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`); throw new Error(`HTTP error! status: ${response.status}`);
} }
const text = await response.text(); const text = await response.text();
const markdown = addPrefixToImageUrls(text, getUrlPrefix(url)); const { data, content } = matter(text);
return markdown; const markdown = addPrefixToImageUrls(content, getUrlPrefix(url));
return {
config: data,
content: markdown,
};
} catch (error) { } catch (error) {
console.error('Error fetching the markdown file:', error); console.error('Error fetching the markdown file:', error);
throw error; throw error;
@ -31,17 +50,27 @@ type TutorialItem = {
subItems?: TutorialItem[]; subItems?: TutorialItem[];
}; };
const processIcon = (item: TutorialItem) => {
if ("icon" in item && typeof item.icon === 'string' && !item.icon.startsWith('http')) {
item.icon = `${BASE_URL}/${item.icon}`;
}
};
export async function getTutorialList() { export async function getTutorialList() {
return await getTutorial('SUMMARY.md').then((markdown) => { const { config, content } = await getTutorial('SUMMARY.md');
const map = parseTutorialToMap(markdown); const navigation = config as Record<string, TutorialItem[]> | undefined;
map.forEach((value, key) => {
map.set( if (!navigation) {
key, return parseTutorialToMap(content);
value.filter((item) => item.title !== 'README'), }
);
Object.values(navigation)
.flat()
.forEach(item => {
item.subItems?.forEach(processIcon);
}); });
return map;
}); return new Map(Object.entries(navigation));
} }
function parseTutorialToMap(markdown: string): Map<string, TutorialItem[]> { function parseTutorialToMap(markdown: string): Map<string, TutorialItem[]> {