Layout.tsx

1'use client'
2
3import * as React from 'react'
4import { config } from '@/configs/main'
5import { Link } from 'nextjs13-progress'
6import Image from 'next/image'
7import { DynamicIcon, IconName } from './dynamic-icon'
8
9export default function Layout({ children }: { children: React.ReactNode }) {
10	const [isScrolled, setIsScrolled] = React.useState(false)
11
12	React.useEffect(() => {
13		const handleScroll = () => {
14			setIsScrolled(window.scrollY > 0)
15		}
16
17		window.addEventListener('scroll', handleScroll)
18		return () => window.removeEventListener('scroll', handleScroll)
19	}, [])
20
21	React.useEffect(() => {
22		const canvas = document.getElementById(
23			'particle-canvas',
24		) as HTMLCanvasElement
25		const ctx = canvas.getContext('2d')
26
27		if (!ctx) return
28
29		canvas.width = window.innerWidth
30		canvas.height = window.innerHeight
31
32		const particles: Particle[] = []
33
34		class Particle {
35			x: number
36			y: number
37			size: number
38			speedX: number
39			speedY: number
40
41			constructor() {
42				this.x = Math.random() * canvas.width
43				this.y = Math.random() * canvas.height
44				this.size = Math.random() * 5 + 1
45				this.speedX = Math.random() * 3 - 1.5
46				this.speedY = Math.random() * 3 - 1.5
47			}
48
49			update() {
50				this.x += this.speedX
51				this.y += this.speedY
52
53				if (this.size > 0.2) this.size -= 0.1
54			}
55
56			draw() {
57				if (ctx) {
58					ctx.fillStyle = 'rgba(255,255,255,0.6)'
59					ctx.strokeStyle = 'rgba(255,255,255,0.6)'
60					ctx.lineWidth = 2
61					ctx.beginPath()
62					ctx.arc(this.x, this.y, this.size, 0, Math.PI * 2)
63					ctx.closePath()
64					ctx.fill()
65				}
66			}
67		}
68
69		function handleParticles() {
70			for (let i = 0; i < particles.length; i++) {
71				particles[i].update()
72				particles[i].draw()
73
74				if (particles[i].size <= 0.2) {
75					particles.splice(i, 1)
76					i--
77				}
78			}
79		}
80
81		function animate() {
82			if (!ctx) return
83			ctx.clearRect(0, 0, canvas.width, canvas.height)
84			if (particles.length < 100) {
85				particles.push(new Particle())
86			}
87			handleParticles()
88			requestAnimationFrame(animate)
89		}
90
91		animate()
92
93		window.addEventListener('resize', () => {
94			canvas.width = window.innerWidth
95			canvas.height = window.innerHeight
96		})
97
98		// Cleanup
99		return () => {
100			window.removeEventListener('resize', () => {
101				canvas.width = window.innerWidth
102				canvas.height = window.innerHeight
103			})
104		}
105	}, [])
106
107	return (
108		<>
109			<div className="flex min-h-screen flex-col bg-gray-900 text-white">
110				<canvas id="particle-canvas" className="fixed inset-0 z-0 blur-[2px]" />
111
112				{/* Header */}
113				<header
114					className={`sticky top-0 z-50 border-b border-gray-800 backdrop-blur-sm transition-all duration-200 ${isScrolled ? 'bg-gray-900/75 shadow-lg' : 'bg-transparent'} `}
115				>
116					<nav className="container mx-auto px-4 py-4">
117						<div className="flex items-center justify-between">
118							<Link
119								href="/"
120								className="flex items-center space-x-3 transition-opacity hover:opacity-80"
121							>
122								<Image
123									src={config.avatar}
124									alt={config.name}
125									width={40}
126									height={40}
127									className="rounded-full"
128								/>
129								<span className="text-xl font-bold">{config.name}</span>
130							</Link>
131
132							<div className="flex items-center space-x-6">
133								{config.header?.map((item) => (
134									<div key={item.name} className="group relative">
135										{item.dropdown ?
136											<>
137												<button className="group flex items-center space-x-1 transition-colors hover:text-purple-400">
138													<span>{item.name}</span>
139													<svg
140														className="h-4 w-4 transition-transform group-hover:rotate-180"
141														fill="none"
142														viewBox="0 0 24 24"
143														stroke="currentColor"
144													>
145														<path
146															strokeLinecap="round"
147															strokeLinejoin="round"
148															strokeWidth={2}
149															d="M19 9l-7 7-7-7"
150														/>
151													</svg>
152												</button>
153												<div className="absolute right-0 mt-2 w-48 origin-top-right scale-0 transform rounded-md bg-gray-800 py-1 opacity-0 transition-all group-hover:scale-100 group-hover:opacity-100">
154													{item.dropdown.map((dropdownItem) => (
155														<Link
156															key={dropdownItem.name}
157															href={dropdownItem.href}
158															className="block px-4 py-2 text-sm text-gray-300 hover:bg-gray-700 hover:text-white"
159														>
160															{dropdownItem.name}
161														</Link>
162													))}
163												</div>
164											</>
165										:	<Link
166												href={item.href}
167												className="transition-colors hover:text-purple-400"
168												target={
169													item.href.startsWith('http') ? '_blank' : undefined
170												}
171												rel={
172													item.href.startsWith('http') ?
173														'noopener noreferrer'
174													:	undefined
175												}
176											>
177												{item.name}
178											</Link>
179										}
180									</div>
181								))}
182							</div>
183						</div>
184					</nav>
185				</header>
186
187				{/* Main content */}
188				<main className="relative z-10 flex-grow">{children}</main>
189
190				{/* Footer */}
191				<footer className="relative z-10 border-t border-gray-800 backdrop-blur-sm">
192					<div className="container mx-auto px-4 py-8">
193						<div className="grid grid-cols-1 gap-8 md:grid-cols-3">
194							{/* Contact Info */}
195							<div>
196								<h3 className="mb-4 text-lg font-bold">Contact</h3>
197								<p className="text-gray-400">{config.email}</p>
198								<p className="text-gray-400">{config.location}</p>
199								<p className="text-gray-400">Timezone: {config.timezone}</p>
200							</div>
201
202							{/* Social Links */}
203							<div>
204								<h3 className="mb-4 text-lg font-bold">Connect</h3>
205								<div className="flex space-x-4">
206									{config.socials?.map((social) => (
207										<a
208											key={social.name}
209											href={social.url}
210											target="_blank"
211											rel="noopener noreferrer"
212											className="transition-opacity hover:opacity-75"
213											style={{ color: social.color }}
214										>
215											<DynamicIcon
216												name={social.iconName as IconName}
217												size={24}
218												color={social.color}
219											/>
220										</a>
221									))}
222								</div>
223							</div>
224
225							{/* Copyright */}
226							<div className="text-right">
227								<p className="text-gray-400">
228									© {new Date().getFullYear()} {config.name}
229								</p>
230								<p className="mt-2 text-sm text-gray-400">
231									Built with ❤️ using Next.js
232								</p>
233							</div>
234						</div>
235					</div>
236				</footer>
237			</div>
238		</>
239	)
240}
241