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 { 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=''>
|
||||||
@ -135,37 +136,4 @@ export function DocumentButton({ items }: { items: API.Document[] }) {
|
|||||||
</ul>
|
</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 { 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
|
||||||
@ -153,37 +160,4 @@ export function TutorialButton({ items }: { items: Item[] }) {
|
|||||||
</ul>
|
</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() {
|
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) => {
|
||||||
|
|||||||
@ -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",
|
||||||
|
|||||||
@ -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[]> {
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user