基于SSM的酒店管理系统-计算机毕业设计源码+无LW文档
2026/1/17 22:02:09
https://chat.xutongbao.top/nextjs/light/nano
'use client' import Header from '@/components/header' import { ArrowLeft, Send, RefreshCw, Sparkles, Upload, X, Download, Copy, Check, ImagePlus, Maximize2, } from 'lucide-react' import { useRouter } from 'next/navigation' import { useState, useEffect, useRef } from 'react' import Image from 'next/image' import Api from '@/api/h5Api' interface MessageItem { uid: string createTime: number info: { message?: string originalMessage?: string aiMessage?: string imgUrlCdn?: string | string[] visionImgList?: Array<{ url: string }> } } interface ApiResponse<T = any> { code: number message?: string data: T } export default function Page() { const router = useRouter() const [dataSource, setDataSource] = useState<MessageItem[]>([]) const [isLoading, setIsLoading] = useState(false) const [isDrawing, setIsDrawing] = useState(false) const [inputMessage, setInputMessage] = useState('') const [current, setCurrent] = useState(1) const [isHasMore, setIsHasMore] = useState(true) const [copiedText, setCopiedText] = useState<string | null>(null) const [uploadedImages, setUploadedImages] = useState<string[]>([]) const [previewImage, setPreviewImage] = useState<string | null>(null) const [qiniuToken, setQiniuToken] = useState<string>('') const [isMobileDevice, setIsMobileDevice] = useState(false) const messagesEndRef = useRef<HTMLDivElement>(null) const scrollContainerRef = useRef<HTMLDivElement>(null) const fileInputRef = useRef<HTMLInputElement>(null) // 检测是否是移动设备/触摸设备 useEffect(() => { const checkMobileDevice = () => { // 检测是否支持触摸事件 const isTouchDevice = 'ontouchstart' in window || navigator.maxTouchPoints > 0 || (navigator as any).msMaxTouchPoints > 0 // 检测 UserAgent const userAgent = navigator.userAgent.toLowerCase() const isMobileUA = /android|webos|iphone|ipad|ipod|blackberry|iemobile|opera mini/i.test( userAgent ) setIsMobileDevice(isTouchDevice || isMobileUA) } checkMobileDevice() }, []) // 获取七牛云上传 token useEffect(() => { const getUploadToken = async () => { try { const res = (await Api.uploadGetTokenForH5( {} )) as unknown as ApiResponse if (res.code === 200 && res.data?.token) { setQiniuToken(res.data.token) } } catch (error) { console.error('获取上传token失败:', error) } } getUploadToken() }, []) // 获取提示词 const getPrompt = (item: MessageItem) => { if (item.info?.originalMessage) { return item.info.originalMessage } else if (item.info?.message) { return item.info.message } return '' } // 加载数据 const handleSearch = async ({ page = 1, isRefresh = false } = {}) => { if (isRefresh) { setDataSource([]) setIsLoading(true) } try { const res = (await Api.mjAppSearch({ pageNum: page, pageSize: 10, })) as unknown as ApiResponse if (res.code === 200) { const { pageNum, pageSize, total } = res.data let list = res.data.list if (isRefresh) { setDataSource([...list]) } else { setDataSource((prev) => [...prev, ...list]) } const currentTemp = pageNum + 1 setCurrent(currentTemp) setIsHasMore(pageNum < Math.ceil(total / pageSize)) setIsLoading(false) } } catch (error) { console.error('加载失败:', error) setIsLoading(false) } } // 将图片转换为 PNG 格式 const convertImageToPng = (file: File): Promise<File> => { return new Promise((resolve, reject) => { const reader = new FileReader() reader.onload = (e) => { const img = new window.Image() img.onload = () => { // 创建 canvas const canvas = document.createElement('canvas') canvas.width = img.width canvas.height = img.height // 绘制图片到 canvas const ctx = canvas.getContext('2d') if (!ctx) { reject(new Error('无法获取 canvas context')) return } ctx.drawImage(img, 0, 0) // 转换为 PNG blob canvas.toBlob( (blob) => { if (!blob) { reject(new Error('图片转换失败')) return } // 创建新的 File 对象 const pngFile = new File( [blob], file.name.replace(/\.[^.]+$/, '.png'), { type: 'image/png', } ) resolve(pngFile) }, 'image/png', 1.0 ) } img.onerror = () => reject(new Error('图片加载失败')) img.src = e.target?.result as string } reader.onerror = () => reject(new Error('文件读取失败')) reader.readAsDataURL(file) }) } // 上传图片到七牛云 const handleImageUpload = async (e: React.ChangeEvent<HTMLInputElement>) => { const files = e.target.files if (!files || files.length === 0 || !qiniuToken) return const file = files[0] try { // 将图片转换为 PNG 格式 const pngFile = await convertImageToPng(file) const formData = new FormData() const key = `ai/mjBaseImg/${Date.now()}_${pngFile.name}` formData.append('file', pngFile) formData.append('token', qiniuToken) formData.append('key', key) const response = await fetch('https://upload-z1.qiniup.com', { method: 'POST', body: formData, }) const result = await response.json() if (result.code === 200) { const imageUrl = `https://static.xutongbao.top/${result.data.key}` setUploadedImages((prev) => [...prev, imageUrl]) } } catch (error) { console.error('上传失败:', error) } // 清空 input,允许重复选择同一文件 e.target.value = '' } // 移除上传的图片 const handleRemoveImage = (index: number) => { setUploadedImages((prev) => prev.filter((_, i) => i !== index)) } // 发送消息 const handleSendMessage = async () => { if (!inputMessage.trim() || isDrawing) return setIsDrawing(true) try { let message = inputMessage const res = (await Api.mjAdd({ info: { message, drawType: 'grid', type: 'imagine', uploadedImages }, })) as unknown as ApiResponse if (res.code === 200) { setInputMessage('') setUploadedImages([]) await handleSearch({ page: 1, isRefresh: true }) // 滚动到顶部 setTimeout(() => { if (scrollContainerRef.current) { scrollContainerRef.current.scrollTop = 0 } }, 100) } } catch (error) { console.error('发送失败:', error) } finally { setIsDrawing(false) } } // 复制文本 const handleCopy = async (text: string) => { try { await navigator.clipboard.writeText(text) setCopiedText(text) setTimeout(() => setCopiedText(null), 2000) } catch (error) { console.error('复制失败:', error) } } // 下载图片 const handleDownload = async (url: string, filename?: string) => { try { const response = await fetch(url) const blob = await response.blob() const link = document.createElement('a') link.href = URL.createObjectURL(blob) link.download = filename || `image_${Date.now()}.png` document.body.appendChild(link) link.click() document.body.removeChild(link) URL.revokeObjectURL(link.href) } catch (error) { console.error('下载失败:', error) } } // 加载更多 const handleLoadMore = () => { if (!isLoading && isHasMore) { handleSearch({ page: current }) } } // 初始化加载 useEffect(() => { handleSearch({ page: 1, isRefresh: true }) }, []) return ( <> <Header /> <main className='min-h-screen bg-linear-to-br from-primary/5 via-background to-secondary/5 relative overflow-hidden'> {/* 背景装饰 */} <div className='absolute inset-0 overflow-hidden pointer-events-none'> <div className='absolute top-20 left-1/4 w-96 h-96 bg-primary/5 rounded-full blur-3xl animate-pulse-slow' /> <div className='absolute bottom-20 right-1/4 w-96 h-96 bg-secondary/5 rounded-full blur-3xl animate-pulse-slow' style={{ animationDelay: '2s' }} /> <div className='absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 w-96 h-96 bg-accent/5 rounded-full blur-3xl animate-pulse-slow' style={{ animationDelay: '4s' }} /> </div> {/* 内容区域 */} <div className='relative max-w-6xl mx-auto px-3 sm:px-4 py-3 sm:py-6 h-screen flex flex-col gap-3 sm:gap-4'> {/* 头部:返回按钮和刷新按钮 */} <div className='flex items-center justify-between animate-fade-in'> <button onClick={() => router.push('/light')} className='group flex items-center gap-1.5 sm:gap-2 px-3 sm:px-4 py-2 sm:py-2.5 rounded-lg sm:rounded-xl bg-card/60 backdrop-blur-xl border border-border/50 hover:border-primary/50 shadow-sm hover:shadow-md transition-all duration-300' > <ArrowLeft className='w-4 h-4 text-muted-foreground group-hover:text-primary transition-colors duration-300 group-hover:-translate-x-0.5' /> <span className='text-xs sm:text-sm font-medium text-muted-foreground group-hover:text-foreground transition-colors duration-300'> 返回 </span> </button> <button onClick={() => handleSearch({ page: 1, isRefresh: true })} disabled={isLoading} className='group flex items-center gap-1.5 sm:gap-2 px-3 sm:px-4 py-2 sm:py-2.5 rounded-lg sm:rounded-xl bg-card/60 backdrop-blur-xl border border-border/50 hover:border-primary/50 shadow-sm hover:shadow-md transition-all duration-300 disabled:opacity-50 disabled:cursor-not-allowed' > <RefreshCw className={`w-4 h-4 text-muted-foreground group-hover:text-primary transition-all duration-300 ${ isLoading ? 'animate-spin' : 'group-hover:rotate-180' }`} /> <span className='text-xs sm:text-sm font-medium text-muted-foreground group-hover:text-foreground transition-colors duration-300'> 刷新 </span> </button> </div> {/* 创作输入区域 - 放在顶部,突出显示 */} <div className='bg-card/90 backdrop-blur-2xl rounded-2xl sm:rounded-3xl border-2 border-primary/20 shadow-2xl shadow-primary/10 p-3 sm:p-6 animate-fade-in-up relative overflow-hidden' style={{ animationDelay: '0.1s' }} > {/* 装饰性背景 */} <div className='absolute top-0 right-0 w-64 h-64 bg-linear-to-br from-primary/10 to-transparent rounded-full blur-3xl pointer-events-none' /> <div className='absolute bottom-0 left-0 w-64 h-64 bg-linear-to-tr from-secondary/10 to-transparent rounded-full blur-3xl pointer-events-none' /> <div className='relative space-y-3 sm:space-y-4'> {/* 标题 */} <div className='flex items-center gap-2 sm:gap-3 mb-1 sm:mb-2'> <div className='flex items-center justify-center w-8 h-8 sm:w-10 sm:h-10 rounded-xl sm:rounded-2xl bg-linear-to-br from-primary to-secondary shadow-lg'> <Sparkles className='w-4 h-4 sm:w-5 sm:h-5 text-primary-foreground animate-pulse' /> </div> <div> <h2 className='text-base sm:text-xl font-bold bg-linear-to-r from-primary to-secondary bg-clip-text text-transparent'> Nano Banana AI 绘画创作 </h2> <p className='text-xs text-muted-foreground hidden sm:block'> 描述您的创意,AI 为您创作精美图像 </p> </div> </div> {/* 上传图片区域 */} {uploadedImages.length > 0 && ( <div className='flex flex-wrap gap-1.5 sm:gap-2'> {uploadedImages.map((img, index) => ( <div key={index} className='relative group w-16 h-16 sm:w-20 sm:h-20 rounded-lg sm:rounded-xl overflow-hidden border-2 border-border hover:border-primary transition-all duration-300' > <Image src={img} alt={`上传 ${index + 1}`} fill className='object-cover' /> <button onClick={() => handleRemoveImage(index)} className={`absolute top-1 right-1 w-5 h-5 rounded-full bg-destructive/80 backdrop-blur-sm flex items-center justify-center transition-opacity duration-300 ${ isMobileDevice ? 'opacity-100' : 'opacity-0 group-hover:opacity-100' }`} > <X className='w-3 h-3 text-destructive-foreground' /> </button> </div> ))} </div> )} {/* 输入框 */} <div className='relative'> <textarea value={inputMessage} onChange={(e) => setInputMessage(e.target.value)} onKeyDown={(e) => { if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault() handleSendMessage() } }} placeholder='描述您想要创作的图像...' disabled={isDrawing} className='w-full px-3 sm:px-4 py-2.5 sm:py-3 rounded-xl sm:rounded-2xl bg-background/50 border-2 border-border focus:border-primary focus:outline-none resize-none transition-all duration-300 text-sm sm:text-base text-foreground placeholder:text-muted-foreground disabled:opacity-50 disabled:cursor-not-allowed min-h-[80px] sm:min-h-[100px]' rows={3} maxLength={2000} /> <div className='absolute bottom-2 sm:bottom-3 right-2 sm:right-3 text-xs text-muted-foreground bg-background/80 px-1.5 sm:px-2 py-0.5 sm:py-1 rounded'> {inputMessage.length}/2000 </div> </div> {/* 操作按钮 */} <div className='flex items-center gap-2 sm:gap-3'> <input ref={fileInputRef} type='file' accept='image/*' onChange={handleImageUpload} className='hidden' multiple={false} /> <button onClick={() => fileInputRef.current?.click()} disabled={isDrawing || uploadedImages.length >= 3} className='flex items-center gap-1.5 sm:gap-2 px-3 sm:px-4 py-2 sm:py-2.5 rounded-lg sm:rounded-xl bg-muted/50 hover:bg-muted border border-border hover:border-primary/50 text-muted-foreground hover:text-foreground transition-all duration-300 disabled:opacity-50 disabled:cursor-not-allowed' > <ImagePlus className='w-4 h-4' /> <span className='text-xs sm:text-sm font-medium hidden xs:inline'>上传</span> {uploadedImages.length > 0 && ( <span className='text-xs bg-primary/20 text-primary px-1.5 sm:px-2 py-0.5 rounded-full'> {uploadedImages.length}/3 </span> )} </button> <div className='flex-1' /> <button onClick={handleSendMessage} disabled={ (!inputMessage.trim() && uploadedImages.length === 0) || isDrawing } className='group relative overflow-hidden rounded-xl sm:rounded-2xl bg-linear-to-r from-primary to-secondary p-0.5 hover:shadow-xl hover:shadow-primary/25 transition-all duration-300 disabled:opacity-50 disabled:cursor-not-allowed disabled:hover:shadow-none hover:scale-105' > <div className='relative bg-card/90 backdrop-blur-sm rounded-xl sm:rounded-2xl px-4 sm:px-8 py-2 sm:py-3 group-hover:bg-transparent transition-all duration-300'> <div className='flex items-center gap-1.5 sm:gap-2 text-foreground group-hover:text-primary-foreground transition-colors duration-300'> {isDrawing ? ( <> <RefreshCw className='w-4 h-4 sm:w-5 sm:h-5 animate-spin' /> <span className='text-xs sm:text-base font-semibold'>创作中...</span> </> ) : ( <> <Send className='w-4 h-4 sm:w-5 sm:h-5' /> <span className='text-xs sm:text-base font-semibold'>开始创作</span> </> )} </div> </div> </button> </div> </div> </div> {/* 作品列表区域 - 低调展示 */} <div className='flex-1 overflow-hidden flex flex-col'> <div className='text-xs text-muted-foreground mb-1.5 sm:mb-2 px-1'> 创作历史 </div> <div ref={scrollContainerRef} className='flex-1 overflow-y-auto space-y-2 sm:space-y-3 pr-1 sm:pr-2 custom-scrollbar animate-fade-in-up' style={{ animationDelay: '0.2s' }} > {dataSource.map((item, index) => ( <div key={item.uid} className='group bg-card/40 backdrop-blur-sm rounded-xl sm:rounded-2xl border border-border/50 hover:border-border hover:bg-card/60 shadow-sm hover:shadow-md transition-all duration-300 p-2.5 sm:p-4' > <div className='flex gap-2 sm:gap-3'> {/* 图片区域 */} {(item.info.imgUrlCdn || item.info.visionImgList) && ( <div className='flex-shrink-0'> {/* Vision 图片列表 */} {Array.isArray(item.info.visionImgList) && item.info.visionImgList.length > 0 && ( <div className='grid grid-cols-2 gap-1.5 sm:gap-2'> {item.info.visionImgList.map((img, imgIndex) => ( <div key={imgIndex} className='relative w-16 h-16 sm:w-24 sm:h-24 rounded-lg sm:rounded-xl overflow-hidden border border-border hover:border-primary transition-all duration-300 cursor-pointer group/img' onClick={() => setPreviewImage(img.url)} > <Image src={img.url} alt={`Vision ${imgIndex + 1}`} fill className='object-cover' /> <div className='absolute inset-0 bg-black/0 group-hover/img:bg-black/40 transition-colors duration-300 flex items-center justify-center'> <Maximize2 className='w-6 h-6 text-white opacity-0 group-hover/img:opacity-100 transition-opacity duration-300' /> </div> {/* 操作按钮 - 移动端禁用点击避免阻挡预览 */} <div className='absolute bottom-1 right-1 flex gap-1 opacity-0 group-hover/img:opacity-100 transition-opacity duration-300 pointer-events-none sm:pointer-events-auto'> <button onClick={(e) => { e.stopPropagation() handleDownload(img.url) }} className='w-6 h-6 rounded-lg bg-primary/90 backdrop-blur-sm flex items-center justify-center hover:bg-primary transition-colors duration-300' > <Download className='w-3 h-3 text-primary-foreground' /> </button> <button onClick={(e) => { e.stopPropagation() handleCopy(img.url) }} className='w-6 h-6 rounded-lg bg-primary/90 backdrop-blur-sm flex items-center justify-center hover:bg-primary transition-colors duration-300' > {copiedText === img.url ? ( <Check className='w-3 h-3 text-primary-foreground' /> ) : ( <Copy className='w-3 h-3 text-primary-foreground' /> )} </button> </div> </div> ))} </div> )} {/* CDN 图片 */} {Array.isArray(item.info.imgUrlCdn) ? ( <div className='grid grid-cols-2 gap-1.5 sm:gap-2'> {item.info.imgUrlCdn.map((url, imgIndex) => ( <div key={imgIndex} className='relative w-16 h-16 sm:w-24 sm:h-24 rounded-lg sm:rounded-xl overflow-hidden border border-border hover:border-primary transition-all duration-300 cursor-pointer group/img' onClick={() => setPreviewImage(url)} > <Image src={url} alt={`图片 ${imgIndex + 1}`} fill className='object-cover' /> <div className='absolute inset-0 bg-black/0 group-hover/img:bg-black/40 transition-colors duration-300 flex items-center justify-center'> <Maximize2 className='w-6 h-6 text-white opacity-0 group-hover/img:opacity-100 transition-opacity duration-300' /> </div> <div className='absolute top-1 right-1 bg-black/60 backdrop-blur-sm text-white text-xs px-2 py-0.5 rounded-lg'> {imgIndex + 1} </div> {/* 操作按钮 - 移动端禁用点击避免阻挡预览 */} <div className='absolute bottom-1 right-1 flex gap-1 opacity-0 group-hover/img:opacity-100 transition-opacity duration-300 pointer-events-none sm:pointer-events-auto'> <button onClick={(e) => { e.stopPropagation() handleDownload(url) }} className='w-6 h-6 rounded-lg bg-primary/90 backdrop-blur-sm flex items-center justify-center hover:bg-primary transition-colors duration-300' > <Download className='w-3 h-3 text-primary-foreground' /> </button> <button onClick={(e) => { e.stopPropagation() handleCopy(url) }} className='w-6 h-6 rounded-lg bg-primary/90 backdrop-blur-sm flex items-center justify-center hover:bg-primary transition-colors duration-300' > {copiedText === url ? ( <Check className='w-3 h-3 text-primary-foreground' /> ) : ( <Copy className='w-3 h-3 text-primary-foreground' /> )} </button> </div> </div> ))} </div> ) : item.info.imgUrlCdn ? ( <div className='relative w-20 h-20 sm:w-28 sm:h-28 rounded-lg sm:rounded-xl overflow-hidden border border-border hover:border-primary transition-all duration-300 cursor-pointer group/img' onClick={() => setPreviewImage(item.info.imgUrlCdn as string) } > <Image src={item.info.imgUrlCdn} alt='图片' fill className='object-cover' /> <div className='absolute inset-0 bg-black/0 group-hover/img:bg-black/40 transition-colors duration-300 flex items-center justify-center'> <Maximize2 className='w-6 h-6 text-white opacity-0 group-hover/img:opacity-100 transition-opacity duration-300' /> </div> {/* 操作按钮 - 移动端禁用点击避免阻挡预览 */} <div className='absolute bottom-1 right-1 flex gap-1 opacity-0 group-hover/img:opacity-100 transition-opacity duration-300 pointer-events-none sm:pointer-events-auto'> <button onClick={(e) => { e.stopPropagation() handleDownload(item.info.imgUrlCdn as string) }} className='w-6 h-6 rounded-lg bg-primary/90 backdrop-blur-sm flex items-center justify-center hover:bg-primary transition-colors duration-300' > <Download className='w-3 h-3 text-primary-foreground' /> </button> <button onClick={(e) => { e.stopPropagation() handleCopy(item.info.imgUrlCdn as string) }} className='w-6 h-6 rounded-lg bg-primary/90 backdrop-blur-sm flex items-center justify-center hover:bg-primary transition-colors duration-300' > {copiedText === item.info.imgUrlCdn ? ( <Check className='w-3 h-3 text-primary-foreground' /> ) : ( <Copy className='w-3 h-3 text-primary-foreground' /> )} </button> </div> </div> ) : null} </div> )} {/* 文本内容区域 */} <div className='flex-1 min-w-0 space-y-1.5 sm:space-y-2'> {/* 提示词 */} {getPrompt(item) && ( <div className='space-y-0.5 sm:space-y-1'> <div className='text-xs text-muted-foreground'> 提示词 </div> <p className='text-xs sm:text-sm text-foreground/80 leading-relaxed line-clamp-2'> {getPrompt(item)} </p> <button onClick={() => handleCopy(getPrompt(item))} className='text-xs text-muted-foreground hover:text-foreground transition-colors duration-300 flex items-center gap-1' > {copiedText === getPrompt(item) ? ( <> <Check className='w-3 h-3' /> <span>已复制</span> </> ) : ( <> <Copy className='w-3 h-3' /> <span>复制</span> </> )} </button> </div> )} {/* AI 回复 */} {item.info?.aiMessage && ( <div className='space-y-0.5 sm:space-y-1'> <div className='text-xs text-muted-foreground flex items-center gap-1'> <Sparkles className='w-3 h-3' /> AI 回复 </div> <p className='text-xs sm:text-sm text-foreground/60 leading-relaxed line-clamp-2'> {item.info.aiMessage} </p> </div> )} {/* 时间戳 */} <div className='text-xs text-muted-foreground'> {new Date(Number(item.createTime)).toLocaleString( 'zh-CN', { month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit', } )} </div> </div> </div> </div> ))} {/* 加载更多按钮 */} {isHasMore && dataSource.length > 0 && ( <div className='flex justify-center py-3'> <button onClick={handleLoadMore} disabled={isLoading} className='flex items-center gap-2 px-4 py-2 rounded-xl bg-card/60 backdrop-blur-sm border border-border/50 hover:border-border text-muted-foreground hover:text-foreground transition-all duration-300 disabled:opacity-50 disabled:cursor-not-allowed text-sm' > <RefreshCw className={`w-4 h-4 transition-transform duration-300 ${ isLoading ? 'animate-spin' : '' }`} /> <span>{isLoading ? '加载中...' : '加载更多'}</span> </button> </div> )} {/* 空状态 */} {dataSource.length === 0 && !isLoading && ( <div className='flex flex-col items-center justify-center h-full space-y-3 opacity-40'> <Sparkles className='w-12 h-12 text-muted-foreground' /> <p className='text-muted-foreground text-sm'>暂无创作历史</p> </div> )} <div ref={messagesEndRef} /> </div> </div> </div> </main> {/* 图片预览弹窗 */} {previewImage && ( <div className='fixed inset-0 z-50 bg-black/80 backdrop-blur-sm flex items-center justify-center p-2 sm:p-4 animate-fade-in' onClick={() => setPreviewImage(null)} > <div className='relative max-w-4xl max-h-[90vh] w-full' onClick={(e) => e.stopPropagation()} > <Image src={previewImage} alt='预览' width={1200} height={1200} className='rounded-xl sm:rounded-2xl object-contain max-h-[85vh] w-full' /> <div className='absolute top-2 sm:top-4 right-2 sm:right-4 flex gap-1.5 sm:gap-2'> <button onClick={() => handleDownload(previewImage)} className='w-8 h-8 sm:w-10 sm:h-10 rounded-lg sm:rounded-xl bg-primary/90 backdrop-blur-sm flex items-center justify-center hover:bg-primary transition-colors duration-300' > <Download className='w-4 h-4 sm:w-5 sm:h-5 text-primary-foreground' /> </button> <button onClick={() => handleCopy(previewImage)} className='w-8 h-8 sm:w-10 sm:h-10 rounded-lg sm:rounded-xl bg-primary/90 backdrop-blur-sm flex items-center justify-center hover:bg-primary transition-colors duration-300' > {copiedText === previewImage ? ( <Check className='w-4 h-4 sm:w-5 sm:h-5 text-primary-foreground' /> ) : ( <Copy className='w-4 h-4 sm:w-5 sm:h-5 text-primary-foreground' /> )} </button> <button onClick={() => setPreviewImage(null)} className='w-8 h-8 sm:w-10 sm:h-10 rounded-lg sm:rounded-xl bg-destructive/90 backdrop-blur-sm flex items-center justify-center hover:bg-destructive transition-colors duration-300' > <X className='w-4 h-4 sm:w-5 sm:h-5 text-destructive-foreground' /> </button> </div> </div> </div> )} <style jsx global>{` .custom-scrollbar::-webkit-scrollbar { width: 4px; } @media (min-width: 640px) { .custom-scrollbar::-webkit-scrollbar { width: 6px; } } .custom-scrollbar::-webkit-scrollbar-track { background: transparent; } .custom-scrollbar::-webkit-scrollbar-thumb { background: hsl(var(--muted)); border-radius: 3px; } .custom-scrollbar::-webkit-scrollbar-thumb:hover { background: hsl(var(--muted-foreground) / 0.5); } `}</style> </> ) }