mirror of
https://github.com/perfect-panel/ppanel-web.git
synced 2026-02-06 11:40:28 -05:00
Merge pull request #11 from turbolnk-com/develop
♻️ refactor(ui): Optimize document display
This commit is contained in:
commit
c2b858e3fe
35
apps/user/app/(main)/(user)/document/close-icon.tsx
Normal file
35
apps/user/app/(main)/(user)/document/close-icon.tsx
Normal 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>
|
||||
);
|
||||
};
|
||||
@ -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>
|
||||
);
|
||||
};
|
||||
}
|
||||
@ -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>
|
||||
);
|
||||
};
|
||||
}
|
||||
@ -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) => {
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -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[]> {
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user