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