's'
This commit is contained in:
23
app/components/Footer.tsx
Normal file
23
app/components/Footer.tsx
Normal file
@@ -0,0 +1,23 @@
|
||||
export default function Footer() {
|
||||
return (
|
||||
<footer className="bg-white border-t">
|
||||
<div className="container mx-auto px-4 py-6 text-center text-gray-600">
|
||||
<p>
|
||||
© 2017-{new Date().getFullYear()}{' '}
|
||||
<a href="/about" className="font-bold hover:text-gray-900">
|
||||
dh.leti.ltd
|
||||
</a>
|
||||
{' '}design by{' '}
|
||||
<a
|
||||
href="https://www.viggoz.com"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="font-bold hover:text-gray-900"
|
||||
>
|
||||
Viggo
|
||||
</a>
|
||||
</p>
|
||||
</div>
|
||||
</footer>
|
||||
)
|
||||
}
|
||||
18
app/components/Header.tsx
Normal file
18
app/components/Header.tsx
Normal file
@@ -0,0 +1,18 @@
|
||||
export default function Header() {
|
||||
return (
|
||||
<header className="bg-white shadow-sm">
|
||||
<div className="container mx-auto px-4 py-3 flex justify-between items-center">
|
||||
<div className="flex items-center">
|
||||
<div className="hidden md:flex items-center space-x-4">
|
||||
<a href="https://github.com/tianyaxiang/CoderNavigation"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-gray-600 hover:text-gray-900">
|
||||
<i className="fa-github"></i> GitHub
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
)
|
||||
}
|
||||
17
app/components/Layout.tsx
Normal file
17
app/components/Layout.tsx
Normal file
@@ -0,0 +1,17 @@
|
||||
import { ReactNode } from 'react'
|
||||
import Header from './Header'
|
||||
import Footer from './Footer'
|
||||
|
||||
interface LayoutProps {
|
||||
children: ReactNode
|
||||
}
|
||||
|
||||
export default function Layout({ children }: LayoutProps) {
|
||||
return (
|
||||
<div className="flex flex-col min-h-screen">
|
||||
<Header />
|
||||
<main className="flex-grow">{children}</main>
|
||||
<Footer />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
32
app/components/ResourceCard.tsx
Normal file
32
app/components/ResourceCard.tsx
Normal file
@@ -0,0 +1,32 @@
|
||||
import Image from 'next/image'
|
||||
import Link from 'next/link'
|
||||
import { ResourceItem } from '../types/navigation'
|
||||
|
||||
interface ResourceCardProps extends ResourceItem {}
|
||||
|
||||
export default function ResourceCard({ title, description, icon, url }: ResourceCardProps) {
|
||||
return (
|
||||
<Link
|
||||
href={url}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="card hover:shadow-lg transition-shadow"
|
||||
>
|
||||
<div className="card-content">
|
||||
<div className="card-icon">
|
||||
<Image
|
||||
src={icon}
|
||||
alt={title}
|
||||
width={48}
|
||||
height={48}
|
||||
className="rounded-lg"
|
||||
/>
|
||||
</div>
|
||||
<div className="card-text">
|
||||
<h3 className="card-title">{title}</h3>
|
||||
<p className="card-description">{description}</p>
|
||||
</div>
|
||||
</div>
|
||||
</Link>
|
||||
)
|
||||
}
|
||||
41
app/components/ScrollToTop.tsx
Normal file
41
app/components/ScrollToTop.tsx
Normal file
@@ -0,0 +1,41 @@
|
||||
'use client'
|
||||
|
||||
import { useEffect, useState } from 'react'
|
||||
|
||||
export function ScrollToTop() {
|
||||
const [isVisible, setIsVisible] = useState(false)
|
||||
|
||||
// 检查滚动位置
|
||||
const toggleVisibility = () => {
|
||||
if (window.pageYOffset > 300) {
|
||||
setIsVisible(true)
|
||||
} else {
|
||||
setIsVisible(false)
|
||||
}
|
||||
}
|
||||
|
||||
// 滚动到顶部
|
||||
const scrollToTop = () => {
|
||||
window.scrollTo({
|
||||
top: 0,
|
||||
behavior: 'smooth'
|
||||
})
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
window.addEventListener('scroll', toggleVisibility)
|
||||
return () => {
|
||||
window.removeEventListener('scroll', toggleVisibility)
|
||||
}
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<button
|
||||
className={`scroll-to-top ${isVisible ? 'show' : ''}`}
|
||||
onClick={scrollToTop}
|
||||
aria-label="Scroll to top"
|
||||
>
|
||||
<i className="fas fa-arrow-up"></i>
|
||||
</button>
|
||||
)
|
||||
}
|
||||
31
app/components/ThemeToggle.tsx
Normal file
31
app/components/ThemeToggle.tsx
Normal file
@@ -0,0 +1,31 @@
|
||||
'use client'
|
||||
|
||||
import { useTheme } from 'next-themes'
|
||||
import { useEffect, useState } from 'react'
|
||||
|
||||
export function ThemeToggle() {
|
||||
const [mounted, setMounted] = useState(false)
|
||||
const { theme, setTheme } = useTheme()
|
||||
|
||||
useEffect(() => {
|
||||
setMounted(true)
|
||||
}, [])
|
||||
|
||||
if (!mounted) {
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
<button
|
||||
onClick={() => setTheme(theme === 'dark' ? 'light' : 'dark')}
|
||||
className="theme-toggle"
|
||||
aria-label="Toggle theme"
|
||||
>
|
||||
{theme === 'dark' ? (
|
||||
<i className="fas fa-sun text-xl animate-spin-slow" />
|
||||
) : (
|
||||
<i className="fas fa-moon text-xl animate-pulse" />
|
||||
)}
|
||||
</button>
|
||||
)
|
||||
}
|
||||
8
app/components/theme-provider.tsx
Normal file
8
app/components/theme-provider.tsx
Normal file
@@ -0,0 +1,8 @@
|
||||
'use client'
|
||||
|
||||
import { ThemeProvider as NextThemesProvider } from 'next-themes'
|
||||
import { type ThemeProviderProps } from 'next-themes/dist/types'
|
||||
|
||||
export function ThemeProvider({ children, ...props }: ThemeProviderProps) {
|
||||
return <NextThemesProvider {...props}>{children}</NextThemesProvider>
|
||||
}
|
||||
76
app/components/ui/icon-picker.tsx
Normal file
76
app/components/ui/icon-picker.tsx
Normal file
@@ -0,0 +1,76 @@
|
||||
'use client'
|
||||
|
||||
import { useState } from 'react'
|
||||
import { Button } from "@/registry/new-york/ui/button"
|
||||
import { Input } from "@/registry/new-york/ui/input"
|
||||
import {
|
||||
Popover,
|
||||
PopoverContent,
|
||||
PopoverTrigger,
|
||||
} from "@/registry/new-york/ui/popover"
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const ICONS = [
|
||||
{ name: 'linecons-star', label: '星星' },
|
||||
{ name: 'linecons-desktop', label: '桌面' },
|
||||
{ name: 'linecons-database', label: '数据库' },
|
||||
{ name: 'linecons-inbox', label: '收件箱' },
|
||||
{ name: 'linecons-globe', label: '地球' },
|
||||
{ name: 'linecons-cloud', label: '云' },
|
||||
{ name: 'linecons-paper-plane', label: '纸飞机' },
|
||||
{ name: 'linecons-search', label: '搜索' },
|
||||
// 添加更多图标...
|
||||
]
|
||||
|
||||
interface IconPickerProps {
|
||||
value: string
|
||||
onChange: (value: string) => void
|
||||
}
|
||||
|
||||
export function IconPicker({ value, onChange }: IconPickerProps) {
|
||||
const [open, setOpen] = useState(false)
|
||||
|
||||
return (
|
||||
<Popover open={open} onOpenChange={setOpen}>
|
||||
<PopoverTrigger asChild>
|
||||
<Button
|
||||
variant="outline"
|
||||
role="combobox"
|
||||
aria-expanded={open}
|
||||
className="w-full justify-between"
|
||||
>
|
||||
{value ? (
|
||||
<>
|
||||
<i className={cn("mr-2 h-4 w-4", value)} />
|
||||
{ICONS.find(icon => icon.name === value)?.label || value}
|
||||
</>
|
||||
) : (
|
||||
"选择图标..."
|
||||
)}
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="w-[200px] p-0">
|
||||
<div className="grid grid-cols-3 gap-2 p-4">
|
||||
{ICONS.map(icon => (
|
||||
<Button
|
||||
key={icon.name}
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className={cn(
|
||||
"justify-start",
|
||||
value === icon.name && "bg-muted"
|
||||
)}
|
||||
onClick={() => {
|
||||
onChange(icon.name)
|
||||
setOpen(false)
|
||||
}}
|
||||
>
|
||||
<i className={cn("mr-2 h-4 w-4", icon.name)} />
|
||||
<span className="text-xs">{icon.label}</span>
|
||||
</Button>
|
||||
))}
|
||||
</div>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user