master
This commit is contained in:
98
my-app/src/components/AnimatedCounter.tsx
Normal file
98
my-app/src/components/AnimatedCounter.tsx
Normal file
@@ -0,0 +1,98 @@
|
||||
"use client";
|
||||
|
||||
import React, { useEffect, useRef, useState } from 'react';
|
||||
|
||||
interface AnimatedCounterProps {
|
||||
end: number;
|
||||
duration?: number;
|
||||
suffix?: string;
|
||||
prefix?: string;
|
||||
title: string;
|
||||
icon: React.ReactNode;
|
||||
}
|
||||
|
||||
export const AnimatedCounter: React.FC<AnimatedCounterProps> = ({
|
||||
end,
|
||||
duration = 2000,
|
||||
suffix = '',
|
||||
prefix = '',
|
||||
title,
|
||||
icon
|
||||
}) => {
|
||||
const [count, setCount] = useState(0);
|
||||
const [isVisible, setIsVisible] = useState(false);
|
||||
const ref = useRef<HTMLDivElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
const observer = new IntersectionObserver(
|
||||
([entry]) => {
|
||||
if (entry.isIntersecting && !isVisible) {
|
||||
setIsVisible(true);
|
||||
}
|
||||
},
|
||||
{ threshold: 0.3 }
|
||||
);
|
||||
|
||||
if (ref.current) {
|
||||
observer.observe(ref.current);
|
||||
}
|
||||
|
||||
return () => observer.disconnect();
|
||||
}, [isVisible]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isVisible) return;
|
||||
|
||||
let startTime: number;
|
||||
let animationFrame: number;
|
||||
|
||||
const animate = (timestamp: number) => {
|
||||
if (!startTime) startTime = timestamp;
|
||||
const progress = Math.min((timestamp - startTime) / duration, 1);
|
||||
|
||||
// Easing function for smooth animation
|
||||
const easeOutQuart = 1 - Math.pow(1 - progress, 4);
|
||||
|
||||
setCount(Math.floor(easeOutQuart * end));
|
||||
|
||||
if (progress < 1) {
|
||||
animationFrame = requestAnimationFrame(animate);
|
||||
}
|
||||
};
|
||||
|
||||
animationFrame = requestAnimationFrame(animate);
|
||||
|
||||
return () => cancelAnimationFrame(animationFrame);
|
||||
}, [isVisible, end, duration]);
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={ref}
|
||||
className="relative group p-6 bg-zsl-card/50 backdrop-blur-sm border border-zsl-primary/20 rounded-xl hover:border-zsl-primary/50 transition-all duration-300"
|
||||
>
|
||||
{/* Background Glow */}
|
||||
<div className="absolute inset-0 bg-zsl-primary/5 rounded-xl opacity-0 group-hover:opacity-100 transition-opacity duration-300" />
|
||||
|
||||
{/* Icon */}
|
||||
<div className="text-zsl-primary text-3xl mb-4 group-hover:scale-110 transition-transform duration-300">
|
||||
{icon}
|
||||
</div>
|
||||
|
||||
{/* Counter */}
|
||||
<div className="font-mono text-4xl md:text-5xl font-bold text-white mb-2">
|
||||
<span className="text-zsl-primary">{prefix}</span>
|
||||
{count.toLocaleString()}
|
||||
<span className="text-zsl-accent">{suffix}</span>
|
||||
</div>
|
||||
|
||||
{/* Title */}
|
||||
<div className="text-zsl-muted text-sm uppercase tracking-wider">
|
||||
{title}
|
||||
</div>
|
||||
|
||||
{/* Decorative Corner */}
|
||||
<div className="absolute top-0 right-0 w-8 h-8 border-t-2 border-r-2 border-zsl-primary/30 rounded-tr-xl" />
|
||||
<div className="absolute bottom-0 left-0 w-8 h-8 border-b-2 border-l-2 border-zsl-primary/30 rounded-bl-xl" />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
191
my-app/src/components/ChatInterface.tsx
Normal file
191
my-app/src/components/ChatInterface.tsx
Normal file
@@ -0,0 +1,191 @@
|
||||
import React, { useState, useRef, useEffect } from 'react';
|
||||
import { Message } from '@/types';
|
||||
import { streamChatResponse } from '@/services/geminiService';
|
||||
|
||||
const ChatInterface: React.FC = () => {
|
||||
const [input, setInput] = useState('');
|
||||
const [messages, setMessages] = useState<Message[]>([
|
||||
{
|
||||
id: 'welcome',
|
||||
role: 'model',
|
||||
text: "Sistem aktif. ZeroSixLab AI arayüzüne hoş geldiniz. Araştırmanız veya projeniz için nasıl yardımcı olabilirim?",
|
||||
timestamp: new Date(),
|
||||
}
|
||||
]);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [isInitialLoad, setIsInitialLoad] = useState(true);
|
||||
const messagesEndRef = useRef<HTMLDivElement>(null);
|
||||
const messagesContainerRef = useRef<HTMLDivElement>(null);
|
||||
const inputRef = useRef<HTMLInputElement>(null);
|
||||
|
||||
const scrollToBottom = () => {
|
||||
// Only scroll inside the messages container, not the whole page
|
||||
if (messagesContainerRef.current) {
|
||||
messagesContainerRef.current.scrollTop = messagesContainerRef.current.scrollHeight;
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
// Skip auto-scroll on initial load
|
||||
if (isInitialLoad) {
|
||||
setIsInitialLoad(false);
|
||||
return;
|
||||
}
|
||||
scrollToBottom();
|
||||
}, [messages, isInitialLoad]);
|
||||
|
||||
// Focus input on load (without scrolling page)
|
||||
useEffect(() => {
|
||||
inputRef.current?.focus({ preventScroll: true });
|
||||
}, []);
|
||||
|
||||
const handleSend = async () => {
|
||||
if (!input.trim() || isLoading) return;
|
||||
|
||||
const userMsg: Message = {
|
||||
id: Date.now().toString(),
|
||||
role: 'user',
|
||||
text: input,
|
||||
timestamp: new Date(),
|
||||
};
|
||||
|
||||
setMessages(prev => [...prev, userMsg]);
|
||||
setInput('');
|
||||
setIsLoading(true);
|
||||
|
||||
// Prepare history for API
|
||||
const history = messages.map(m => ({
|
||||
role: m.role,
|
||||
parts: [{ text: m.text }]
|
||||
}));
|
||||
|
||||
try {
|
||||
const stream = await streamChatResponse(userMsg.text, history);
|
||||
|
||||
const modelMsgId = (Date.now() + 1).toString();
|
||||
const modelMsg: Message = {
|
||||
id: modelMsgId,
|
||||
role: 'model',
|
||||
text: '',
|
||||
timestamp: new Date(),
|
||||
};
|
||||
|
||||
setMessages(prev => [...prev, modelMsg]);
|
||||
|
||||
let fullText = '';
|
||||
for await (const chunk of stream) {
|
||||
fullText += chunk;
|
||||
setMessages(prev =>
|
||||
prev.map(msg => msg.id === modelMsgId ? { ...msg, text: fullText } : msg)
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
setMessages(prev => [...prev, {
|
||||
id: Date.now().toString(),
|
||||
role: 'model',
|
||||
text: "Hata: Nöral bağlantı kararsız. Lütfen API anahtarını kontrol edin.",
|
||||
timestamp: new Date(),
|
||||
isError: true
|
||||
}]);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex flex-col h-162.5 w-full max-w-5xl mx-auto glass-panel rounded-xl shadow-2xl overflow-hidden relative border border-zsl-primary/20 group">
|
||||
{/* Decorative Corner Lines */}
|
||||
<div className="absolute top-0 left-0 w-8 h-8 border-t-2 border-l-2 border-zsl-primary/50 rounded-tl-xl z-20"></div>
|
||||
<div className="absolute top-0 right-0 w-8 h-8 border-t-2 border-r-2 border-zsl-primary/50 rounded-tr-xl z-20"></div>
|
||||
<div className="absolute bottom-0 left-0 w-8 h-8 border-b-2 border-l-2 border-zsl-primary/50 rounded-bl-xl z-20"></div>
|
||||
<div className="absolute bottom-0 right-0 w-8 h-8 border-b-2 border-r-2 border-zsl-primary/50 rounded-br-xl z-20"></div>
|
||||
|
||||
{/* HUD Header */}
|
||||
<div className="bg-black/40 p-3 border-b border-zsl-primary/20 flex justify-between items-center backdrop-blur-md z-10">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="relative">
|
||||
<div className="w-2.5 h-2.5 bg-zsl-accent rounded-full animate-pulse shadow-[0_0_10px_#FFAA00]"></div>
|
||||
<div className="absolute inset-0 bg-zsl-accent rounded-full animate-ping opacity-20"></div>
|
||||
</div>
|
||||
<span className="font-mono text-xs text-zsl-primary tracking-widest font-bold">CANLI_BAĞLANTI // GEMINI-2.5-FLASH</span>
|
||||
</div>
|
||||
<div className="font-mono text-[10px] text-zsl-muted flex gap-4 uppercase tracking-wider">
|
||||
<span className="hidden sm:inline">MEM: <span className="text-zsl-primary">32GB</span></span>
|
||||
<span className="hidden sm:inline">NET: <span className="text-green-400">SECURE</span></span>
|
||||
<span>LATENCY: <span className="text-zsl-primary">12ms</span></span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Background Effect inside Chat */}
|
||||
<div className="absolute inset-0 bg-[linear-gradient(rgba(10,25,47,0.8)_2px,transparent_2px)] bg-size-[100%_4px] pointer-events-none opacity-20"></div>
|
||||
|
||||
{/* Messages Area */}
|
||||
<div ref={messagesContainerRef} className="flex-1 overflow-y-auto p-6 space-y-6 scrollbar-thin scrollbar-thumb-zsl-card relative z-10">
|
||||
{messages.map((msg) => (
|
||||
<div key={msg.id} className={`flex ${msg.role === 'user' ? 'justify-end' : 'justify-start'}`}>
|
||||
<div className={`max-w-[85%] rounded-xl p-5 relative transition-all duration-300 ${
|
||||
msg.role === 'user'
|
||||
? 'bg-zsl-primary/10 border border-zsl-primary/30 text-white rounded-tr-none shadow-[0_0_15px_rgba(0,212,255,0.05)]'
|
||||
: 'bg-[#0f1d35] border border-slate-700/50 text-zsl-text rounded-tl-none shadow-lg'
|
||||
} ${msg.isError ? 'border-red-500/50 text-red-200 bg-red-900/10' : ''}`}>
|
||||
|
||||
<div className="font-mono text-[10px] uppercase opacity-60 mb-2 flex justify-between gap-4 border-b border-white/5 pb-1 select-none">
|
||||
<span className={`font-bold ${msg.role === 'user' ? 'text-zsl-primary' : 'text-zsl-accent'}`}>
|
||||
{msg.role === 'user' ? '>> OPERATOR' : '>> ZERO_AI'}
|
||||
</span>
|
||||
<span>{msg.timestamp.toLocaleTimeString([], {hour: '2-digit', minute:'2-digit'})}</span>
|
||||
</div>
|
||||
|
||||
<div className="whitespace-pre-wrap leading-relaxed text-sm md:text-base font-light tracking-wide">
|
||||
{msg.text}
|
||||
{msg.role === 'model' && msg.id === messages[messages.length - 1].id && isLoading && (
|
||||
<span className="inline-block w-2 h-4 bg-zsl-accent ml-1 animate-pulse align-middle"></span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Decorative side accent for model */}
|
||||
{msg.role === 'model' && (
|
||||
<div className="absolute left-0 top-6 w-0.5 h-8 bg-zsl-accent shadow-[0_0_10px_#FFAA00]"></div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
|
||||
{/* Loading Indicator */}
|
||||
{isLoading && messages[messages.length - 1].role === 'user' && (
|
||||
<div className="flex justify-start animate-in fade-in duration-300">
|
||||
<div className="bg-[#0f1d35] p-3 rounded-lg rounded-tl-none border border-slate-700/50 flex items-center gap-2">
|
||||
<span className="font-mono text-xs text-zsl-accent animate-pulse">PROCESSING...</span>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<div ref={messagesEndRef} />
|
||||
</div>
|
||||
|
||||
{/* Input Area */}
|
||||
<div className="p-4 bg-black/40 border-t border-zsl-primary/20 backdrop-blur relative z-10">
|
||||
<div className="relative flex items-center gap-3">
|
||||
<div className="absolute left-4 top-1/2 -translate-y-1/2 text-zsl-primary font-mono text-lg animate-pulse">{'>'}</div>
|
||||
<input
|
||||
ref={inputRef}
|
||||
type="text"
|
||||
value={input}
|
||||
onChange={(e) => setInput(e.target.value)}
|
||||
onKeyDown={(e) => e.key === 'Enter' && handleSend()}
|
||||
placeholder="Komut girişi yapın..."
|
||||
className="flex-1 bg-zsl-bg/50 border border-slate-700 focus:border-zsl-primary text-white pl-8 pr-5 py-4 rounded-lg font-mono text-sm outline-none transition-all placeholder-slate-600 focus:shadow-[0_0_15px_rgba(0,212,255,0.1)] focus:bg-zsl-bg/80"
|
||||
/>
|
||||
<button
|
||||
onClick={handleSend}
|
||||
disabled={isLoading || !input.trim()}
|
||||
className="bg-zsl-primary hover:bg-white text-black px-6 py-4 rounded-lg font-mono text-sm font-bold uppercase transition-all hover:shadow-[0_0_20px_rgba(0,212,255,0.4)] disabled:opacity-50 disabled:cursor-not-allowed group relative overflow-hidden active:scale-95"
|
||||
>
|
||||
<span className="relative z-10">GÖNDER</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ChatInterface;
|
||||
89
my-app/src/components/CookieBanner.tsx
Normal file
89
my-app/src/components/CookieBanner.tsx
Normal file
@@ -0,0 +1,89 @@
|
||||
"use client";
|
||||
|
||||
import React, { useState, useEffect } from 'react';
|
||||
|
||||
interface CookieBannerProps {
|
||||
onAccept?: () => void;
|
||||
onDecline?: () => void;
|
||||
}
|
||||
|
||||
export const CookieBanner: React.FC<CookieBannerProps> = ({ onAccept, onDecline }) => {
|
||||
const [isVisible, setIsVisible] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
// Check if user has already made a choice
|
||||
const consent = localStorage.getItem('cookie-consent');
|
||||
if (!consent) {
|
||||
// Small delay for better UX
|
||||
setTimeout(() => setIsVisible(true), 1500);
|
||||
}
|
||||
}, []);
|
||||
|
||||
const handleAccept = () => {
|
||||
localStorage.setItem('cookie-consent', 'accepted');
|
||||
setIsVisible(false);
|
||||
onAccept?.();
|
||||
};
|
||||
|
||||
const handleDecline = () => {
|
||||
localStorage.setItem('cookie-consent', 'declined');
|
||||
setIsVisible(false);
|
||||
onDecline?.();
|
||||
};
|
||||
|
||||
if (!isVisible) return null;
|
||||
|
||||
return (
|
||||
<div className="fixed bottom-0 left-0 right-0 z-50 p-4 md:p-6">
|
||||
<div className="max-w-4xl mx-auto bg-zsl-card/95 backdrop-blur-md border border-zsl-primary/20 rounded-2xl p-6 shadow-2xl shadow-black/50">
|
||||
<div className="flex flex-col md:flex-row items-start md:items-center gap-4">
|
||||
{/* Icon */}
|
||||
<div className="w-12 h-12 rounded-xl bg-zsl-primary/10 flex items-center justify-center shrink-0">
|
||||
<svg className="w-6 h-6 text-zsl-primary" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 15v2m-6 4h12a2 2 0 002-2v-6a2 2 0 00-2-2H6a2 2 0 00-2 2v6a2 2 0 002 2zm10-10V7a4 4 0 00-8 0v4h8z" />
|
||||
</svg>
|
||||
</div>
|
||||
|
||||
{/* Text */}
|
||||
<div className="flex-1">
|
||||
<h3 className="font-semibold text-white mb-1">🍪 Çerez Kullanımı & KVKK</h3>
|
||||
<p className="text-sm text-zsl-muted leading-relaxed">
|
||||
Web sitemizde deneyiminizi iyileştirmek için çerezler kullanıyoruz.
|
||||
Kişisel verileriniz 6698 sayılı KVKK kapsamında korunmaktadır.
|
||||
Devam ederek{' '}
|
||||
<a href="#" className="text-zsl-primary hover:underline">Gizlilik Politikamızı</a>
|
||||
{' '}kabul etmiş olursunuz.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Buttons */}
|
||||
<div className="flex gap-3 shrink-0 w-full md:w-auto">
|
||||
<button
|
||||
onClick={handleDecline}
|
||||
className="flex-1 md:flex-none px-5 py-2.5 text-sm font-medium text-zsl-muted border border-zsl-primary/20 rounded-lg hover:border-zsl-primary/40 hover:text-white transition-all duration-300"
|
||||
>
|
||||
Reddet
|
||||
</button>
|
||||
<button
|
||||
onClick={handleAccept}
|
||||
className="flex-1 md:flex-none px-5 py-2.5 text-sm font-medium bg-zsl-primary text-zsl-bg rounded-lg hover:bg-zsl-primary/90 transition-all duration-300"
|
||||
>
|
||||
Kabul Et
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Close button */}
|
||||
<button
|
||||
onClick={handleDecline}
|
||||
className="absolute top-3 right-3 w-8 h-8 rounded-full flex items-center justify-center text-zsl-muted hover:text-white hover:bg-zsl-primary/10 transition-all duration-300"
|
||||
aria-label="Kapat"
|
||||
>
|
||||
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
111
my-app/src/components/CustomCursor.tsx
Normal file
111
my-app/src/components/CustomCursor.tsx
Normal file
@@ -0,0 +1,111 @@
|
||||
"use client";
|
||||
|
||||
import React, { useEffect, useState } from 'react';
|
||||
|
||||
export const CustomCursor: React.FC = () => {
|
||||
const [position, setPosition] = useState({ x: 0, y: 0 });
|
||||
const [isPointer, setIsPointer] = useState(false);
|
||||
const [isClicking, setIsClicking] = useState(false);
|
||||
const [isVisible, setIsVisible] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
// Check if device supports hover (not touch)
|
||||
const hasHover = window.matchMedia('(hover: hover)').matches;
|
||||
if (!hasHover) return;
|
||||
|
||||
const handleMouseMove = (e: MouseEvent) => {
|
||||
setPosition({ x: e.clientX, y: e.clientY });
|
||||
setIsVisible(true);
|
||||
|
||||
const target = e.target as HTMLElement;
|
||||
const isClickable = !!(
|
||||
target.tagName === 'BUTTON' ||
|
||||
target.tagName === 'A' ||
|
||||
target.closest('button') ||
|
||||
target.closest('a') ||
|
||||
target.style.cursor === 'pointer' ||
|
||||
window.getComputedStyle(target).cursor === 'pointer'
|
||||
);
|
||||
|
||||
setIsPointer(isClickable);
|
||||
};
|
||||
|
||||
const handleMouseDown = () => setIsClicking(true);
|
||||
const handleMouseUp = () => setIsClicking(false);
|
||||
const handleMouseLeave = () => setIsVisible(false);
|
||||
const handleMouseEnter = () => setIsVisible(true);
|
||||
|
||||
document.addEventListener('mousemove', handleMouseMove);
|
||||
document.addEventListener('mousedown', handleMouseDown);
|
||||
document.addEventListener('mouseup', handleMouseUp);
|
||||
document.documentElement.addEventListener('mouseleave', handleMouseLeave);
|
||||
document.documentElement.addEventListener('mouseenter', handleMouseEnter);
|
||||
|
||||
// Hide default cursor
|
||||
document.body.style.cursor = 'none';
|
||||
document.querySelectorAll('a, button').forEach(el => {
|
||||
(el as HTMLElement).style.cursor = 'none';
|
||||
});
|
||||
|
||||
return () => {
|
||||
document.removeEventListener('mousemove', handleMouseMove);
|
||||
document.removeEventListener('mousedown', handleMouseDown);
|
||||
document.removeEventListener('mouseup', handleMouseUp);
|
||||
document.documentElement.removeEventListener('mouseleave', handleMouseLeave);
|
||||
document.documentElement.removeEventListener('mouseenter', handleMouseEnter);
|
||||
document.body.style.cursor = 'auto';
|
||||
};
|
||||
}, []);
|
||||
|
||||
if (!isVisible) return null;
|
||||
|
||||
return (
|
||||
<>
|
||||
{/* Main cursor dot */}
|
||||
<div
|
||||
className="fixed pointer-events-none z-9999 mix-blend-difference"
|
||||
style={{
|
||||
left: position.x,
|
||||
top: position.y,
|
||||
transform: 'translate(-50%, -50%)',
|
||||
}}
|
||||
>
|
||||
<div
|
||||
className={`
|
||||
rounded-full bg-zsl-primary transition-all duration-150 ease-out
|
||||
${isClicking ? 'w-2 h-2' : isPointer ? 'w-4 h-4' : 'w-3 h-3'}
|
||||
`}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Outer ring */}
|
||||
<div
|
||||
className="fixed pointer-events-none z-9998 transition-all duration-300 ease-out"
|
||||
style={{
|
||||
left: position.x,
|
||||
top: position.y,
|
||||
transform: 'translate(-50%, -50%)',
|
||||
}}
|
||||
>
|
||||
<div
|
||||
className={`
|
||||
rounded-full border-2 border-zsl-primary/50 transition-all duration-200 ease-out
|
||||
${isClicking ? 'w-6 h-6 border-zsl-accent' : isPointer ? 'w-10 h-10 border-zsl-accent' : 'w-8 h-8'}
|
||||
`}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Glow trail effect */}
|
||||
<div
|
||||
className="fixed pointer-events-none z-9997 transition-all duration-500 ease-out opacity-30"
|
||||
style={{
|
||||
left: position.x,
|
||||
top: position.y,
|
||||
transform: 'translate(-50%, -50%)',
|
||||
}}
|
||||
>
|
||||
<div className="w-16 h-16 rounded-full bg-zsl-primary/20 blur-xl" />
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
67
my-app/src/components/FAQAccordion.tsx
Normal file
67
my-app/src/components/FAQAccordion.tsx
Normal file
@@ -0,0 +1,67 @@
|
||||
"use client";
|
||||
|
||||
import React, { useState } from 'react';
|
||||
|
||||
interface FAQItem {
|
||||
question: string;
|
||||
answer: string;
|
||||
}
|
||||
|
||||
interface FAQAccordionProps {
|
||||
items: FAQItem[];
|
||||
}
|
||||
|
||||
export const FAQAccordion: React.FC<FAQAccordionProps> = ({ items }) => {
|
||||
const [openIndex, setOpenIndex] = useState<number | null>(0);
|
||||
|
||||
return (
|
||||
<div className="space-y-4 max-w-3xl mx-auto">
|
||||
{items.map((item, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className={`
|
||||
border rounded-xl overflow-hidden transition-all duration-300
|
||||
${openIndex === index
|
||||
? 'border-zsl-primary/50 bg-zsl-card/50'
|
||||
: 'border-zsl-primary/10 bg-zsl-card/30 hover:border-zsl-primary/30'
|
||||
}
|
||||
`}
|
||||
>
|
||||
{/* Question */}
|
||||
<button
|
||||
onClick={() => setOpenIndex(openIndex === index ? null : index)}
|
||||
className="w-full px-6 py-5 flex items-center justify-between text-left"
|
||||
>
|
||||
<span className="font-medium text-white pr-4">{item.question}</span>
|
||||
<div className={`
|
||||
w-8 h-8 rounded-full border border-zsl-primary/30 flex items-center justify-center
|
||||
transition-all duration-300 shrink-0
|
||||
${openIndex === index ? 'bg-zsl-primary rotate-180' : ''}
|
||||
`}>
|
||||
<svg
|
||||
className={`w-4 h-4 transition-colors duration-300 ${openIndex === index ? 'text-zsl-bg' : 'text-zsl-primary'}`}
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 9l-7 7-7-7" />
|
||||
</svg>
|
||||
</div>
|
||||
</button>
|
||||
|
||||
{/* Answer */}
|
||||
<div className={`
|
||||
grid transition-all duration-300 ease-in-out
|
||||
${openIndex === index ? 'grid-rows-[1fr]' : 'grid-rows-[0fr]'}
|
||||
`}>
|
||||
<div className="overflow-hidden">
|
||||
<div className="px-6 pb-5 text-zsl-muted leading-relaxed border-t border-zsl-primary/10 pt-4">
|
||||
{item.answer}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
44
my-app/src/components/FeatureCard.tsx
Normal file
44
my-app/src/components/FeatureCard.tsx
Normal file
@@ -0,0 +1,44 @@
|
||||
import React from 'react';
|
||||
import { FeatureCardProps } from '@/types';
|
||||
|
||||
export const FeatureCard: React.FC<FeatureCardProps> = ({ title, description, icon, accentColor }) => {
|
||||
const accentClass = accentColor === 'primary'
|
||||
? 'border-zsl-primary/20 text-zsl-primary group-hover:border-zsl-primary/50'
|
||||
: 'border-zsl-accent/20 text-zsl-accent group-hover:border-zsl-accent/50';
|
||||
|
||||
const bgGradient = accentColor === 'primary'
|
||||
? 'from-zsl-card to-zsl-bg group-hover:shadow-[0_0_30px_rgba(0,212,255,0.15)]'
|
||||
: 'from-zsl-card to-zsl-bg group-hover:shadow-[0_0_30px_rgba(255,170,0,0.15)]';
|
||||
|
||||
const iconBg = accentColor === 'primary' ? 'bg-zsl-primary/10' : 'bg-zsl-accent/10';
|
||||
const iconColor = accentColor === 'primary' ? 'text-zsl-primary' : 'text-zsl-accent';
|
||||
|
||||
return (
|
||||
<div className={`group p-6 rounded-xl border ${accentClass} bg-linear-to-br ${bgGradient} transition-all duration-300 transform hover:-translate-y-2 relative overflow-hidden h-full backdrop-blur-sm`}>
|
||||
{/* Tech Circuit Pattern Background (Visible on Hover) */}
|
||||
<div className="absolute inset-0 opacity-0 group-hover:opacity-10 transition-opacity duration-500 pointer-events-none">
|
||||
<svg className="w-full h-full" width="100%" height="100%">
|
||||
<pattern id={`circuit-${title.replace(/\s/g, '')}`} x="0" y="0" width="20" height="20" patternUnits="userSpaceOnUse">
|
||||
<path d="M10 0v20M0 10h20" stroke="currentColor" strokeWidth="0.5" fill="none" />
|
||||
</pattern>
|
||||
<rect width="100%" height="100%" fill={`url(#circuit-${title.replace(/\s/g, '')})`} />
|
||||
</svg>
|
||||
</div>
|
||||
|
||||
<div className="absolute top-0 right-0 p-3 opacity-20 group-hover:opacity-40 transition-opacity rotate-12 group-hover:rotate-0 duration-500">
|
||||
<svg width="60" height="60" viewBox="0 0 100 100" fill="none" className={accentColor === 'primary' ? 'stroke-zsl-primary' : 'stroke-zsl-accent'}>
|
||||
<circle cx="50" cy="50" r="40" strokeWidth="1" strokeDasharray="4 4" className="animate-spin-slow" />
|
||||
<path d="M50 10 L50 90 M10 50 L90 50" strokeWidth="0.5" />
|
||||
</svg>
|
||||
</div>
|
||||
|
||||
<div className="mb-6 relative z-10">
|
||||
<div className={`p-3 rounded-lg inline-flex items-center justify-center ${iconBg} ${iconColor} group-hover:scale-110 transition-all shadow-lg`}>
|
||||
{icon}
|
||||
</div>
|
||||
</div>
|
||||
<h3 className="text-xl font-mono font-bold text-white mb-3 relative z-10 group-hover:translate-x-1 transition-transform">{title}</h3>
|
||||
<p className="text-zsl-muted text-sm leading-relaxed relative z-10 group-hover:text-zsl-text transition-colors">{description}</p>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
80
my-app/src/components/LoadingScreen.tsx
Normal file
80
my-app/src/components/LoadingScreen.tsx
Normal file
@@ -0,0 +1,80 @@
|
||||
"use client";
|
||||
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { Logo } from './Logo';
|
||||
|
||||
export const LoadingScreen: React.FC = () => {
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [progress, setProgress] = useState(0);
|
||||
|
||||
useEffect(() => {
|
||||
const interval = setInterval(() => {
|
||||
setProgress(prev => {
|
||||
if (prev >= 100) {
|
||||
clearInterval(interval);
|
||||
setTimeout(() => setIsLoading(false), 300);
|
||||
return 100;
|
||||
}
|
||||
return prev + Math.random() * 15;
|
||||
});
|
||||
}, 100);
|
||||
|
||||
return () => clearInterval(interval);
|
||||
}, []);
|
||||
|
||||
if (!isLoading) return null;
|
||||
|
||||
return (
|
||||
<div className={`fixed inset-0 z-100 bg-zsl-bg flex flex-col items-center justify-center transition-opacity duration-500 ${progress >= 100 ? 'opacity-0' : 'opacity-100'}`}>
|
||||
{/* Cyber Grid Background */}
|
||||
<div className="absolute inset-0 cyber-grid opacity-20"></div>
|
||||
|
||||
{/* Floating Particles */}
|
||||
<div className="absolute inset-0 overflow-hidden">
|
||||
{[...Array(20)].map((_, i) => (
|
||||
<div
|
||||
key={i}
|
||||
className="absolute w-1 h-1 bg-zsl-primary/50 rounded-full animate-pulse"
|
||||
style={{
|
||||
left: `${Math.random() * 100}%`,
|
||||
top: `${Math.random() * 100}%`,
|
||||
animationDelay: `${Math.random() * 2}s`,
|
||||
animationDuration: `${2 + Math.random() * 3}s`
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Logo with Glow Animation */}
|
||||
<div className="relative mb-8 animate-pulse">
|
||||
<div className="absolute inset-0 bg-zsl-primary/20 rounded-full blur-3xl scale-150"></div>
|
||||
<Logo size="lg" />
|
||||
</div>
|
||||
|
||||
{/* Loading Text */}
|
||||
<div className="font-mono text-zsl-primary text-sm tracking-widest mb-6 uppercase">
|
||||
Sistem Başlatılıyor...
|
||||
</div>
|
||||
|
||||
{/* Progress Bar */}
|
||||
<div className="w-64 h-1 bg-zsl-card rounded-full overflow-hidden">
|
||||
<div
|
||||
className="h-full bg-linear-to-r from-zsl-primary to-zsl-accent transition-all duration-200 ease-out"
|
||||
style={{ width: `${Math.min(progress, 100)}%` }}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Progress Percentage */}
|
||||
<div className="font-mono text-xs text-zsl-muted mt-3">
|
||||
{Math.min(Math.round(progress), 100)}%
|
||||
</div>
|
||||
|
||||
{/* Terminal-style Messages */}
|
||||
<div className="absolute bottom-8 left-8 font-mono text-[10px] text-zsl-muted/50 hidden md:block">
|
||||
<div className="animate-pulse">> Modüller yükleniyor...</div>
|
||||
<div className="animate-pulse delay-100">> Bağlantı güvenli...</div>
|
||||
<div className="animate-pulse delay-200">> ZeroSixLab hazır.</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
46
my-app/src/components/Logo.tsx
Normal file
46
my-app/src/components/Logo.tsx
Normal file
@@ -0,0 +1,46 @@
|
||||
import React, { useState } from 'react';
|
||||
|
||||
export const Logo: React.FC<{ size?: 'sm' | 'md' | 'lg' }> = ({ size = 'md' }) => {
|
||||
const [imageError, setImageError] = useState(false);
|
||||
|
||||
const sizeClasses = {
|
||||
sm: 'h-8 w-8',
|
||||
md: 'h-12 w-12',
|
||||
lg: 'h-32 w-32',
|
||||
};
|
||||
|
||||
const textSizes = {
|
||||
sm: 'text-lg',
|
||||
md: 'text-2xl',
|
||||
lg: 'text-5xl',
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex items-center gap-3 select-none group">
|
||||
<div className={`relative rounded-full bg-linear-to-br from-zsl-card to-black p-0.5 border border-zsl-primary/30 group-hover:border-zsl-primary group-hover:shadow-[0_0_15px_rgba(0,212,255,0.4)] transition-all duration-300 ${sizeClasses[size]} overflow-hidden flex items-center justify-center`}>
|
||||
{/* Logo Image - Hidden if error */}
|
||||
{!imageError && (
|
||||
<img
|
||||
src="/assets/logo.png"
|
||||
alt="ZeroSixLab"
|
||||
className="w-full h-full object-cover rounded-full opacity-90 group-hover:opacity-100 transition-opacity"
|
||||
onError={() => setImageError(true)}
|
||||
/>
|
||||
)}
|
||||
{/* Fallback CSS Logo - Shown if image fails or not found */}
|
||||
{imageError && (
|
||||
<div className="absolute inset-0 bg-zsl-bg flex items-center justify-center">
|
||||
<div className="absolute left-[25%] top-[40%] w-[20%] h-[20%] rounded-full bg-zsl-primary shadow-[0_0_8px_#00D4FF] flex items-center justify-center text-black font-bold text-[60%] leading-none">0</div>
|
||||
<div className="absolute right-[25%] top-[40%] w-[20%] h-[20%] rounded-full bg-zsl-accent shadow-[0_0_8px_#FFAA00] flex items-center justify-center text-black font-bold text-[60%] leading-none">6</div>
|
||||
<div className="w-[80%] h-[80%] border-2 border-zsl-text/80 rounded-full opacity-30"></div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className={`font-mono font-bold tracking-wider ${textSizes[size]}`}>
|
||||
<span className="text-white">Zero</span>
|
||||
<span className="text-zsl-primary">Six</span>
|
||||
<span className="text-white">Lab</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
142
my-app/src/components/ParticleBackground.tsx
Normal file
142
my-app/src/components/ParticleBackground.tsx
Normal file
@@ -0,0 +1,142 @@
|
||||
"use client";
|
||||
|
||||
import React, { useEffect, useRef, useState } from 'react';
|
||||
|
||||
interface Particle {
|
||||
id: number;
|
||||
x: number;
|
||||
y: number;
|
||||
size: number;
|
||||
speedX: number;
|
||||
speedY: number;
|
||||
opacity: number;
|
||||
color: string;
|
||||
}
|
||||
|
||||
export const ParticleBackground: React.FC = () => {
|
||||
const canvasRef = useRef<HTMLCanvasElement>(null);
|
||||
const [dimensions, setDimensions] = useState({ width: 0, height: 0 });
|
||||
const particlesRef = useRef<Particle[]>([]);
|
||||
const animationRef = useRef<number | undefined>(undefined);
|
||||
const mouseRef = useRef({ x: 0, y: 0 });
|
||||
|
||||
useEffect(() => {
|
||||
const handleResize = () => {
|
||||
setDimensions({
|
||||
width: window.innerWidth,
|
||||
height: window.innerHeight
|
||||
});
|
||||
};
|
||||
|
||||
const handleMouseMove = (e: MouseEvent) => {
|
||||
mouseRef.current = { x: e.clientX, y: e.clientY };
|
||||
};
|
||||
|
||||
handleResize();
|
||||
window.addEventListener('resize', handleResize);
|
||||
window.addEventListener('mousemove', handleMouseMove);
|
||||
|
||||
return () => {
|
||||
window.removeEventListener('resize', handleResize);
|
||||
window.removeEventListener('mousemove', handleMouseMove);
|
||||
};
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
const canvas = canvasRef.current;
|
||||
if (!canvas) return;
|
||||
|
||||
const ctx = canvas.getContext('2d');
|
||||
if (!ctx) return;
|
||||
|
||||
canvas.width = dimensions.width;
|
||||
canvas.height = dimensions.height;
|
||||
|
||||
// Initialize particles
|
||||
const particleCount = Math.floor((dimensions.width * dimensions.height) / 15000);
|
||||
particlesRef.current = Array.from({ length: particleCount }, (_, i) => ({
|
||||
id: i,
|
||||
x: Math.random() * dimensions.width,
|
||||
y: Math.random() * dimensions.height,
|
||||
size: Math.random() * 2 + 0.5,
|
||||
speedX: (Math.random() - 0.5) * 0.5,
|
||||
speedY: (Math.random() - 0.5) * 0.5,
|
||||
opacity: Math.random() * 0.5 + 0.2,
|
||||
color: Math.random() > 0.7 ? '#FFAA00' : '#00D4FF'
|
||||
}));
|
||||
|
||||
const animate = () => {
|
||||
ctx.clearRect(0, 0, dimensions.width, dimensions.height);
|
||||
|
||||
particlesRef.current.forEach((particle, index) => {
|
||||
// Update position
|
||||
particle.x += particle.speedX;
|
||||
particle.y += particle.speedY;
|
||||
|
||||
// Mouse interaction - particles move away from cursor
|
||||
const dx = mouseRef.current.x - particle.x;
|
||||
const dy = mouseRef.current.y - particle.y;
|
||||
const distance = Math.sqrt(dx * dx + dy * dy);
|
||||
|
||||
if (distance < 100) {
|
||||
const force = (100 - distance) / 100;
|
||||
particle.x -= (dx / distance) * force * 2;
|
||||
particle.y -= (dy / distance) * force * 2;
|
||||
}
|
||||
|
||||
// Wrap around screen
|
||||
if (particle.x < 0) particle.x = dimensions.width;
|
||||
if (particle.x > dimensions.width) particle.x = 0;
|
||||
if (particle.y < 0) particle.y = dimensions.height;
|
||||
if (particle.y > dimensions.height) particle.y = 0;
|
||||
|
||||
// Draw particle
|
||||
ctx.beginPath();
|
||||
ctx.arc(particle.x, particle.y, particle.size, 0, Math.PI * 2);
|
||||
ctx.fillStyle = particle.color.replace(')', `, ${particle.opacity})`).replace('rgb', 'rgba').replace('#', '');
|
||||
|
||||
// Convert hex to rgba
|
||||
const hex = particle.color;
|
||||
const r = parseInt(hex.slice(1, 3), 16);
|
||||
const g = parseInt(hex.slice(3, 5), 16);
|
||||
const b = parseInt(hex.slice(5, 7), 16);
|
||||
ctx.fillStyle = `rgba(${r}, ${g}, ${b}, ${particle.opacity})`;
|
||||
ctx.fill();
|
||||
|
||||
// Draw connections between nearby particles
|
||||
particlesRef.current.slice(index + 1).forEach(otherParticle => {
|
||||
const dx = particle.x - otherParticle.x;
|
||||
const dy = particle.y - otherParticle.y;
|
||||
const distance = Math.sqrt(dx * dx + dy * dy);
|
||||
|
||||
if (distance < 120) {
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(particle.x, particle.y);
|
||||
ctx.lineTo(otherParticle.x, otherParticle.y);
|
||||
ctx.strokeStyle = `rgba(0, 212, 255, ${0.1 * (1 - distance / 120)})`;
|
||||
ctx.lineWidth = 0.5;
|
||||
ctx.stroke();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
animationRef.current = requestAnimationFrame(animate);
|
||||
};
|
||||
|
||||
animate();
|
||||
|
||||
return () => {
|
||||
if (animationRef.current) {
|
||||
cancelAnimationFrame(animationRef.current);
|
||||
}
|
||||
};
|
||||
}, [dimensions]);
|
||||
|
||||
return (
|
||||
<canvas
|
||||
ref={canvasRef}
|
||||
className="fixed inset-0 pointer-events-none z-0"
|
||||
style={{ background: 'transparent' }}
|
||||
/>
|
||||
);
|
||||
};
|
||||
67
my-app/src/components/PartnerLogos.tsx
Normal file
67
my-app/src/components/PartnerLogos.tsx
Normal file
@@ -0,0 +1,67 @@
|
||||
"use client";
|
||||
|
||||
import React from 'react';
|
||||
|
||||
interface Partner {
|
||||
name: string;
|
||||
logo?: string;
|
||||
}
|
||||
|
||||
interface PartnerLogosProps {
|
||||
partners: Partner[];
|
||||
}
|
||||
|
||||
export const PartnerLogos: React.FC<PartnerLogosProps> = ({ partners }) => {
|
||||
// Duplicate for seamless infinite scroll
|
||||
const duplicatedPartners = [...partners, ...partners];
|
||||
|
||||
return (
|
||||
<div className="relative overflow-hidden py-8">
|
||||
{/* Gradient masks */}
|
||||
<div className="absolute left-0 top-0 bottom-0 w-32 bg-linear-to-r from-zsl-bg to-transparent z-10" />
|
||||
<div className="absolute right-0 top-0 bottom-0 w-32 bg-linear-to-l from-zsl-bg to-transparent z-10" />
|
||||
|
||||
{/* Scrolling container */}
|
||||
<div className="flex animate-scroll">
|
||||
{duplicatedPartners.map((partner, index) => (
|
||||
<div
|
||||
key={`${partner.name}-${index}`}
|
||||
className="shrink-0 mx-8 md:mx-12"
|
||||
>
|
||||
<div className="w-32 h-16 md:w-40 md:h-20 flex items-center justify-center bg-zsl-card/30 border border-zsl-primary/10 rounded-lg hover:border-zsl-primary/30 transition-all duration-300 group px-4">
|
||||
{partner.logo ? (
|
||||
<img
|
||||
src={partner.logo}
|
||||
alt={partner.name}
|
||||
className="max-w-full max-h-full object-contain opacity-50 group-hover:opacity-100 transition-opacity duration-300 filter grayscale group-hover:grayscale-0"
|
||||
/>
|
||||
) : (
|
||||
<span className="text-sm font-medium text-zsl-muted/50 group-hover:text-zsl-primary transition-colors duration-300 text-center">
|
||||
{partner.name}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Add CSS animation */}
|
||||
<style jsx>{`
|
||||
@keyframes scroll {
|
||||
0% {
|
||||
transform: translateX(0);
|
||||
}
|
||||
100% {
|
||||
transform: translateX(-50%);
|
||||
}
|
||||
}
|
||||
.animate-scroll {
|
||||
animation: scroll 30s linear infinite;
|
||||
}
|
||||
.animate-scroll:hover {
|
||||
animation-play-state: paused;
|
||||
}
|
||||
`}</style>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
175
my-app/src/components/ProjectModal.tsx
Normal file
175
my-app/src/components/ProjectModal.tsx
Normal file
@@ -0,0 +1,175 @@
|
||||
"use client";
|
||||
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import Image from 'next/image';
|
||||
|
||||
interface Project {
|
||||
id: string;
|
||||
title: string;
|
||||
description: string;
|
||||
longDescription?: string;
|
||||
image: string;
|
||||
technologies: string[];
|
||||
category: string;
|
||||
link?: string;
|
||||
github?: string;
|
||||
features?: string[];
|
||||
}
|
||||
|
||||
interface ProjectModalProps {
|
||||
project: Project | null;
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
export const ProjectModal: React.FC<ProjectModalProps> = ({ project, isOpen, onClose }) => {
|
||||
const [isAnimating, setIsAnimating] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (isOpen) {
|
||||
setIsAnimating(true);
|
||||
document.body.style.overflow = 'hidden';
|
||||
} else {
|
||||
document.body.style.overflow = 'auto';
|
||||
}
|
||||
|
||||
return () => {
|
||||
document.body.style.overflow = 'auto';
|
||||
};
|
||||
}, [isOpen]);
|
||||
|
||||
useEffect(() => {
|
||||
const handleKeyDown = (e: KeyboardEvent) => {
|
||||
if (e.key === 'Escape' && isOpen) {
|
||||
onClose();
|
||||
}
|
||||
};
|
||||
|
||||
window.addEventListener('keydown', handleKeyDown);
|
||||
return () => window.removeEventListener('keydown', handleKeyDown);
|
||||
}, [isOpen, onClose]);
|
||||
|
||||
if (!isOpen || !project) return null;
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`fixed inset-0 z-50 flex items-center justify-center p-4 transition-all duration-300 ${isAnimating ? 'opacity-100' : 'opacity-0'}`}
|
||||
onClick={onClose}
|
||||
>
|
||||
{/* Backdrop */}
|
||||
<div className="absolute inset-0 bg-black/80 backdrop-blur-sm" />
|
||||
|
||||
{/* Modal Content */}
|
||||
<div
|
||||
className={`relative bg-zsl-card border border-zsl-primary/20 rounded-2xl max-w-4xl w-full max-h-[90vh] overflow-hidden transition-all duration-300 ${isAnimating ? 'scale-100 translate-y-0' : 'scale-95 translate-y-8'}`}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
{/* Close button */}
|
||||
<button
|
||||
onClick={onClose}
|
||||
className="absolute top-4 right-4 z-10 w-10 h-10 rounded-full bg-zsl-bg/80 backdrop-blur-sm border border-zsl-primary/20 flex items-center justify-center text-zsl-muted hover:text-white hover:border-zsl-primary transition-all duration-300"
|
||||
aria-label="Kapat"
|
||||
>
|
||||
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
|
||||
</svg>
|
||||
</button>
|
||||
|
||||
{/* Scrollable content */}
|
||||
<div className="overflow-y-auto max-h-[90vh]">
|
||||
{/* Hero Image */}
|
||||
<div className="relative h-64 md:h-80 bg-zsl-bg">
|
||||
<div className="absolute inset-0 bg-linear-to-t from-zsl-card to-transparent z-10" />
|
||||
<div className="w-full h-full bg-linear-to-br from-zsl-primary/20 to-zsl-accent/20 flex items-center justify-center">
|
||||
<span className="text-6xl">🚀</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Content */}
|
||||
<div className="p-6 md:p-8 -mt-16 relative z-20">
|
||||
{/* Category Badge */}
|
||||
<span className="inline-block px-3 py-1 text-xs font-medium bg-zsl-primary/20 text-zsl-primary rounded-full mb-4">
|
||||
{project.category}
|
||||
</span>
|
||||
|
||||
{/* Title */}
|
||||
<h2 className="text-2xl md:text-3xl font-bold text-white mb-4">
|
||||
{project.title}
|
||||
</h2>
|
||||
|
||||
{/* Description */}
|
||||
<p className="text-zsl-muted leading-relaxed mb-6">
|
||||
{project.longDescription || project.description}
|
||||
</p>
|
||||
|
||||
{/* Features */}
|
||||
{project.features && project.features.length > 0 && (
|
||||
<div className="mb-6">
|
||||
<h3 className="text-lg font-semibold text-white mb-3">Özellikler</h3>
|
||||
<ul className="grid grid-cols-1 md:grid-cols-2 gap-2">
|
||||
{project.features.map((feature, index) => (
|
||||
<li key={index} className="flex items-center gap-2 text-zsl-muted">
|
||||
<svg className="w-4 h-4 text-zsl-primary shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 13l4 4L19 7" />
|
||||
</svg>
|
||||
{feature}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Technologies */}
|
||||
<div className="mb-6">
|
||||
<h3 className="text-lg font-semibold text-white mb-3">Teknolojiler</h3>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{project.technologies.map((tech, index) => (
|
||||
<span
|
||||
key={index}
|
||||
className="px-3 py-1.5 text-sm bg-zsl-bg border border-zsl-primary/20 text-zsl-muted rounded-lg"
|
||||
>
|
||||
{tech}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Action buttons */}
|
||||
<div className="flex flex-wrap gap-4 pt-4 border-t border-zsl-primary/10">
|
||||
{project.link && (
|
||||
<a
|
||||
href={project.link}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="flex items-center gap-2 px-6 py-3 bg-zsl-primary text-zsl-bg font-medium rounded-lg hover:bg-zsl-primary/90 transition-all duration-300"
|
||||
>
|
||||
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M10 6H6a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2v-4M14 4h6m0 0v6m0-6L10 14" />
|
||||
</svg>
|
||||
Canlı Demo
|
||||
</a>
|
||||
)}
|
||||
{project.github && (
|
||||
<a
|
||||
href={project.github}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="flex items-center gap-2 px-6 py-3 border border-zsl-primary/30 text-zsl-primary font-medium rounded-lg hover:bg-zsl-primary/10 transition-all duration-300"
|
||||
>
|
||||
<svg className="w-5 h-5" fill="currentColor" viewBox="0 0 24 24">
|
||||
<path fillRule="evenodd" d="M12 2C6.477 2 2 6.484 2 12.017c0 4.425 2.865 8.18 6.839 9.504.5.092.682-.217.682-.483 0-.237-.008-.868-.013-1.703-2.782.605-3.369-1.343-3.369-1.343-.454-1.158-1.11-1.466-1.11-1.466-.908-.62.069-.608.069-.608 1.003.07 1.531 1.032 1.531 1.032.892 1.53 2.341 1.088 2.91.832.092-.647.35-1.088.636-1.338-2.22-.253-4.555-1.113-4.555-4.951 0-1.093.39-1.988 1.029-2.688-.103-.253-.446-1.272.098-2.65 0 0 .84-.27 2.75 1.026A9.564 9.564 0 0112 6.844c.85.004 1.705.115 2.504.337 1.909-1.296 2.747-1.027 2.747-1.027.546 1.379.202 2.398.1 2.651.64.7 1.028 1.595 1.028 2.688 0 3.848-2.339 4.695-4.566 4.943.359.309.678.92.678 1.855 0 1.338-.012 2.419-.012 2.747 0 .268.18.58.688.482A10.019 10.019 0 0022 12.017C22 6.484 17.522 2 12 2z" clipRule="evenodd" />
|
||||
</svg>
|
||||
GitHub
|
||||
</a>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Decorative corners */}
|
||||
<div className="absolute top-0 left-0 w-16 h-16 border-t-2 border-l-2 border-zsl-primary/30 rounded-tl-2xl pointer-events-none" />
|
||||
<div className="absolute bottom-0 right-0 w-16 h-16 border-b-2 border-r-2 border-zsl-primary/30 rounded-br-2xl pointer-events-none" />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
101
my-app/src/components/ScrollAnimation.tsx
Normal file
101
my-app/src/components/ScrollAnimation.tsx
Normal file
@@ -0,0 +1,101 @@
|
||||
"use client";
|
||||
|
||||
import React, { useEffect, useState, useRef, ReactNode } from 'react';
|
||||
|
||||
interface ScrollAnimationProps {
|
||||
children: ReactNode;
|
||||
animation?: 'fade-up' | 'fade-down' | 'fade-left' | 'fade-right' | 'zoom' | 'flip';
|
||||
delay?: number;
|
||||
duration?: number;
|
||||
threshold?: number;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export const ScrollAnimation: React.FC<ScrollAnimationProps> = ({
|
||||
children,
|
||||
animation = 'fade-up',
|
||||
delay = 0,
|
||||
duration = 600,
|
||||
threshold = 0.1,
|
||||
className = ''
|
||||
}) => {
|
||||
const [isVisible, setIsVisible] = useState(false);
|
||||
const ref = useRef<HTMLDivElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
const observer = new IntersectionObserver(
|
||||
([entry]) => {
|
||||
if (entry.isIntersecting) {
|
||||
setIsVisible(true);
|
||||
observer.unobserve(entry.target);
|
||||
}
|
||||
},
|
||||
{ threshold }
|
||||
);
|
||||
|
||||
if (ref.current) {
|
||||
observer.observe(ref.current);
|
||||
}
|
||||
|
||||
return () => observer.disconnect();
|
||||
}, [threshold]);
|
||||
|
||||
const getAnimationStyles = () => {
|
||||
const baseStyles = {
|
||||
transition: `all ${duration}ms cubic-bezier(0.4, 0, 0.2, 1) ${delay}ms`,
|
||||
};
|
||||
|
||||
if (!isVisible) {
|
||||
switch (animation) {
|
||||
case 'fade-up':
|
||||
return { ...baseStyles, opacity: 0, transform: 'translateY(40px)' };
|
||||
case 'fade-down':
|
||||
return { ...baseStyles, opacity: 0, transform: 'translateY(-40px)' };
|
||||
case 'fade-left':
|
||||
return { ...baseStyles, opacity: 0, transform: 'translateX(-40px)' };
|
||||
case 'fade-right':
|
||||
return { ...baseStyles, opacity: 0, transform: 'translateX(40px)' };
|
||||
case 'zoom':
|
||||
return { ...baseStyles, opacity: 0, transform: 'scale(0.8)' };
|
||||
case 'flip':
|
||||
return { ...baseStyles, opacity: 0, transform: 'perspective(1000px) rotateY(90deg)' };
|
||||
default:
|
||||
return { ...baseStyles, opacity: 0 };
|
||||
}
|
||||
}
|
||||
|
||||
return { ...baseStyles, opacity: 1, transform: 'none' };
|
||||
};
|
||||
|
||||
return (
|
||||
<div ref={ref} style={getAnimationStyles()} className={className}>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
// Hook for custom scroll animations
|
||||
export const useScrollAnimation = (threshold = 0.1) => {
|
||||
const [isVisible, setIsVisible] = useState(false);
|
||||
const ref = useRef<HTMLDivElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
const observer = new IntersectionObserver(
|
||||
([entry]) => {
|
||||
if (entry.isIntersecting) {
|
||||
setIsVisible(true);
|
||||
observer.unobserve(entry.target);
|
||||
}
|
||||
},
|
||||
{ threshold }
|
||||
);
|
||||
|
||||
if (ref.current) {
|
||||
observer.observe(ref.current);
|
||||
}
|
||||
|
||||
return () => observer.disconnect();
|
||||
}, [threshold]);
|
||||
|
||||
return { ref, isVisible };
|
||||
};
|
||||
41
my-app/src/components/ScrollToTop.tsx
Normal file
41
my-app/src/components/ScrollToTop.tsx
Normal file
@@ -0,0 +1,41 @@
|
||||
"use client";
|
||||
|
||||
import React, { useState, useEffect } from 'react';
|
||||
|
||||
export const ScrollToTop: React.FC = () => {
|
||||
const [isVisible, setIsVisible] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
const toggleVisibility = () => {
|
||||
if (window.scrollY > 300) {
|
||||
setIsVisible(true);
|
||||
} else {
|
||||
setIsVisible(false);
|
||||
}
|
||||
};
|
||||
|
||||
window.addEventListener('scroll', toggleVisibility);
|
||||
return () => window.removeEventListener('scroll', toggleVisibility);
|
||||
}, []);
|
||||
|
||||
const scrollToTop = () => {
|
||||
window.scrollTo({
|
||||
top: 0,
|
||||
behavior: 'smooth'
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<button
|
||||
onClick={scrollToTop}
|
||||
className={`fixed bottom-8 right-8 z-50 p-4 rounded-full bg-zsl-primary text-black shadow-[0_0_20px_rgba(0,212,255,0.4)] hover:bg-white hover:shadow-[0_0_30px_rgba(255,255,255,0.5)] transition-all duration-300 ${
|
||||
isVisible ? 'opacity-100 translate-y-0' : 'opacity-0 translate-y-10 pointer-events-none'
|
||||
}`}
|
||||
aria-label="Yukarı kaydır"
|
||||
>
|
||||
<svg className="w-6 h-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 10l7-7m0 0l7 7m-7-7v18" />
|
||||
</svg>
|
||||
</button>
|
||||
);
|
||||
};
|
||||
145
my-app/src/components/TestimonialSlider.tsx
Normal file
145
my-app/src/components/TestimonialSlider.tsx
Normal file
@@ -0,0 +1,145 @@
|
||||
"use client";
|
||||
|
||||
import React, { useState, useEffect, useCallback } from 'react';
|
||||
|
||||
interface Testimonial {
|
||||
id: number;
|
||||
name: string;
|
||||
role: string;
|
||||
company: string;
|
||||
content: string;
|
||||
avatar?: string;
|
||||
rating: number;
|
||||
}
|
||||
|
||||
interface TestimonialSliderProps {
|
||||
testimonials: Testimonial[];
|
||||
autoPlayInterval?: number;
|
||||
}
|
||||
|
||||
export const TestimonialSlider: React.FC<TestimonialSliderProps> = ({
|
||||
testimonials,
|
||||
autoPlayInterval = 5000
|
||||
}) => {
|
||||
const [currentIndex, setCurrentIndex] = useState(0);
|
||||
const [isAnimating, setIsAnimating] = useState(false);
|
||||
|
||||
const goToNext = useCallback(() => {
|
||||
if (isAnimating) return;
|
||||
setIsAnimating(true);
|
||||
setCurrentIndex((prev) => (prev + 1) % testimonials.length);
|
||||
setTimeout(() => setIsAnimating(false), 500);
|
||||
}, [isAnimating, testimonials.length]);
|
||||
|
||||
const goToPrev = useCallback(() => {
|
||||
if (isAnimating) return;
|
||||
setIsAnimating(true);
|
||||
setCurrentIndex((prev) => (prev - 1 + testimonials.length) % testimonials.length);
|
||||
setTimeout(() => setIsAnimating(false), 500);
|
||||
}, [isAnimating, testimonials.length]);
|
||||
|
||||
useEffect(() => {
|
||||
const interval = setInterval(goToNext, autoPlayInterval);
|
||||
return () => clearInterval(interval);
|
||||
}, [goToNext, autoPlayInterval]);
|
||||
|
||||
const currentTestimonial = testimonials[currentIndex];
|
||||
|
||||
return (
|
||||
<div className="relative max-w-4xl mx-auto">
|
||||
{/* Background decoration */}
|
||||
<div className="absolute -top-4 -left-4 text-8xl text-zsl-primary/10 font-serif select-none">"</div>
|
||||
<div className="absolute -bottom-4 -right-4 text-8xl text-zsl-primary/10 font-serif rotate-180 select-none">"</div>
|
||||
|
||||
{/* Main card */}
|
||||
<div className="relative bg-zsl-card/80 backdrop-blur-md border border-zsl-primary/20 rounded-2xl p-8 md:p-12">
|
||||
{/* Content */}
|
||||
<div className={`transition-all duration-500 ${isAnimating ? 'opacity-0 translate-y-4' : 'opacity-100 translate-y-0'}`}>
|
||||
{/* Stars */}
|
||||
<div className="flex gap-1 mb-6">
|
||||
{[...Array(5)].map((_, i) => (
|
||||
<svg
|
||||
key={i}
|
||||
className={`w-5 h-5 ${i < currentTestimonial.rating ? 'text-zsl-accent' : 'text-zsl-muted/30'}`}
|
||||
fill="currentColor"
|
||||
viewBox="0 0 20 20"
|
||||
>
|
||||
<path d="M9.049 2.927c.3-.921 1.603-.921 1.902 0l1.07 3.292a1 1 0 00.95.69h3.462c.969 0 1.371 1.24.588 1.81l-2.8 2.034a1 1 0 00-.364 1.118l1.07 3.292c.3.921-.755 1.688-1.54 1.118l-2.8-2.034a1 1 0 00-1.175 0l-2.8 2.034c-.784.57-1.838-.197-1.539-1.118l1.07-3.292a1 1 0 00-.364-1.118L2.98 8.72c-.783-.57-.38-1.81.588-1.81h3.461a1 1 0 00.951-.69l1.07-3.292z" />
|
||||
</svg>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Quote */}
|
||||
<p className="text-lg md:text-xl text-white leading-relaxed mb-8">
|
||||
"{currentTestimonial.content}"
|
||||
</p>
|
||||
|
||||
{/* Author */}
|
||||
<div className="flex items-center gap-4">
|
||||
{/* Avatar */}
|
||||
<div className="w-14 h-14 rounded-full bg-linear-to-br from-zsl-primary to-zsl-accent p-0.5">
|
||||
<div className="w-full h-full rounded-full bg-zsl-card flex items-center justify-center text-xl font-bold text-zsl-primary">
|
||||
{currentTestimonial.name.charAt(0)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Info */}
|
||||
<div>
|
||||
<div className="font-semibold text-white">{currentTestimonial.name}</div>
|
||||
<div className="text-sm text-zsl-muted">
|
||||
{currentTestimonial.role} • {currentTestimonial.company}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Navigation */}
|
||||
<div className="flex items-center justify-between mt-8 pt-6 border-t border-zsl-primary/10">
|
||||
{/* Arrows */}
|
||||
<div className="flex gap-2">
|
||||
<button
|
||||
onClick={goToPrev}
|
||||
className="w-10 h-10 rounded-full border border-zsl-primary/30 flex items-center justify-center text-zsl-primary hover:bg-zsl-primary hover:text-zsl-bg transition-all duration-300"
|
||||
aria-label="Previous testimonial"
|
||||
>
|
||||
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 19l-7-7 7-7" />
|
||||
</svg>
|
||||
</button>
|
||||
<button
|
||||
onClick={goToNext}
|
||||
className="w-10 h-10 rounded-full border border-zsl-primary/30 flex items-center justify-center text-zsl-primary hover:bg-zsl-primary hover:text-zsl-bg transition-all duration-300"
|
||||
aria-label="Next testimonial"
|
||||
>
|
||||
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 5l7 7-7 7" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Dots */}
|
||||
<div className="flex gap-2">
|
||||
{testimonials.map((_, index) => (
|
||||
<button
|
||||
key={index}
|
||||
onClick={() => {
|
||||
if (!isAnimating) {
|
||||
setIsAnimating(true);
|
||||
setCurrentIndex(index);
|
||||
setTimeout(() => setIsAnimating(false), 500);
|
||||
}
|
||||
}}
|
||||
className={`w-2 h-2 rounded-full transition-all duration-300 ${
|
||||
index === currentIndex
|
||||
? 'bg-zsl-primary w-6'
|
||||
: 'bg-zsl-muted/30 hover:bg-zsl-muted/50'
|
||||
}`}
|
||||
aria-label={`Go to testimonial ${index + 1}`}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
Reference in New Issue
Block a user