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 { useTranslations } from 'next-intl';
import { RefObject, useEffect, useId, useRef, useState } from 'react';
import { CloseIcon } from './close-icon';
export function DocumentButton({ items }: { items: API.Document[] }) {
const t = useTranslations('document');
@ -78,7 +79,7 @@ export function DocumentButton({ items }: { items: API.Document[] }) {
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)}
>
<CloseIcon />
@ -104,7 +105,7 @@ export function DocumentButton({ items }: { items: API.Document[] }) {
<div className='flex flex-row items-center gap-4'>
<motion.div layoutId={`image-${item.id}-${id}`}>
<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>
</motion.div>
<div className=''>
@ -135,37 +136,4 @@ export function DocumentButton({ items }: { items: API.Document[] }) {
</ul>
</>
);
}
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 { 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 { Markdown } from '@workspace/ui/custom-components/markdown';
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 { useTranslations } from 'next-intl';
import { RefObject, useEffect, useId, useRef, useState } from 'react';
import { CloseIcon } from './close-icon';
import { formatDate } from '@workspace/ui/utils';
interface Item {
path: string;
title: string;
updated_at?: string;
icon?: string;
}
export function TutorialButton({ items }: { items: Item[] }) {
const t = useTranslations('document');
@ -80,7 +84,7 @@ export function TutorialButton({ items }: { items: Item[] }) {
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)}
>
<CloseIcon />
@ -105,7 +109,7 @@ export function TutorialButton({ items }: { items: Item[] }) {
},
}}
>
{data || ''}
{data?.content || ''}
</Markdown>
</motion.div>
</div>
@ -122,19 +126,22 @@ export function TutorialButton({ items }: { items: Item[] }) {
<div className='flex flex-row items-center gap-4'>
<motion.div layoutId={`image-${item.title}-${id}`}>
<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>
</motion.div>
<div className=''>
<motion.h3 layoutId={`title-${item.title}-${id}`} className='font-medium'>
{item.title}
</motion.h3>
{/* <motion.p
layoutId={`description-${item.title}-${id}`}
className='text-center text-neutral-600 md:text-left dark:text-neutral-400'
>
{formatDate(item.updated_at)}
</motion.p> */}
{item.updated_at && (
<motion.p
layoutId={`description-${item.title}-${id}`}
className='text-center text-neutral-600 md:text-left dark:text-neutral-400'
>
{formatDate(new Date(item.updated_at), false)}
</motion.p>
)}
</div>
</div>
<motion.button
@ -153,37 +160,4 @@ export function TutorialButton({ items }: { items: Item[] }) {
</ul>
</>
);
}
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() {
const locale = useLocale();
const country = getCountry(locale);
const t = useTranslations('language');
const router = useRouter();
const handleLanguageChange = (value: string) => {

View File

@ -18,6 +18,7 @@
"ahooks": "^3.8.4",
"axios": "^1.7.9",
"framer-motion": "^11.16.1",
"gray-matter": "^4.0.3",
"lucide-react": "^0.469.0",
"next": "^15.1.4",
"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';
async function getVersion() {
// API rate limit: 60 requests per hour
const response = await fetch(
'https://api.github.com/repos/perfect-panel/ppanel-tutorial/commits',
);
@ -8,17 +11,33 @@ async function getVersion() {
return json[0].sha;
}
export async function getTutorial(path: string): Promise<string> {
const version = await getVersion();
async function getVersionPath() {
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 {
const url = `${BASE_URL}@${version}/${path}`;
const url = `${versionPath}/${path}`;
const response = await fetch(url);
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const text = await response.text();
const markdown = addPrefixToImageUrls(text, getUrlPrefix(url));
return markdown;
const { data, content } = matter(text);
const markdown = addPrefixToImageUrls(content, getUrlPrefix(url));
return {
config: data,
content: markdown,
};
} catch (error) {
console.error('Error fetching the markdown file:', error);
throw error;
@ -31,17 +50,27 @@ type 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() {
return await getTutorial('SUMMARY.md').then((markdown) => {
const map = parseTutorialToMap(markdown);
map.forEach((value, key) => {
map.set(
key,
value.filter((item) => item.title !== 'README'),
);
const { config, content } = await getTutorial('SUMMARY.md');
const navigation = config as Record<string, TutorialItem[]> | undefined;
if (!navigation) {
return parseTutorialToMap(content);
}
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[]> {