mirror of
https://github.com/MoonTechLab/LunaTV.git
synced 2026-02-26 22:24:42 +08:00
fix: refactor
This commit is contained in:
@@ -1,212 +1,7 @@
|
||||
import { NextResponse } from 'next/server';
|
||||
|
||||
import { API_CONFIG, ApiSite, getApiSites, getCacheTime } from '@/lib/config';
|
||||
|
||||
const M3U8_PATTERN = /(https?:\/\/[^"'\s]+?\.m3u8)/g;
|
||||
|
||||
// 清理 HTML 标签的工具函数
|
||||
function cleanHtmlTags(text: string): string {
|
||||
if (!text) return '';
|
||||
return text
|
||||
.replace(/<[^>]+>/g, '\n') // 将 HTML 标签替换为换行
|
||||
.replace(/\n+/g, '\n') // 将多个连续换行合并为一个
|
||||
.replace(/[ \t]+/g, ' ') // 将多个连续空格和制表符合并为一个空格,但保留换行符
|
||||
.replace(/^\n+|\n+$/g, '') // 去掉首尾换行
|
||||
.replace(/ /g, ' ') // 将 替换为空格
|
||||
.trim(); // 去掉首尾空格
|
||||
}
|
||||
|
||||
export interface VideoDetail {
|
||||
code: number;
|
||||
episodes: string[];
|
||||
detailUrl: string;
|
||||
videoInfo: {
|
||||
title: string;
|
||||
cover?: string;
|
||||
desc?: string;
|
||||
type?: string;
|
||||
year?: string;
|
||||
area?: string;
|
||||
director?: string;
|
||||
actor?: string;
|
||||
remarks?: string;
|
||||
source_name: string;
|
||||
source: string;
|
||||
id: string;
|
||||
};
|
||||
}
|
||||
|
||||
async function handleSpecialSourceDetail(
|
||||
id: string,
|
||||
apiSite: ApiSite
|
||||
): Promise<VideoDetail> {
|
||||
const detailUrl = `${apiSite.detail}/index.php/vod/detail/id/${id}.html`;
|
||||
|
||||
const controller = new AbortController();
|
||||
const timeoutId = setTimeout(() => controller.abort(), 10000);
|
||||
|
||||
const response = await fetch(detailUrl, {
|
||||
headers: API_CONFIG.detail.headers,
|
||||
signal: controller.signal,
|
||||
});
|
||||
|
||||
clearTimeout(timeoutId);
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`详情页请求失败: ${response.status}`);
|
||||
}
|
||||
|
||||
const html = await response.text();
|
||||
let matches: string[] = [];
|
||||
|
||||
if (apiSite.key === 'ffzy') {
|
||||
const ffzyPattern =
|
||||
/\$(https?:\/\/[^"'\s]+?\/\d{8}\/\d+_[a-f0-9]+\/index\.m3u8)/g;
|
||||
matches = html.match(ffzyPattern) || [];
|
||||
}
|
||||
|
||||
if (matches.length === 0) {
|
||||
const generalPattern = /\$(https?:\/\/[^"'\s]+?\.m3u8)/g;
|
||||
matches = html.match(generalPattern) || [];
|
||||
}
|
||||
|
||||
matches = Array.from(new Set(matches));
|
||||
matches = matches.map((link: string) => {
|
||||
link = link.substring(1, link.length);
|
||||
const parenIndex = link.indexOf('(');
|
||||
return parenIndex > 0 ? link.substring(0, parenIndex) : link;
|
||||
});
|
||||
|
||||
const titleMatch = html.match(/<h1[^>]*>([^<]+)<\/h1>/);
|
||||
const titleText = titleMatch ? titleMatch[1].trim() : '';
|
||||
|
||||
const descMatch = html.match(
|
||||
/<div[^>]*class=["']sketch["'][^>]*>([\s\S]*?)<\/div>/
|
||||
);
|
||||
const descText = descMatch ? cleanHtmlTags(descMatch[1]) : '';
|
||||
|
||||
const coverMatch = html.match(/(https?:\/\/[^"'\s]+?\.jpg)/g);
|
||||
const coverUrl = coverMatch ? coverMatch[0].trim() : '';
|
||||
|
||||
return {
|
||||
code: 200,
|
||||
episodes: matches,
|
||||
detailUrl: detailUrl,
|
||||
videoInfo: {
|
||||
title: titleText,
|
||||
cover: coverUrl,
|
||||
desc: descText,
|
||||
source_name: apiSite.name,
|
||||
source: apiSite.key,
|
||||
id: id,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
async function getDetailFromApi(
|
||||
apiSite: ApiSite,
|
||||
id: string
|
||||
): Promise<VideoDetail> {
|
||||
const detailUrl = `${apiSite.api}${API_CONFIG.detail.path}${id}`;
|
||||
|
||||
const controller = new AbortController();
|
||||
const timeoutId = setTimeout(() => controller.abort(), 10000);
|
||||
|
||||
const response = await fetch(detailUrl, {
|
||||
headers: API_CONFIG.detail.headers,
|
||||
signal: controller.signal,
|
||||
});
|
||||
|
||||
clearTimeout(timeoutId);
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`详情请求失败: ${response.status}`);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (
|
||||
!data ||
|
||||
!data.list ||
|
||||
!Array.isArray(data.list) ||
|
||||
data.list.length === 0
|
||||
) {
|
||||
throw new Error('获取到的详情内容无效');
|
||||
}
|
||||
|
||||
const videoDetail = data.list[0];
|
||||
let episodes: string[] = [];
|
||||
|
||||
if (videoDetail.vod_play_url) {
|
||||
const playSources = videoDetail.vod_play_url.split('$$$');
|
||||
|
||||
if (playSources.length > 0) {
|
||||
const mainSource = playSources[0];
|
||||
const episodeList = mainSource.split('#');
|
||||
|
||||
episodes = episodeList
|
||||
.map((ep: string) => {
|
||||
const parts = ep.split('$');
|
||||
return parts.length > 1 ? parts[1] : '';
|
||||
})
|
||||
.filter(
|
||||
(url: string) =>
|
||||
url && (url.startsWith('http://') || url.startsWith('https://'))
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (episodes.length === 0 && videoDetail.vod_content) {
|
||||
const matches = videoDetail.vod_content.match(M3U8_PATTERN) || [];
|
||||
episodes = matches.map((link: string) => link.replace(/^\$/, ''));
|
||||
}
|
||||
|
||||
return {
|
||||
code: 200,
|
||||
episodes: episodes,
|
||||
detailUrl: detailUrl,
|
||||
videoInfo: {
|
||||
title: videoDetail.vod_name,
|
||||
cover: videoDetail.vod_pic,
|
||||
desc: cleanHtmlTags(videoDetail.vod_content),
|
||||
type: videoDetail.type_name,
|
||||
year: videoDetail.vod_year,
|
||||
area: videoDetail.vod_area,
|
||||
director: videoDetail.vod_director,
|
||||
actor: videoDetail.vod_actor,
|
||||
remarks: videoDetail.vod_remarks,
|
||||
source_name: apiSite.name,
|
||||
source: apiSite.key,
|
||||
id: id,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export async function getVideoDetail(
|
||||
id: string,
|
||||
sourceCode: string
|
||||
): Promise<VideoDetail> {
|
||||
if (!id) {
|
||||
throw new Error('缺少视频ID参数');
|
||||
}
|
||||
|
||||
if (!/^[\w-]+$/.test(id)) {
|
||||
throw new Error('无效的视频ID格式');
|
||||
}
|
||||
|
||||
const apiSites = getApiSites();
|
||||
const apiSite = apiSites.find((site) => site.key === sourceCode);
|
||||
|
||||
if (!apiSite) {
|
||||
throw new Error('无效的API来源');
|
||||
}
|
||||
|
||||
if (apiSite.detail) {
|
||||
return await handleSpecialSourceDetail(id, apiSite);
|
||||
} else {
|
||||
return await getDetailFromApi(apiSite, id);
|
||||
}
|
||||
}
|
||||
import { getCacheTime } from '@/lib/config';
|
||||
import { getVideoDetail } from '@/lib/video';
|
||||
|
||||
export async function GET(request: Request) {
|
||||
const { searchParams } = new URL(request.url);
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
import { NextResponse } from 'next/server';
|
||||
|
||||
import { API_CONFIG, ApiSite, getApiSites, getCacheTime } from '@/lib/config';
|
||||
|
||||
import { getVideoDetail } from '../detail/route';
|
||||
import { getVideoDetail } from '@/lib/video';
|
||||
|
||||
export interface SearchResult {
|
||||
id: string;
|
||||
|
||||
@@ -1,17 +0,0 @@
|
||||
import { Metadata } from 'next';
|
||||
import * as React from 'react';
|
||||
|
||||
import '@/styles/colors.css';
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: 'Components',
|
||||
description: 'Pre-built components with awesome default',
|
||||
};
|
||||
|
||||
export default function ComponentsLayout({
|
||||
children,
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
}) {
|
||||
return <>{children}</>;
|
||||
}
|
||||
@@ -1,461 +0,0 @@
|
||||
'use client';
|
||||
|
||||
import clsx from 'clsx';
|
||||
import {
|
||||
ArrowRight,
|
||||
CreditCard,
|
||||
Laptop,
|
||||
Phone,
|
||||
Plus,
|
||||
Shield,
|
||||
} from 'lucide-react';
|
||||
import * as React from 'react';
|
||||
|
||||
import Button from '@/components/buttons/Button';
|
||||
import IconButton from '@/components/buttons/IconButton';
|
||||
import TextButton from '@/components/buttons/TextButton';
|
||||
import ArrowLink from '@/components/links/ArrowLink';
|
||||
import ButtonLink from '@/components/links/ButtonLink';
|
||||
import PrimaryLink from '@/components/links/PrimaryLink';
|
||||
import UnderlineLink from '@/components/links/UnderlineLink';
|
||||
import UnstyledLink from '@/components/links/UnstyledLink';
|
||||
import NextImage from '@/components/NextImage';
|
||||
import Skeleton from '@/components/Skeleton';
|
||||
|
||||
type Color = (typeof colorList)[number];
|
||||
|
||||
export default function ComponentPage() {
|
||||
const [mode, setMode] = React.useState<'dark' | 'light'>('light');
|
||||
const [color, setColor] = React.useState<Color>('sky');
|
||||
function toggleMode() {
|
||||
return mode === 'dark' ? setMode('light') : setMode('dark');
|
||||
}
|
||||
|
||||
const textColor = mode === 'dark' ? 'text-gray-300' : 'text-gray-600';
|
||||
|
||||
return (
|
||||
<main>
|
||||
<section
|
||||
className={clsx(mode === 'dark' ? 'bg-dark' : 'bg-white', color)}
|
||||
>
|
||||
<div
|
||||
className={clsx(
|
||||
'layout min-h-screen py-20',
|
||||
mode === 'dark' ? 'text-white' : 'text-black'
|
||||
)}
|
||||
>
|
||||
<h1>Built-in Components</h1>
|
||||
<ArrowLink direction='left' className='mt-2' href='/'>
|
||||
Back to Home
|
||||
</ArrowLink>
|
||||
|
||||
<div className='mt-8 flex flex-wrap gap-2'>
|
||||
<Button
|
||||
onClick={toggleMode}
|
||||
variant={mode === 'dark' ? 'light' : 'dark'}
|
||||
>
|
||||
Set to {mode === 'dark' ? 'light' : 'dark'}
|
||||
</Button>
|
||||
{/* <Button onClick={randomize}>Randomize CSS Variable</Button> */}
|
||||
</div>
|
||||
|
||||
<ol className='mt-8 space-y-6'>
|
||||
<li className='space-y-2'>
|
||||
<h2 className='text-lg md:text-xl'>Customize Colors</h2>
|
||||
<p className={clsx('!mt-1 text-sm', textColor)}>
|
||||
You can change primary color to any Tailwind CSS colors. See
|
||||
globals.css to change your color.
|
||||
</p>
|
||||
<div className='flex flex-wrap gap-2'>
|
||||
<select
|
||||
name='color'
|
||||
id='color'
|
||||
value={color}
|
||||
className={clsx(
|
||||
'block max-w-xs rounded',
|
||||
mode === 'dark'
|
||||
? 'bg-dark border border-gray-600'
|
||||
: 'border-gray-300 bg-white',
|
||||
'focus:border-primary-400 focus:ring-primary-400 focus:outline-none focus:ring'
|
||||
)}
|
||||
onChange={(e) => setColor(e.target.value as Color)}
|
||||
>
|
||||
{colorList.map((c) => (
|
||||
<option key={c} value={c}>
|
||||
{c}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
<ButtonLink href='https://github.com/theodorusclarence/ts-nextjs-tailwind-starter/blob/main/src/styles/colors.css'>
|
||||
Check list of colors
|
||||
</ButtonLink>
|
||||
</div>
|
||||
<div className='flex flex-wrap gap-2 text-xs font-medium'>
|
||||
<div className='bg-primary-50 flex h-10 w-10 items-center justify-center rounded text-black'>
|
||||
50
|
||||
</div>
|
||||
<div className='bg-primary-100 flex h-10 w-10 items-center justify-center rounded text-black'>
|
||||
100
|
||||
</div>
|
||||
<div className='bg-primary-200 flex h-10 w-10 items-center justify-center rounded text-black'>
|
||||
200
|
||||
</div>
|
||||
<div className='bg-primary-300 flex h-10 w-10 items-center justify-center rounded text-black'>
|
||||
300
|
||||
</div>
|
||||
<div className='bg-primary-400 flex h-10 w-10 items-center justify-center rounded text-black'>
|
||||
400
|
||||
</div>
|
||||
<div className='bg-primary-500 flex h-10 w-10 items-center justify-center rounded text-black'>
|
||||
500
|
||||
</div>
|
||||
<div className='bg-primary-600 flex h-10 w-10 items-center justify-center rounded text-white'>
|
||||
600
|
||||
</div>
|
||||
<div className='bg-primary-700 flex h-10 w-10 items-center justify-center rounded text-white'>
|
||||
700
|
||||
</div>
|
||||
<div className='bg-primary-800 flex h-10 w-10 items-center justify-center rounded text-white'>
|
||||
800
|
||||
</div>
|
||||
<div className='bg-primary-900 flex h-10 w-10 items-center justify-center rounded text-white'>
|
||||
900
|
||||
</div>
|
||||
<div className='bg-primary-950 flex h-10 w-10 items-center justify-center rounded text-white'>
|
||||
950
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
<li className='space-y-2'>
|
||||
<h2 className='text-lg md:text-xl'>UnstyledLink</h2>
|
||||
<p className={clsx('!mt-1 text-sm', textColor)}>
|
||||
No style applied, differentiate internal and outside links, give
|
||||
custom cursor for outside links.
|
||||
</p>
|
||||
<div className='space-x-2'>
|
||||
<UnstyledLink href='/'>Internal Links</UnstyledLink>
|
||||
<UnstyledLink href='https://theodorusclarence.com'>
|
||||
Outside Links
|
||||
</UnstyledLink>
|
||||
</div>
|
||||
</li>
|
||||
<li className='space-y-2'>
|
||||
<h2 className='text-lg md:text-xl'>PrimaryLink</h2>
|
||||
<p className={clsx('!mt-1 text-sm', textColor)}>
|
||||
Add styling on top of UnstyledLink, giving a primary color to
|
||||
the link.
|
||||
</p>
|
||||
<div className='space-x-2'>
|
||||
<PrimaryLink href='/'>Internal Links</PrimaryLink>
|
||||
<PrimaryLink href='https://theodorusclarence.com'>
|
||||
Outside Links
|
||||
</PrimaryLink>
|
||||
</div>
|
||||
</li>
|
||||
<li className='space-y-2'>
|
||||
<h2 className='text-lg md:text-xl'>UnderlineLink</h2>
|
||||
<p className={clsx('!mt-1 text-sm', textColor)}>
|
||||
Add styling on top of UnstyledLink, giving a dotted and animated
|
||||
underline.
|
||||
</p>
|
||||
<div className='space-x-2'>
|
||||
<UnderlineLink href='/'>Internal Links</UnderlineLink>
|
||||
<UnderlineLink href='https://theodorusclarence.com'>
|
||||
Outside Links
|
||||
</UnderlineLink>
|
||||
</div>
|
||||
</li>
|
||||
<li className='space-y-2'>
|
||||
<h2 className='text-lg md:text-xl'>ArrowLink</h2>
|
||||
<p className={clsx('!mt-1 text-sm', textColor)}>
|
||||
Useful for indicating navigation, I use this quite a lot, so why
|
||||
not build a component with some whimsy touch?
|
||||
</p>
|
||||
<div className='flex flex-wrap items-center gap-4'>
|
||||
<ArrowLink href='/' direction='left'>
|
||||
Direction Left
|
||||
</ArrowLink>
|
||||
<ArrowLink href='/'>Direction Right</ArrowLink>
|
||||
<ArrowLink
|
||||
as={UnstyledLink}
|
||||
className='inline-flex items-center'
|
||||
href='/'
|
||||
>
|
||||
Polymorphic
|
||||
</ArrowLink>
|
||||
<ArrowLink
|
||||
as={ButtonLink}
|
||||
variant='light'
|
||||
className='inline-flex items-center'
|
||||
href='/'
|
||||
>
|
||||
Polymorphic
|
||||
</ArrowLink>
|
||||
</div>
|
||||
</li>
|
||||
<li className='space-y-2'>
|
||||
<h2 className='text-lg md:text-xl'>ButtonLink</h2>
|
||||
<p className={clsx('!mt-1 text-sm', textColor)}>
|
||||
Button styled link with 3 variants.
|
||||
</p>
|
||||
<div className='flex flex-wrap gap-2'>
|
||||
<ButtonLink
|
||||
variant='primary'
|
||||
href='https://theodorusclarence.com'
|
||||
>
|
||||
Primary Variant
|
||||
</ButtonLink>
|
||||
<ButtonLink
|
||||
variant='outline'
|
||||
isDarkBg={mode === 'dark'}
|
||||
href='https://theodorusclarence.com'
|
||||
>
|
||||
Outline Variant
|
||||
</ButtonLink>
|
||||
<ButtonLink
|
||||
variant='ghost'
|
||||
isDarkBg={mode === 'dark'}
|
||||
href='https://theodorusclarence.com'
|
||||
>
|
||||
Ghost Variant
|
||||
</ButtonLink>
|
||||
<ButtonLink variant='dark' href='https://theodorusclarence.com'>
|
||||
Dark Variant
|
||||
</ButtonLink>
|
||||
<ButtonLink
|
||||
variant='light'
|
||||
href='https://theodorusclarence.com'
|
||||
>
|
||||
Light Variant
|
||||
</ButtonLink>
|
||||
</div>
|
||||
</li>
|
||||
<li className='space-y-2'>
|
||||
<h2 className='text-lg md:text-xl'>Button</h2>
|
||||
<p className={clsx('!mt-1 text-sm', textColor)}>
|
||||
Ordinary button with style.
|
||||
</p>
|
||||
<div className='flex flex-wrap gap-2'>
|
||||
<Button variant='primary'>Primary Variant</Button>
|
||||
<Button variant='outline' isDarkBg={mode === 'dark'}>
|
||||
Outline Variant
|
||||
</Button>
|
||||
<Button variant='ghost' isDarkBg={mode === 'dark'}>
|
||||
Ghost Variant
|
||||
</Button>
|
||||
<Button variant='dark'>Dark Variant</Button>
|
||||
<Button variant='light'>Light Variant</Button>
|
||||
</div>
|
||||
<div className='flex flex-wrap gap-2'>
|
||||
<Button
|
||||
variant='primary'
|
||||
leftIcon={Plus}
|
||||
rightIcon={ArrowRight}
|
||||
>
|
||||
Icon
|
||||
</Button>
|
||||
<Button
|
||||
variant='outline'
|
||||
leftIcon={Plus}
|
||||
rightIcon={ArrowRight}
|
||||
isDarkBg={mode === 'dark'}
|
||||
>
|
||||
Icon
|
||||
</Button>
|
||||
<Button
|
||||
variant='ghost'
|
||||
leftIcon={Plus}
|
||||
rightIcon={ArrowRight}
|
||||
isDarkBg={mode === 'dark'}
|
||||
>
|
||||
Icon
|
||||
</Button>
|
||||
<Button variant='dark' leftIcon={Plus} rightIcon={ArrowRight}>
|
||||
Icon
|
||||
</Button>
|
||||
<Button variant='light' leftIcon={Plus} rightIcon={ArrowRight}>
|
||||
Icon
|
||||
</Button>
|
||||
</div>
|
||||
<div className='!mt-4 flex flex-wrap gap-2'>
|
||||
<Button size='sm' variant='primary'>
|
||||
Small Size
|
||||
</Button>
|
||||
<Button size='sm' variant='outline' isDarkBg={mode === 'dark'}>
|
||||
Small Size
|
||||
</Button>
|
||||
<Button size='sm' variant='ghost' isDarkBg={mode === 'dark'}>
|
||||
Small Size
|
||||
</Button>
|
||||
<Button size='sm' variant='dark'>
|
||||
Small Size
|
||||
</Button>
|
||||
<Button size='sm' variant='light'>
|
||||
Small Size
|
||||
</Button>
|
||||
</div>
|
||||
<div className='flex flex-wrap gap-2'>
|
||||
<Button
|
||||
size='sm'
|
||||
variant='primary'
|
||||
leftIcon={Plus}
|
||||
rightIcon={ArrowRight}
|
||||
>
|
||||
Icon
|
||||
</Button>
|
||||
<Button
|
||||
size='sm'
|
||||
variant='outline'
|
||||
leftIcon={Plus}
|
||||
rightIcon={ArrowRight}
|
||||
isDarkBg={mode === 'dark'}
|
||||
>
|
||||
Icon
|
||||
</Button>
|
||||
<Button
|
||||
size='sm'
|
||||
variant='ghost'
|
||||
leftIcon={Plus}
|
||||
rightIcon={ArrowRight}
|
||||
isDarkBg={mode === 'dark'}
|
||||
>
|
||||
Icon
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
size='sm'
|
||||
variant='dark'
|
||||
leftIcon={Plus}
|
||||
rightIcon={ArrowRight}
|
||||
>
|
||||
Icon
|
||||
</Button>
|
||||
<Button
|
||||
size='sm'
|
||||
variant='light'
|
||||
leftIcon={Plus}
|
||||
rightIcon={ArrowRight}
|
||||
>
|
||||
Icon
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className='!mt-4 flex flex-wrap gap-2'>
|
||||
<Button disabled variant='primary'>
|
||||
Disabled
|
||||
</Button>
|
||||
<Button disabled variant='outline' isDarkBg={mode === 'dark'}>
|
||||
Disabled
|
||||
</Button>
|
||||
<Button disabled variant='ghost' isDarkBg={mode === 'dark'}>
|
||||
Disabled
|
||||
</Button>
|
||||
<Button disabled variant='dark'>
|
||||
Disabled
|
||||
</Button>
|
||||
<Button disabled variant='light'>
|
||||
Disabled
|
||||
</Button>
|
||||
</div>
|
||||
<div className='flex flex-wrap gap-2'>
|
||||
<Button isLoading variant='primary'>
|
||||
Disabled
|
||||
</Button>
|
||||
<Button isLoading variant='outline' isDarkBg={mode === 'dark'}>
|
||||
Disabled
|
||||
</Button>
|
||||
<Button isLoading variant='ghost' isDarkBg={mode === 'dark'}>
|
||||
Disabled
|
||||
</Button>
|
||||
<Button isLoading variant='dark'>
|
||||
Disabled
|
||||
</Button>
|
||||
<Button isLoading variant='light'>
|
||||
Disabled
|
||||
</Button>
|
||||
</div>
|
||||
</li>
|
||||
<li className='space-y-2'>
|
||||
<h2 className='text-lg md:text-xl'>TextButton</h2>
|
||||
<p className={clsx('!mt-1 text-sm', textColor)}>
|
||||
Button with a text style
|
||||
</p>
|
||||
<div className='space-x-2'>
|
||||
<TextButton>Primary Variant</TextButton>
|
||||
<TextButton variant='basic'>Basic Variant</TextButton>
|
||||
</div>
|
||||
</li>
|
||||
<li className='space-y-2'>
|
||||
<h2 className='text-lg md:text-xl'>IconButton</h2>
|
||||
<p className={clsx('!mt-1 text-sm', textColor)}>
|
||||
Button with only icon inside
|
||||
</p>
|
||||
<div className='space-x-2'>
|
||||
<IconButton icon={Plus} />
|
||||
<IconButton variant='outline' icon={Laptop} />
|
||||
<IconButton variant='ghost' icon={Phone} />
|
||||
<IconButton variant='dark' icon={Shield} />
|
||||
<IconButton variant='light' icon={CreditCard} />
|
||||
</div>
|
||||
</li>
|
||||
<li className='space-y-2'>
|
||||
<h2 className='text-lg md:text-xl'>Custom 404 Page</h2>
|
||||
<p className={clsx('!mt-1 text-sm', textColor)}>
|
||||
Styled 404 page with some animation.
|
||||
</p>
|
||||
<div className='flex flex-wrap gap-2'>
|
||||
<ButtonLink href='/404'>Visit the 404 page</ButtonLink>
|
||||
</div>
|
||||
</li>
|
||||
<li className='space-y-2'>
|
||||
<h2 className='text-lg md:text-xl'>Next Image</h2>
|
||||
<p className={clsx('!mt-1 text-sm', textColor)}>
|
||||
Next Image with default props and skeleton animation
|
||||
</p>
|
||||
<NextImage
|
||||
useSkeleton
|
||||
className='w-32 md:w-40'
|
||||
src='/favicon/android-chrome-192x192.png'
|
||||
width='180'
|
||||
height='180'
|
||||
alt='Icon'
|
||||
/>
|
||||
</li>
|
||||
<li className='space-y-2'>
|
||||
<h2 className='text-lg md:text-xl'>Skeleton</h2>
|
||||
<p className={clsx('!mt-1 text-sm', textColor)}>
|
||||
Skeleton with shimmer effect
|
||||
</p>
|
||||
<Skeleton className='h-72 w-72' />
|
||||
</li>
|
||||
</ol>
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
|
||||
const colorList = [
|
||||
'slate',
|
||||
'gray',
|
||||
'zinc',
|
||||
'neutral',
|
||||
'stone',
|
||||
'red',
|
||||
'orange',
|
||||
'amber',
|
||||
'yellow',
|
||||
'lime',
|
||||
'green',
|
||||
'emerald',
|
||||
'teal',
|
||||
'cyan',
|
||||
'sky',
|
||||
'blue',
|
||||
'indigo',
|
||||
'violet',
|
||||
'purple',
|
||||
'fuchsia',
|
||||
'pink',
|
||||
'rose',
|
||||
] as const;
|
||||
@@ -6,10 +6,9 @@ import { useEffect, useState } from 'react';
|
||||
|
||||
import type { PlayRecord } from '@/lib/db.client';
|
||||
import { generateStorageKey, getAllPlayRecords } from '@/lib/db.client';
|
||||
import { VideoDetail } from '@/lib/video';
|
||||
|
||||
import PageLayout from '@/components/layout/PageLayout';
|
||||
|
||||
import { VideoDetail } from '../api/detail/route';
|
||||
import PageLayout from '@/components/PageLayout';
|
||||
|
||||
export default function DetailPage() {
|
||||
const searchParams = useSearchParams();
|
||||
|
||||
@@ -5,7 +5,7 @@ import { useEffect, useRef, useState } from 'react';
|
||||
|
||||
import DemoCard from '@/components/DemoCard';
|
||||
import DoubanCardSkeleton from '@/components/DoubanCardSkeleton';
|
||||
import PageLayout from '@/components/layout/PageLayout';
|
||||
import PageLayout from '@/components/PageLayout';
|
||||
|
||||
// 定义豆瓣数据项类型
|
||||
interface DoubanItem {
|
||||
|
||||
@@ -1,38 +0,0 @@
|
||||
'use client'; // Error components must be Client Components
|
||||
|
||||
import * as React from 'react';
|
||||
import { RiAlarmWarningFill } from 'react-icons/ri';
|
||||
|
||||
import TextButton from '@/components/buttons/TextButton';
|
||||
|
||||
export default function Error({
|
||||
error,
|
||||
reset,
|
||||
}: {
|
||||
error: Error & { digest?: string };
|
||||
reset: () => void;
|
||||
}) {
|
||||
React.useEffect(() => {
|
||||
// eslint-disable-next-line no-console
|
||||
console.error(error);
|
||||
}, [error]);
|
||||
|
||||
return (
|
||||
<main>
|
||||
<section className='bg-white'>
|
||||
<div className='layout flex min-h-screen flex-col items-center justify-center text-center text-black'>
|
||||
<RiAlarmWarningFill
|
||||
size={60}
|
||||
className='drop-shadow-glow animate-flicker text-red-500'
|
||||
/>
|
||||
<h1 className='mt-8 text-4xl md:text-6xl'>
|
||||
Oops, something went wrong!
|
||||
</h1>
|
||||
<TextButton variant='basic' onClick={reset} className='mt-4'>
|
||||
Try again
|
||||
</TextButton>
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
@@ -1,24 +0,0 @@
|
||||
import { Metadata } from 'next';
|
||||
import * as React from 'react';
|
||||
import { RiAlarmWarningFill } from 'react-icons/ri';
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: 'Not Found',
|
||||
};
|
||||
|
||||
export default function NotFound() {
|
||||
return (
|
||||
<main>
|
||||
<section className='bg-white'>
|
||||
<div className='layout flex min-h-screen flex-col items-center justify-center text-center text-black'>
|
||||
<RiAlarmWarningFill
|
||||
size={60}
|
||||
className='drop-shadow-glow animate-flicker text-red-500'
|
||||
/>
|
||||
<h1 className='mt-8 text-4xl md:text-6xl'>Page Not Found</h1>
|
||||
<a href='/'>Back to home</a>
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
@@ -15,7 +15,7 @@ import CapsuleSwitch from '@/components/CapsuleSwitch';
|
||||
import CollectionCard from '@/components/CollectionCard';
|
||||
import ContinueWatching from '@/components/ContinueWatching';
|
||||
import DemoCard from '@/components/DemoCard';
|
||||
import PageLayout from '@/components/layout/PageLayout';
|
||||
import PageLayout from '@/components/PageLayout';
|
||||
import ScrollableRow from '@/components/ScrollableRow';
|
||||
|
||||
interface DoubanItem {
|
||||
|
||||
@@ -12,8 +12,7 @@ import {
|
||||
getAllPlayRecords,
|
||||
savePlayRecord,
|
||||
} from '@/lib/db.client';
|
||||
|
||||
import { VideoDetail } from '../api/detail/route';
|
||||
import { VideoDetail } from '@/lib/video';
|
||||
|
||||
// 动态导入 Artplayer 和 Hls 以避免 SSR 问题
|
||||
let Artplayer: any = null;
|
||||
|
||||
@@ -4,7 +4,7 @@ import { Search } from 'lucide-react';
|
||||
import { useRouter, useSearchParams } from 'next/navigation';
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
|
||||
import PageLayout from '@/components/layout/PageLayout';
|
||||
import PageLayout from '@/components/PageLayout';
|
||||
import VideoCard from '@/components/VideoCard';
|
||||
|
||||
// 模拟搜索历史数据
|
||||
|
||||
Reference in New Issue
Block a user