This commit is contained in:
2025-12-21 20:40:32 +01:00
commit c7f669fc11
54 changed files with 7575 additions and 0 deletions

View File

@@ -0,0 +1,57 @@
import React, { useEffect } from 'react';
import { useLocation } from 'react-router-dom';
import { supabase, isSupabaseConfigured } from '../lib/supabaseClient';
import { useAuth } from '../context/AuthContext';
export const AnalyticsTracker: React.FC = () => {
const location = useLocation();
const { user, loading } = useAuth();
useEffect(() => {
// Wait for auth to initialize so we don't log "null" user then "user id" immediately after
if (loading) return;
const trackVisit = async () => {
// 1. Check for cookie consent
// If user hasn't allowed or has explicitly denied, do not track.
const consent = localStorage.getItem('cookie_consent');
if (consent !== 'true') {
return;
}
// 2. Don't track if Supabase isn't configured (Demo mode)
if (!isSupabaseConfigured) return;
// 3. Check if we already logged this session
// sessionStorage persists while the tab is open, ensuring 1 count per session
const hasLoggedSession = sessionStorage.getItem('motionweb_visit_logged');
if (hasLoggedSession) return;
try {
// Mark immediately to prevent race conditions if effect fires multiple times rapidly
sessionStorage.setItem('motionweb_visit_logged', 'true');
await supabase.from('page_visits').insert({
page_path: location.pathname + location.hash,
user_agent: navigator.userAgent,
user_id: user?.id || null
});
} catch (error) {
// Silently fail for analytics to not disrupt user experience
console.error('Analytics tracking error:', error);
}
};
trackVisit();
// Add listener for storage events to re-check tracking if consent changes mid-session
const handleStorageChange = () => trackVisit();
window.addEventListener('storage', handleStorageChange);
return () => window.removeEventListener('storage', handleStorageChange);
}, [location, user, loading]);
return null;
};

42
components/Button.tsx Normal file
View File

@@ -0,0 +1,42 @@
import React from 'react';
interface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
variant?: 'primary' | 'secondary' | 'outline' | 'white';
size?: 'sm' | 'md' | 'lg';
fullWidth?: boolean;
}
export const Button: React.FC<ButtonProps> = ({
children,
variant = 'primary',
size = 'md',
fullWidth = false,
className = '',
...props
}) => {
const baseStyles = "inline-flex items-center justify-center font-semibold transition-all duration-300 rounded-full focus:outline-none focus:ring-2 focus:ring-offset-2";
const variants = {
primary: "bg-gradient-to-r from-primary to-secondary hover:from-primary-dark hover:to-secondary-dark text-white shadow-lg hover:shadow-xl border-transparent focus:ring-primary",
secondary: "bg-secondary hover:bg-secondary-dark text-white shadow-md focus:ring-secondary",
outline: "bg-transparent border-2 border-primary text-primary hover:bg-primary hover:text-white focus:ring-primary",
white: "bg-white text-primary hover:bg-gray-100 shadow-md focus:ring-white"
};
const sizes = {
sm: "px-4 py-2 text-sm",
md: "px-6 py-3 text-base",
lg: "px-8 py-4 text-lg",
};
const widthClass = fullWidth ? "w-full" : "";
return (
<button
className={`${baseStyles} ${variants[variant]} ${sizes[size]} ${widthClass} ${className}`}
{...props}
>
{children}
</button>
);
};

132
components/CookieBanner.tsx Normal file
View File

@@ -0,0 +1,132 @@
import React, { useState, useEffect } from 'react';
import { X, Check, Cookie } from 'lucide-react';
import { Link } from 'react-router-dom';
import { useAuth } from '../context/AuthContext';
import { supabase, isSupabaseConfigured } from '../lib/supabaseClient';
export const CookieBanner: React.FC = () => {
const { user } = useAuth();
const [isVisible, setIsVisible] = useState(false);
const [hasChecked, setHasChecked] = useState(false);
useEffect(() => {
const checkAndSyncConsent = async () => {
// 1. Check Local Storage choice
const localConsent = localStorage.getItem('cookie_consent');
// If a choice was already made locally, we hide the banner immediately
if (localConsent !== null) {
setIsVisible(false);
setHasChecked(true);
// AUTO-SYNC: If user is logged in, ensure the local choice is synced to the database
if (user && isSupabaseConfigured) {
try {
const consentValue = localConsent === 'true';
await supabase
.from('profiles')
.update({ cookie_consent: consentValue })
.eq('id', user.id);
} catch (e) {
console.error("Error auto-syncing cookie consent to DB:", e);
}
}
return;
}
// 2. If NO local choice but user IS logged in, try to fetch from DB
if (user && isSupabaseConfigured) {
try {
const { data, error } = await supabase
.from('profiles')
.select('cookie_consent')
.eq('id', user.id)
.maybeSingle();
if (!error && data && data.cookie_consent !== null) {
// Save DB choice to local storage and hide
localStorage.setItem('cookie_consent', data.cookie_consent.toString());
setIsVisible(false);
setHasChecked(true);
return;
}
} catch (e) {
console.error("Error checking cookie consent in DB:", e);
}
}
// 3. If NO choice found anywhere, show the banner
setIsVisible(true);
setHasChecked(true);
};
checkAndSyncConsent();
}, [user]);
const handleConsent = async (allowed: boolean) => {
// Save to local storage immediately to hide banner
localStorage.setItem('cookie_consent', allowed.toString());
setIsVisible(false);
// If logged in, also update the database
if (user && isSupabaseConfigured) {
try {
await supabase
.from('profiles')
.update({ cookie_consent: allowed })
.eq('id', user.id);
} catch (e) {
console.error("Error saving cookie consent to DB manually:", e);
}
}
// Trigger storage event for AnalyticsTracker and other listeners
window.dispatchEvent(new Event('storage'));
};
if (!isVisible || !hasChecked) return null;
return (
<div className="fixed bottom-6 right-6 z-[100] max-w-sm w-full animate-fade-in-up px-4 sm:px-0">
<div className="bg-white/95 backdrop-blur-md rounded-2xl shadow-[0_20px_50px_rgba(0,0,0,0.15)] border border-gray-100 p-6 flex flex-col gap-4">
<div className="flex items-start gap-4">
<div className="p-3 bg-primary/10 rounded-xl text-primary flex-shrink-0">
<Cookie className="w-6 h-6" />
</div>
<div className="flex-grow">
<h3 className="text-sm font-bold text-gray-900">Sütik és Adatvédelem</h3>
<p className="text-xs text-gray-500 mt-1 leading-relaxed">
Az élmény fokozása érdekében sütiket használunk. Az elfogadással hozzájárul az anonim látogatottsági adatok gyűjtéséhez.
</p>
</div>
<button
onClick={() => setIsVisible(false)}
className="text-gray-400 hover:text-gray-600 transition-colors p-1"
title="Bezárás"
>
<X className="w-4 h-4" />
</button>
</div>
<div className="flex gap-3">
<button
onClick={() => handleConsent(true)}
className="flex-1 bg-primary text-white py-2.5 px-4 rounded-xl text-xs font-bold hover:bg-primary-dark transition-all flex items-center justify-center gap-2 shadow-lg shadow-primary/20 hover:scale-[1.02] active:scale-95"
>
<Check className="w-3.5 h-3.5" /> Elfogadom
</button>
<button
onClick={() => handleConsent(false)}
className="flex-1 bg-gray-100 text-gray-600 py-2.5 px-4 rounded-xl text-xs font-bold hover:bg-gray-200 transition-all active:scale-95"
>
Elutasítom
</button>
</div>
<div className="text-[10px] text-center text-gray-400">
További információkért olvassa el az <Link to="/privacy" className="underline hover:text-primary transition-colors">Adatkezelési tájékoztatónkat</Link>.
</div>
</div>
</div>
);
};

View File

@@ -0,0 +1,289 @@
import React, { useState } from 'react';
import { X, Info, CheckCircle, AlertTriangle, MessageSquare, Calendar, ChevronDown, ChevronUp } from 'lucide-react';
import { Button } from './Button';
interface FeedbackModalProps {
isOpen: boolean;
onClose: () => void;
onSubmit: (feedbackData: any) => Promise<void>;
loading: boolean;
}
export const FeedbackModal: React.FC<FeedbackModalProps> = ({ isOpen, onClose, onSubmit, loading }) => {
// --- STATE ---
const [mainDecision, setMainDecision] = useState<'approved' | 'minor' | 'major' | null>(null);
// Approval Flow
const [approvalConfirmed, setApprovalConfirmed] = useState(false);
// Revision Flow
const [designCheckboxes, setDesignCheckboxes] = useState<string[]>([]);
const [designText, setDesignText] = useState('');
const [contentCheckboxes, setContentCheckboxes] = useState<string[]>([]);
const [contentText, setContentText] = useState('');
const [structureCheckboxes, setStructureCheckboxes] = useState<string[]>([]);
const [structureText, setStructureText] = useState('');
const [funcCheckboxes, setFuncCheckboxes] = useState<string[]>([]);
const [funcText, setFuncText] = useState('');
const [priorityText, setPriorityText] = useState('');
const [deadlineType, setDeadlineType] = useState<'none' | 'date' | 'discuss'>('none');
const [deadlineDate, setDeadlineDate] = useState('');
const [extraNotes, setExtraNotes] = useState('');
const [revisionConfirmed, setRevisionConfirmed] = useState(false);
if (!isOpen) return null;
// --- HANDLERS ---
const handleCheckbox = (
currentList: string[],
setList: React.Dispatch<React.SetStateAction<string[]>>,
value: string,
noneValue: string
) => {
if (value === noneValue) {
if (currentList.includes(noneValue)) {
setList([]);
} else {
setList([noneValue]);
}
} else {
let newList = currentList.filter(item => item !== noneValue);
if (newList.includes(value)) {
newList = newList.filter(item => item !== value);
} else {
newList = [...newList, value];
}
setList(newList);
}
};
const isRevision = mainDecision === 'minor' || mainDecision === 'major';
const handleSubmit = () => {
const feedbackData = {
decision: mainDecision,
submittedAt: new Date().toISOString(),
approval: mainDecision === 'approved' ? {
confirmed: approvalConfirmed
} : null,
revision: isRevision ? {
design: { selected: designCheckboxes, comment: designText },
content: { selected: contentCheckboxes, comment: contentText },
structure: { selected: structureCheckboxes, comment: structureText },
functionality: { selected: funcCheckboxes, comment: funcText },
priority: priorityText,
deadline: { type: deadlineType, date: deadlineDate },
extraNotes: extraNotes,
confirmed: revisionConfirmed
} : null
};
onSubmit(feedbackData);
};
const commonInputStyles = "w-full p-4 border border-gray-300 rounded-xl text-sm font-medium text-black placeholder-gray-400 focus:ring-4 focus:ring-primary/10 focus:border-primary outline-none bg-white transition-all shadow-sm";
const renderFeedbackCategory = (
title: string,
options: string[],
selected: string[],
setSelected: React.Dispatch<React.SetStateAction<string[]>>,
textValue: string,
setTextValue: React.Dispatch<React.SetStateAction<string>>,
placeholder: string
) => {
const noneOption = options[options.length - 1];
const showTextarea = selected.length > 0 && !selected.includes(noneOption);
return (
<div className="mb-6 bg-gray-50/50 p-6 rounded-2xl border border-gray-100 shadow-sm">
<h4 className="font-bold text-gray-800 mb-4 text-sm uppercase tracking-widest">{title}</h4>
<div className="grid grid-cols-1 sm:grid-cols-2 gap-3 mb-4">
{options.map(opt => (
<label key={opt} className={`flex items-center space-x-3 p-3 rounded-xl cursor-pointer border-2 transition-all ${selected.includes(opt) ? 'bg-white border-primary shadow-md' : 'bg-white/50 border-transparent hover:border-gray-200'}`}>
<input
type="checkbox"
checked={selected.includes(opt)}
onChange={() => handleCheckbox(selected, setSelected, opt, noneOption)}
className="rounded text-primary focus:ring-primary w-5 h-5 border-gray-300"
/>
<span className={`text-sm font-semibold ${selected.includes(opt) ? 'text-primary' : 'text-gray-600'}`}>{opt}</span>
</label>
))}
</div>
{showTextarea && (
<textarea
value={textValue}
onChange={(e) => setTextValue(e.target.value)}
className={`${commonInputStyles} animate-fade-in mt-2`}
rows={3}
placeholder={placeholder}
></textarea>
)}
</div>
);
};
return (
<div className="fixed inset-0 z-[100] flex items-center justify-center p-4 bg-gray-900/75 backdrop-blur-sm overflow-y-auto">
<div className="bg-white rounded-3xl shadow-2xl w-full max-w-3xl flex flex-col my-8 relative animate-fade-in-up border border-white/20">
<button onClick={onClose} className="absolute top-6 right-6 p-2 hover:bg-gray-100 rounded-full transition-colors z-10 bg-white shadow-sm border border-gray-100">
<X className="w-5 h-5 text-gray-500" />
</button>
<div className="p-8 border-b border-gray-100 bg-gradient-to-r from-blue-50/50 to-purple-50/50 rounded-t-3xl">
<div className="flex items-start gap-4">
<div className="bg-white p-3 rounded-2xl shadow-md text-primary border border-gray-100">
<MessageSquare className="w-6 h-6" />
</div>
<div>
<h2 className="text-2xl font-black text-gray-900 tracking-tighter">Visszajelzés a Weboldalról</h2>
<p className="text-sm text-gray-500 font-medium mt-1">A bemutató verzió alapján kérjük jelezd, ha valamit módosítanál.</p>
</div>
</div>
</div>
<div className="p-8 md:p-10 space-y-10">
<div>
<h3 className="text-lg font-bold text-gray-900 mb-5 flex items-center gap-2">
Döntés a továbblépésről <span className="text-red-500">*</span>
</h3>
<div className="grid grid-cols-1 gap-4">
{[
{ id: 'approved', label: 'Igen, jóváhagyom a terveket', color: 'green', icon: '✅' },
{ id: 'minor', label: 'Alapvetően jó, de kisebb javításokat kérek', color: 'yellow', icon: '🔧' },
{ id: 'major', label: 'Nem megfelelő, jelentős módosításra van szükség', color: 'red', icon: '❌' }
].map(opt => (
<label key={opt.id} className={`flex items-center p-5 border-2 rounded-2xl cursor-pointer transition-all ${mainDecision === opt.id ? `border-${opt.color}-500 bg-${opt.color}-50 shadow-lg ring-1 ring-${opt.color}-500` : 'border-gray-100 hover:border-gray-200'}`}>
<input
type="radio"
name="decision"
checked={mainDecision === opt.id}
onChange={() => setMainDecision(opt.id as any)}
className={`w-6 h-6 text-${opt.color}-600 focus:ring-${opt.color}-500 border-gray-300`}
/>
<span className="ml-4 font-bold text-gray-900">{opt.icon} {opt.label}</span>
</label>
))}
</div>
</div>
{mainDecision === 'approved' && (
<div className="animate-fade-in bg-green-50 p-6 rounded-2xl border-2 border-green-200 shadow-inner">
<h3 className="text-green-900 font-black mb-4 flex items-center gap-2 uppercase text-sm tracking-widest">
<CheckCircle className="w-5 h-5" /> Megerősítés
</h3>
<label className="flex items-start cursor-pointer group">
<input
type="checkbox"
checked={approvalConfirmed}
onChange={(e) => setApprovalConfirmed(e.target.checked)}
className="mt-1 w-6 h-6 text-green-600 rounded-lg focus:ring-green-500 border-green-300"
/>
<span className="ml-4 text-sm text-green-800 font-bold leading-relaxed">
Tudomásul veszem, hogy a jóváhagyás után a végleges fejlesztés megkezdődik a tervek alapján.
</span>
</label>
</div>
)}
{isRevision && (
<div className="animate-fade-in space-y-8 border-t border-gray-100 pt-10">
{renderFeedbackCategory(
"Dizájn módosítások",
["Színek", "Betűtípusok", "Elrendezés", "Képek / Illusztrációk", "Nem szeretnék dizájn módosítást"],
designCheckboxes, setDesignCheckboxes,
designText, setDesignText,
"Írd le pontosan, mit változtatnál a megjelenésen..."
)}
{renderFeedbackCategory(
"Tartalmi változtatások",
["Szövegek stílusa", "Adatok pontosítása", "Hiányzó tartalom", "Nem szeretnék tartalmi módosítást"],
contentCheckboxes, setContentCheckboxes,
contentText, setContentText,
"Írd le a szöveges módosításokat..."
)}
<div>
<h3 className="text-sm font-black text-gray-900 mb-3 uppercase tracking-widest">Mi a legfontosabb kérésed? <span className="text-red-500">*</span></h3>
<textarea
value={priorityText}
onChange={(e) => setPriorityText(e.target.value)}
className={commonInputStyles}
rows={2}
placeholder="A legkritikusabb pont, amin változtatni kell..."
></textarea>
</div>
<div>
<h3 className="text-sm font-black text-gray-900 mb-3 uppercase tracking-widest">Egyéb észrevételek</h3>
<textarea
value={extraNotes}
onChange={(e) => setExtraNotes(e.target.value)}
className={commonInputStyles}
rows={4}
placeholder="Bármi egyéb megjegyzés..."
></textarea>
</div>
<div className="bg-yellow-50 p-6 rounded-2xl border-2 border-yellow-200 shadow-inner">
<label className="flex items-start cursor-pointer">
<input
type="checkbox"
checked={revisionConfirmed}
onChange={(e) => setRevisionConfirmed(e.target.checked)}
className="mt-1 w-6 h-6 text-yellow-600 rounded-lg focus:ring-yellow-500 border-yellow-300"
/>
<span className="ml-4 text-sm text-yellow-900 font-bold leading-relaxed">
Tudomásul veszem, hogy a kért módosítások feldolgozása után kollégáik keresni fognak az újabb verzióval.
</span>
</label>
</div>
</div>
)}
</div>
<div className="p-8 border-t border-gray-100 bg-gray-50/50 rounded-b-3xl flex flex-col sm:flex-row justify-end gap-4">
<Button variant="white" onClick={onClose} disabled={loading} className="px-10 border-gray-200">Mégse</Button>
{mainDecision === 'approved' && (
<Button
onClick={handleSubmit}
disabled={loading || !approvalConfirmed}
className="bg-green-600 hover:bg-green-700 text-white font-black uppercase tracking-widest px-10 shadow-lg shadow-green-200"
>
{loading ? 'Küldés...' : 'Végleges Jóváhagyás'}
</Button>
)}
{isRevision && (
<Button
onClick={handleSubmit}
disabled={loading || !revisionConfirmed || !priorityText.trim()}
className="font-black uppercase tracking-widest px-10 shadow-lg shadow-primary/20"
>
{loading ? 'Küldés...' : 'Visszajelzés Elküldése'}
</Button>
)}
{!mainDecision && (
<Button disabled className="opacity-50 cursor-not-allowed font-black uppercase tracking-widest px-10">
Válasszon opciót
</Button>
)}
</div>
</div>
</div>
);
};

72
components/Footer.tsx Normal file
View File

@@ -0,0 +1,72 @@
import React from 'react';
import { Facebook, Instagram, Code2, Mail } from 'lucide-react';
import { Link } from 'react-router-dom';
export const Footer: React.FC = () => {
return (
<footer className="bg-gray-900 text-white pt-16 pb-8 border-t border-gray-800">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-12 mb-12">
{/* Brand Info */}
<div className="space-y-6">
<div className="flex items-center space-x-2">
<div className="p-2 bg-gradient-to-br from-primary to-secondary rounded-lg">
<Code2 className="h-6 w-6 text-white" />
</div>
<span className="text-2xl font-bold">MotionWeb</span>
</div>
<p className="text-gray-400 text-sm leading-relaxed">
Innovatív webes megoldások, amelyek üzleti eredményeket hoznak. Teljeskörű digitális partner cégek számára.
</p>
<div className="flex space-x-4">
<a href="#" className="w-10 h-10 rounded-full bg-gray-800 flex items-center justify-center text-gray-400 hover:bg-primary hover:text-white transition-all duration-300">
<Facebook size={18} />
</a>
<a href="#" className="w-10 h-10 rounded-full bg-gray-800 flex items-center justify-center text-gray-400 hover:bg-primary hover:text-white transition-all duration-300">
<Instagram size={18} />
</a>
</div>
</div>
{/* Services */}
<div>
<h3 className="text-lg font-bold mb-6 text-white">Szolgáltatások</h3>
<ul className="space-y-3">
<li className="text-gray-400 text-sm">Egyedi Weboldal</li>
<li className="text-gray-400 text-sm">Reszponzív Design</li>
<li className="text-gray-400 text-sm">SEO Optimalizálás</li>
<li className="text-gray-400 text-sm">UI/UX Design</li>
</ul>
</div>
{/* Contact Info - Modified */}
<div>
<h3 className="text-lg font-bold mb-6 text-white">Elérhetőség</h3>
<ul className="space-y-4">
<li className="flex items-center space-x-3">
<Mail className="w-5 h-5 text-primary flex-shrink-0" />
<a href="mailto:motionstudiohq@gmail.com" className="text-gray-400 hover:text-white transition-colors text-sm">motionstudiohq@gmail.com</a>
</li>
</ul>
</div>
{/* Information (Replaced Newsletter) */}
<div>
<h3 className="text-lg font-bold mb-6 text-white">Információk</h3>
<ul className="space-y-3">
<li><Link to="/privacy" className="text-gray-400 hover:text-primary transition-colors text-sm">Adatkezelési tájékoztató</Link></li>
<li><Link to="/terms" className="text-gray-400 hover:text-primary transition-colors text-sm">Általános Szerződési Feltételek</Link></li>
<li><Link to="/faq" className="text-gray-400 hover:text-primary transition-colors text-sm">Gyakori Kérdések</Link></li>
</ul>
</div>
</div>
<div className="border-t border-gray-800 pt-8 flex flex-col md:flex-row justify-between items-center gap-4">
<p className="text-gray-500 text-sm text-center md:text-left">
&copy; {new Date().getFullYear()} Motion Web Stúdió. Minden jog fenntartva.
</p>
</div>
</div>
</footer>
);
};

236
components/Navbar.tsx Normal file
View File

@@ -0,0 +1,236 @@
import React, { useState, useEffect, useRef } from 'react';
import { Menu, X, Code2, User, LogIn, UserPlus, LogOut, LayoutDashboard, ShieldAlert } from 'lucide-react';
import { Link, useLocation, useNavigate } from 'react-router-dom';
import { Button } from './Button';
import { useAuth } from '../context/AuthContext';
export const Navbar: React.FC = () => {
const [isOpen, setIsOpen] = useState(false);
const [isProfileOpen, setIsProfileOpen] = useState(false);
const [scrolled, setScrolled] = useState(false);
const location = useLocation();
const navigate = useNavigate();
const profileRef = useRef<HTMLDivElement>(null);
const { user, signOut, isAdmin } = useAuth();
useEffect(() => {
const handleScroll = () => {
setScrolled(window.scrollY > 20);
};
window.addEventListener('scroll', handleScroll);
// Close dropdown when clicking outside
const handleClickOutside = (event: MouseEvent) => {
if (profileRef.current && !profileRef.current.contains(event.target as Node)) {
setIsProfileOpen(false);
}
};
document.addEventListener('mousedown', handleClickOutside);
return () => {
window.removeEventListener('scroll', handleScroll);
document.removeEventListener('mousedown', handleClickOutside);
};
}, []);
useEffect(() => {
setIsOpen(false);
setIsProfileOpen(false);
}, [location]);
const handleLogout = async () => {
await signOut();
navigate('/');
setIsProfileOpen(false);
};
// Helper to determine if a link is active based on hash or path
const isActive = (path: string) => {
if (path === '/') return location.pathname === '/' && !location.hash;
if (path.startsWith('/#')) return location.hash === path.substring(1);
return location.pathname === path;
};
const navLinks = [
{ name: 'Kezdőlap', path: '/' },
{ name: 'Szolgáltatások', path: '/#services' },
{ name: 'Referenciák', path: '/#references' },
{ name: 'Csomagok', path: '/#products' },
];
const linkClass = (path: string) => `text-sm font-medium transition-colors hover:text-primary ${
isActive(path) ? 'text-primary' : (
!scrolled && location.pathname === '/' ? 'text-gray-100 hover:text-white' : 'text-gray-700'
)
}`;
return (
<nav className={`fixed w-full z-50 transition-all duration-300 ${scrolled ? 'bg-white/90 backdrop-blur-md shadow-md py-2' : 'bg-transparent py-4'}`}>
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div className="flex justify-between items-center h-16">
{/* Logo */}
<Link to="/" className="flex items-center space-x-2 group">
<div className="p-2 bg-gradient-to-br from-primary to-secondary rounded-lg group-hover:shadow-lg transition-all duration-300">
<Code2 className="h-6 w-6 text-white" />
</div>
<span className={`text-2xl font-bold bg-clip-text text-transparent bg-gradient-to-r from-primary to-secondary ${!scrolled && location.pathname === '/' ? 'text-white' : ''}`}>
MotionWeb
</span>
</Link>
{/* Desktop Navigation Links */}
<div className="hidden md:flex items-center space-x-8">
{navLinks.map((link) => (
<Link
key={link.path}
to={link.path}
className={linkClass(link.path)}
>
{link.name}
</Link>
))}
</div>
{/* Right Side Actions */}
<div className="flex items-center gap-4">
{/* Desktop Order Button */}
<div className="hidden md:block">
<Link to="/#rendeles">
<Button variant={!scrolled && location.pathname === '/' ? 'white' : 'primary'} size="sm">
Rendelés
</Button>
</Link>
</div>
{/* Profile Dropdown */}
<div className="relative" ref={profileRef}>
<button
onClick={() => setIsProfileOpen(!isProfileOpen)}
className={`p-2 rounded-full transition-colors duration-300 flex items-center justify-center ${
!scrolled && location.pathname === '/'
? 'text-white hover:bg-white/10'
: 'text-gray-700 hover:bg-gray-100'
} ${user ? 'ring-2 ring-primary/20 bg-primary/5' : ''}`}
aria-label="Felhasználói fiók"
>
<User className="w-6 h-6" />
{isAdmin && <div className="absolute top-1 right-1 w-2.5 h-2.5 bg-red-500 rounded-full border-2 border-white"></div>}
</button>
{isProfileOpen && (
<div className="absolute right-0 mt-3 w-64 bg-white rounded-xl shadow-2xl py-2 border border-gray-100 transform origin-top-right animate-fade-in z-50">
<div className="px-4 py-3 border-b border-gray-100 bg-gray-50/50">
<p className="text-sm font-semibold text-gray-900">Fiókom</p>
<p className="text-xs text-gray-500 truncate">{user ? user.email : 'Vendég felhasználó'}</p>
</div>
<div className="py-2">
{user ? (
<>
<Link to="/dashboard" className="flex items-center px-4 py-2.5 text-sm text-gray-700 hover:bg-purple-50 hover:text-primary transition-colors">
<LayoutDashboard className="w-4 h-4 mr-3" /> Vezérlőpult
</Link>
{isAdmin && (
<Link to="/admin" className="flex items-center px-4 py-2.5 text-sm text-red-600 hover:bg-red-50 hover:text-red-700 transition-colors font-semibold">
<ShieldAlert className="w-4 h-4 mr-3" /> Admin Felület
</Link>
)}
<div className="border-t border-gray-100 my-1"></div>
<button onClick={handleLogout} className="w-full flex items-center px-4 py-2.5 text-sm text-red-600 hover:bg-red-50 transition-colors">
<LogOut className="w-4 h-4 mr-3" /> Kijelentkezés
</button>
</>
) : (
<>
<Link to="/auth/login" className="flex items-center px-4 py-2.5 text-sm text-gray-700 hover:bg-purple-50 hover:text-primary transition-colors">
<LogIn className="w-4 h-4 mr-3" /> Bejelentkezés
</Link>
<Link to="/auth/register" className="flex items-center px-4 py-2.5 text-sm text-gray-700 hover:bg-purple-50 hover:text-primary transition-colors">
<UserPlus className="w-4 h-4 mr-3" /> Regisztráció
</Link>
</>
)}
</div>
</div>
)}
</div>
{/* Mobile Menu Toggle */}
<div className="md:hidden">
<button
onClick={() => setIsOpen(!isOpen)}
className={`p-2 rounded-md ${!scrolled && location.pathname === '/' ? 'text-white' : 'text-gray-700'}`}
>
{isOpen ? <X className="h-6 w-6" /> : <Menu className="h-6 w-6" />}
</button>
</div>
</div>
</div>
</div>
{/* Mobile Menu */}
{isOpen && (
<div className="md:hidden bg-white border-t border-gray-100 shadow-xl absolute w-full animate-fade-in">
<div className="px-2 pt-2 pb-3 space-y-1 sm:px-3">
{navLinks.map((link) => (
<Link
key={link.path}
to={link.path}
className={`block px-3 py-2 rounded-md text-base font-medium ${
isActive(link.path)
? 'text-primary bg-purple-50'
: 'text-gray-700 hover:text-primary hover:bg-gray-50'
}`}
>
{link.name}
</Link>
))}
<div className="pt-4 pb-2 px-3 space-y-3">
<Link to="/#rendeles" className="block w-full">
<Button fullWidth>Rendelés</Button>
</Link>
{!user && (
<div className="grid grid-cols-2 gap-3 pt-2">
<Link to="/auth/login">
<Button variant="outline" fullWidth size="sm">Belépés</Button>
</Link>
<Link to="/auth/register">
<Button variant="secondary" fullWidth size="sm">Regisztráció</Button>
</Link>
</div>
)}
{user && (
<div className="pt-2 border-t border-gray-100">
<div className="px-1 py-2 flex items-center gap-3 mb-2">
<div className="w-8 h-8 rounded-full bg-primary/10 flex items-center justify-center text-primary">
<User className="w-4 h-4" />
</div>
<div className="overflow-hidden">
<p className="text-sm font-medium text-gray-900 truncate">{user.email}</p>
{isAdmin && <p className="text-[10px] text-red-500 font-bold uppercase">Adminisztrátor</p>}
</div>
</div>
<Link to="/dashboard" className="block w-full text-center py-2 text-sm font-medium text-gray-700 bg-gray-50 rounded-lg mb-2">
Vezérlőpult megnyitása
</Link>
{isAdmin && (
<Link to="/admin" className="block w-full text-center py-2 text-sm font-medium text-red-600 bg-red-50 rounded-lg mb-2 border border-red-100">
Admin Felület
</Link>
)}
<button onClick={handleLogout} className="block w-full text-center py-2 text-sm font-medium text-red-600 border border-red-100 rounded-lg hover:bg-red-50">
Kijelentkezés
</button>
</div>
)}
</div>
</div>
</div>
)}
</nav>
);
};

876
components/OrderForm.tsx Normal file
View File

@@ -0,0 +1,876 @@
import React, { useState } from 'react';
import {
Send, CheckCircle, AlertCircle, Globe, Server, Check,
ArrowRight, ArrowLeft, User, FileText, Target, Layout,
Palette, Zap, Lightbulb, Settings, ClipboardCheck, Lock,
Cloud, Upload
} from 'lucide-react';
import { Button } from './Button';
import { useAuth } from '../context/AuthContext';
import { Link } from 'react-router-dom';
import { supabase, isSupabaseConfigured } from '../lib/supabaseClient';
interface Inspiration {
url: string;
comment: string;
}
export const OrderForm: React.FC = () => {
const { user, loading } = useAuth();
const [currentStep, setCurrentStep] = useState(1);
const [isSubmitted, setIsSubmitted] = useState(false);
const [isSubmitting, setIsSubmitting] = useState(false);
const [submitError, setSubmitError] = useState<string | null>(null);
const [errors, setErrors] = useState<string[]>([]);
const [privacyAccepted, setPrivacyAccepted] = useState(false);
const [aszfAccepted, setAszfAccepted] = useState(false);
const totalSteps = 9;
const [formData, setFormData] = useState({
// 1. Kapcsolattartás
name: '',
company: '',
email: user?.email || '',
phone: '',
package: 'Pro Web', // Default selection updated
// 2. Bemutatkozás
description: '',
// 3. Célok
goals: [] as string[],
goalOther: '',
successCriteria: '',
// 4. Tartalom
content: [] as string[],
contentOther: '',
existingAssets: 'Nincs' as 'Igen' | 'Nem' | 'Részben',
contentLink: '', // New field for drive link
// 5. Design
primaryColor: '',
secondaryColor: '',
balanceColor: '',
style: [] as string[],
targetAudience: '',
// 6. Funkciók
features: [] as string[],
// 7. Inspirációk
inspirations: [
{ url: '', comment: '' },
{ url: '', comment: '' },
{ url: '', comment: '' }
] as Inspiration[],
// 8. Extrák & Megjegyzések
extras: [] as string[],
domainName: '',
notes: ''
});
// Helper arrays
const colorOptions = [
{ name: 'Piros', value: '#ef4444' },
{ name: 'Kék', value: '#3b82f6' },
{ name: 'Zöld', value: '#22c55e' },
{ name: 'Lila', value: '#a855f7' },
{ name: 'Fekete', value: '#171717' },
{ name: 'Fehér', value: '#ffffff' },
{ name: 'Szürke', value: '#6b7280' },
{ name: 'Narancs', value: '#f97316' },
{ name: 'Sárga', value: '#eab308' },
{ name: 'Türkiz', value: '#14b8a6' },
{ name: 'Barna', value: '#78350f' },
];
const styleOptions = [
'Modern és letisztult', 'Üzleti és professzionális', 'Fiatalos és energikus',
'Spirituális / nyugodt', 'Természetes / barátságos', 'Luxus / prémium'
];
const steps = [
{ id: 1, title: 'Kapcsolat', icon: User },
{ id: 2, title: 'Bemutatkozás', icon: FileText },
{ id: 3, title: 'Célok', icon: Target },
{ id: 4, title: 'Tartalom', icon: Layout },
{ id: 5, title: 'Design', icon: Palette },
{ id: 6, title: 'Funkciók', icon: Zap },
{ id: 7, title: 'Inspirációk', icon: Lightbulb },
{ id: 8, title: 'Extrák', icon: Settings },
{ id: 9, title: 'Összegzés', icon: ClipboardCheck },
];
// Auto-fill user data (Name and Email) when user loads
React.useEffect(() => {
const prefillUserData = async () => {
if (!user) return;
let emailToSet = user.email || '';
let nameToSet = '';
// 1. Try metadata (from session - fastest)
const meta = user.user_metadata;
if (meta?.last_name && meta?.first_name) {
// Hungarian order: Lastname Firstname
nameToSet = `${meta.last_name} ${meta.first_name}`;
}
// 2. Fallback to DB if Supabase is configured and metadata is missing name
if (!nameToSet && isSupabaseConfigured) {
try {
const { data } = await supabase
.from('profiles')
.select('first_name, last_name')
.eq('id', user.id)
.maybeSingle();
if (data?.first_name && data?.last_name) {
nameToSet = `${data.last_name} ${data.first_name}`;
}
} catch (error) {
console.error('Error fetching user profile for order form:', error);
}
}
setFormData(prev => {
// Only update if fields are empty to avoid overwriting user input
if (prev.name && prev.email) return prev;
return {
...prev,
email: prev.email || emailToSet,
name: prev.name || nameToSet
};
});
};
prefillUserData();
}, [user]);
// If loading auth state, show a skeleton or loader
if (loading) {
return (
<div id="order-form-container" className="bg-white rounded-3xl shadow-xl p-12 text-center border border-gray-100 min-h-[400px] flex items-center justify-center">
<div className="animate-pulse text-gray-400">Betöltés...</div>
</div>
);
}
// Auth Protection - Lock Screen
if (!user) {
return (
<div id="order-form-container" className="bg-white rounded-3xl shadow-xl overflow-hidden border border-gray-100 scroll-mt-24 max-w-4xl mx-auto">
<div className="bg-gradient-to-br from-[#eef2ff] to-[#f5f3ff] pt-12 pb-8 px-6 md:px-12 border-b border-white shadow-sm text-center">
<div className="w-20 h-20 bg-white rounded-full flex items-center justify-center mx-auto mb-6 shadow-md text-[#4e6bff]">
<Lock className="w-10 h-10" />
</div>
<h2 className="text-3xl font-extrabold text-[#111827] mb-3">Jelentkezzen be a rendeléshez</h2>
<p className="text-[#4b5563] text-lg max-w-xl mx-auto">
Weboldal rendelés leadásához kérjük, jelentkezzen be fiókjába, vagy regisztráljon egyet ingyenesen.
</p>
</div>
<div className="p-12 text-center bg-white">
<div className="flex flex-col sm:flex-row gap-4 justify-center max-w-md mx-auto">
<Link to="/auth/login" className="w-full">
<Button fullWidth size="lg">Bejelentkezés</Button>
</Link>
<Link to="/auth/register" className="w-full">
<Button variant="outline" fullWidth size="lg">Regisztráció</Button>
</Link>
</div>
<p className="mt-8 text-sm text-gray-500">
A regisztráció mindössze 1 percet vesz igénybe, és segít a projekt későbbi nyomon követésében.
</p>
</div>
</div>
);
}
const handleInputChange = (e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) => {
const { name, value } = e.target;
setFormData(prev => ({ ...prev, [name]: value }));
};
const handleCheckboxChange = (category: 'goals' | 'content' | 'style' | 'features' | 'extras', value: string) => {
setFormData(prev => {
const list = prev[category];
if (list.includes(value)) {
return { ...prev, [category]: list.filter(item => item !== value) };
} else {
return { ...prev, [category]: [...list, value] };
}
});
};
const handleInspirationChange = (index: number, field: 'url' | 'comment', value: string) => {
const newInspirations = [...formData.inspirations];
newInspirations[index] = { ...newInspirations[index], [field]: value };
setFormData(prev => ({ ...prev, inspirations: newInspirations }));
};
const validateStep = (step: number): boolean => {
const newErrors: string[] = [];
if (step === 1) {
if (!formData.name) newErrors.push('A név megadása kötelező.');
if (!formData.email) newErrors.push('Az e-mail cím megadása kötelező.');
if (!formData.phone) newErrors.push('A telefonszám megadása kötelező.');
}
if (newErrors.length > 0) {
setErrors(newErrors);
return false;
}
setErrors([]);
return true;
};
const nextStep = () => {
if (validateStep(currentStep)) {
if (currentStep < totalSteps) {
setCurrentStep(prev => prev + 1);
window.scrollTo({ top: document.getElementById('order-form-container')?.offsetTop || 0, behavior: 'smooth' });
} else {
handleSubmit();
}
}
};
const prevStep = () => {
if (currentStep > 1) {
setCurrentStep(prev => prev - 1);
window.scrollTo({ top: document.getElementById('order-form-container')?.offsetTop || 0, behavior: 'smooth' });
}
};
const handleSubmit = async () => {
setIsSubmitting(true);
setSubmitError(null);
if (!privacyAccepted) {
setSubmitError('A rendelés leadásához el kell fogadnia az Adatkezelési tájékoztatót.');
setIsSubmitting(false);
return;
}
if (!aszfAccepted) {
setSubmitError('A rendelés leadásához el kell fogadnia az Általános Szerződési Feltételeket (ÁSZF).');
setIsSubmitting(false);
return;
}
// Determine amount text
const amount =
formData.package === 'Landing Page' ? '190.000 Ft' :
formData.package === 'Pro Web' ? '350.000 Ft' :
'Egyedi Árazás';
if (isSupabaseConfigured && user) {
try {
const { error } = await supabase.from('orders').insert({
user_id: user.id,
customer_name: formData.name,
customer_email: formData.email,
package: formData.package,
status: 'new',
amount: amount,
details: formData
});
if (error) {
throw error;
}
setIsSubmitted(true);
window.scrollTo({ top: document.getElementById('order-form-container')?.offsetTop || 0, behavior: 'smooth' });
} catch (err: any) {
console.error('Error submitting order:', err);
setSubmitError('Hiba történt a rendelés elküldésekor: ' + err.message);
} finally {
setIsSubmitting(false);
}
} else {
// Fallback for Demo Mode
console.log('Demo Mode: Order Data:', formData);
await new Promise(resolve => setTimeout(resolve, 1500)); // Simulate delay
setIsSubmitted(true);
setIsSubmitting(false);
window.scrollTo({ top: document.getElementById('order-form-container')?.offsetTop || 0, behavior: 'smooth' });
}
};
// Modern Input Style Class - MotionWeb Style
const inputClass = "w-full px-4 py-3 rounded-lg border border-gray-200 bg-white text-[#111111] placeholder-gray-400 focus:ring-4 focus:ring-[#4e6bff]/10 focus:border-[#4e6bff] outline-none transition-all shadow-sm";
const labelClass = "text-sm font-semibold text-gray-800 block mb-2";
const checkboxClass = "w-5 h-5 text-[#4e6bff] rounded focus:ring-[#4e6bff] border-gray-300 flex-shrink-0 transition-colors cursor-pointer";
const ColorPickerSection = ({ label, selected, onChange }: { label: string, selected: string, onChange: (color: string) => void }) => (
<div className="space-y-2">
<label className={labelClass}>{label}</label>
<div className="flex flex-wrap gap-3">
{colorOptions.map((color) => (
<button
key={color.name}
type="button"
onClick={() => onChange(color.name)}
className={`w-9 h-9 rounded-full border flex items-center justify-center transition-all hover:scale-110 hover:shadow-md ${selected === color.name ? 'border-gray-800 ring-2 ring-offset-2 ring-gray-200 scale-110' : 'border-gray-200'}`}
style={{ backgroundColor: color.value }}
title={color.name}
>
{selected === color.name && <Check className={`w-4 h-4 ${['Fehér', 'Sárga'].includes(color.name) ? 'text-black' : 'text-white'}`} />}
</button>
))}
</div>
<p className="text-xs text-gray-500 min-h-[1.25rem]">{selected ? `Választott: ${selected}` : ''}</p>
</div>
);
if (isSubmitted) {
return (
<div id="order-form-container" className="bg-white rounded-3xl shadow-xl p-12 text-center border border-gray-100 animate-fade-in-up scroll-mt-24 max-w-4xl mx-auto">
<div className="w-24 h-24 bg-green-50 rounded-full flex items-center justify-center mx-auto mb-6 shadow-sm">
<CheckCircle className="w-12 h-12 text-green-500" />
</div>
<h2 className="text-3xl font-extrabold text-gray-900 mb-4">Rendelését sikeresen rögzítettük!</h2>
<p className="text-xl text-gray-600 mb-8 max-w-2xl mx-auto leading-relaxed">
Átirányítjuk az előlegfizetési oldalra. Amennyiben ez nem történik meg, kollégáink hamarosan felveszik Önnel a kapcsolatot.
</p>
<Button onClick={() => { setIsSubmitted(false); setCurrentStep(1); }} variant="outline">Új űrlap kitöltése</Button>
</div>
);
}
return (
<div id="order-form-container" className="bg-white rounded-3xl shadow-xl overflow-hidden border border-gray-100 scroll-mt-24 transition-all duration-500 flex flex-col relative">
{/* Light Gradient Header & Step Navigation */}
<div className="bg-gradient-to-br from-[#eef2ff] to-[#f5f3ff] pt-12 pb-8 px-6 md:px-12 border-b border-white shadow-sm">
<div className="text-center mb-10">
<h2 className="text-3xl md:text-4xl font-extrabold text-[#111827] mb-3">Rendelés Leadása</h2>
<p className="text--[#4b5563] text-lg max-w-2xl mx-auto">Töltse ki az űrlapot a pontos ajánlatadáshoz, és segítünk megvalósítani elképzeléseit.</p>
</div>
{/* Desktop Stepper Navigation */}
<div className="hidden lg:flex justify-between items-center relative max-w-6xl mx-auto px-4 mb-4">
{/* Background Line */}
<div className="absolute left-4 right-4 top-5 transform -translate-y-1/2 h-1 bg-gray-200 rounded-full -z-10" />
{/* Active Progress Line */}
<div
className="absolute left-4 top-5 transform -translate-y-1/2 h-1 bg-[#4e6bff] rounded-full -z-10 transition-all duration-500 ease-out"
style={{ width: `${((currentStep - 1) / (totalSteps - 1)) * 96}%` }} // 96% to account for padding
/>
{steps.map((step) => {
const isActive = step.id === currentStep;
const isCompleted = step.id < currentStep;
return (
<div key={step.id} className="relative flex flex-col items-center group">
<div className={`w-10 h-10 rounded-full flex items-center justify-center transition-all duration-300 border-2 z-10 ${
isActive
? 'bg-[#4e6bff] border-[#4e6bff] text-white shadow-[0_0_0_4px_rgba(78,107,255,0.2)] scale-110'
: isCompleted
? 'bg-[#4e6bff] border-[#4e6bff] text-white'
: 'bg-white border-gray-300 text-gray-400'
}`}>
{isCompleted ? <Check className="w-5 h-5" /> : <step.icon className="w-5 h-5" />}
</div>
<div className={`absolute top-12 text-xs font-semibold whitespace-nowrap transition-all duration-300 ${
isActive ? 'text-[#4e6bff] translate-y-0 opacity-100' : 'text-[#6b7280] translate-y-1'
}`}>
{step.title}
</div>
</div>
);
})}
</div>
{/* Mobile/Tablet Progress Bar */}
<div className="lg:hidden max-w-2xl mx-auto">
<div className="flex justify-between text-xs font-bold text-gray-500 mb-2 uppercase tracking-wider">
<span>Lépés {currentStep} / {totalSteps}</span>
<span className="text-[#4e6bff]">{steps[currentStep - 1].title}</span>
</div>
<div className="h-2 w-full bg-gray-200 rounded-full overflow-hidden">
<div
className="h-full bg-[#4e6bff] shadow-[0_0_10px_rgba(78,107,255,0.5)] transition-all duration-500 ease-out"
style={{ width: `${(currentStep / totalSteps) * 100}%` }}
></div>
</div>
</div>
</div>
<div className="p-6 md:p-12 min-h-[400px] bg-white">
{errors.length > 0 && (
<div className="bg-red-50 border border-red-100 text-red-600 px-4 py-3 rounded-xl flex items-start gap-3 mb-8 animate-fade-in shadow-sm">
<AlertCircle className="w-5 h-5 mt-0.5 flex-shrink-0" />
<div>
<p className="font-bold">Kérjük, javítsa a következő hibákat:</p>
<ul className="list-disc list-inside text-sm mt-1 opacity-80">
{errors.map((err, idx) => <li key={idx}>{err}</li>)}
</ul>
</div>
</div>
)}
{submitError && (
<div className="bg-red-50 border border-red-100 text-red-600 px-4 py-3 rounded-xl flex items-start gap-3 mb-8 animate-fade-in shadow-sm">
<AlertCircle className="w-5 h-5 mt-0.5 flex-shrink-0" />
<div>{submitError}</div>
</div>
)}
{/* Step 1: Kapcsolattartási adatok */}
{currentStep === 1 && (
<div className="space-y-8 animate-fade-in max-w-3xl mx-auto">
<div className="text-center mb-8">
<div className="w-16 h-16 bg-blue-50 rounded-2xl flex items-center justify-center mx-auto mb-4 text-[#4e6bff]">
<User className="w-8 h-8" />
</div>
<h3 className="text-2xl font-bold text-gray-900">Kapcsolattartási adatok</h3>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
<div>
<label className={labelClass}>Név <span className="text-red-500">*</span></label>
<input type="text" name="name" value={formData.name} onChange={handleInputChange} placeholder="Add meg a neved" className={inputClass} />
</div>
<div>
<label className={labelClass}>Cég neve <span className="text-gray-400 font-normal">(opcionális)</span></label>
<input type="text" name="company" value={formData.company} onChange={handleInputChange} placeholder="Ha van céged, írd ide a nevét" className={inputClass} />
</div>
<div>
<label className={labelClass}>E-mail cím <span className="text-red-500">*</span></label>
<input type="email" name="email" value={formData.email} onChange={handleInputChange} placeholder="valami@valami.hu" className={inputClass} readOnly />
<p className="text-xs text-gray-500 mt-1">A bejelentkezett e-mail címed.</p>
</div>
<div>
<label className={labelClass}>Telefonszám <span className="text-red-500">*</span></label>
<input type="tel" name="phone" value={formData.phone} onChange={handleInputChange} placeholder="+36..." className={inputClass} />
</div>
</div>
<div className="pt-4">
<label className={labelClass}>Választott csomag</label>
<div className="grid grid-cols-1 sm:grid-cols-3 gap-4">
{['Landing Page', 'Pro Web', 'Enterprise'].map((pkg) => (
<label key={pkg} className={`relative flex items-center gap-3 px-4 py-4 rounded-xl border-2 cursor-pointer transition-all ${formData.package === pkg ? 'border-[#4e6bff] bg-blue-50/30' : 'border-gray-100 bg-gray-50 hover:border-gray-200'}`}>
<input type="radio" name="package" value={pkg} checked={formData.package === pkg} onChange={(e) => setFormData({...formData, package: e.target.value})} className="sr-only" />
<div className={`w-5 h-5 rounded-full border-2 flex-shrink-0 flex items-center justify-center ${formData.package === pkg ? 'border-[#4e6bff]' : 'border-gray-300'}`}>
{formData.package === pkg && <div className="w-2.5 h-2.5 rounded-full bg-[#4e6bff]" />}
</div>
<span className={`font-bold text-sm sm:text-base ${formData.package === pkg ? 'text-[#4e6bff]' : 'text-gray-700'}`}>{pkg}</span>
</label>
))}
</div>
</div>
</div>
)}
{currentStep === 2 && (
<div className="space-y-8 animate-fade-in max-w-3xl mx-auto">
<div className="text-center mb-8">
<div className="w-16 h-16 bg-blue-50 rounded-2xl flex items-center justify-center mx-auto mb-4 text-[#4e6bff]">
<FileText className="w-8 h-8" />
</div>
<h3 className="text-2xl font-bold text-gray-900">A vállalkozás rövid bemutatása</h3>
</div>
<div>
<label className={labelClass}>Mivel foglalkozik a cég?</label>
<textarea name="description" rows={6} value={formData.description} onChange={handleInputChange} placeholder="Pl.: vízszereléssel és háztartási gépek javításával foglalkozunk." className={inputClass}></textarea>
<p className="text-sm text-gray-500 mt-2 bg-gray-50 p-3 rounded-lg border border-gray-100 inline-block">💡 Tipp: Írjon le mindent, amit fontosnak tart a tevékenységével kapcsolatban.</p>
</div>
</div>
)}
{currentStep === 3 && (
<div className="space-y-8 animate-fade-in max-w-3xl mx-auto">
<div className="text-center mb-8">
<div className="w-16 h-16 bg-blue-50 rounded-2xl flex items-center justify-center mx-auto mb-4 text-[#4e6bff]">
<Target className="w-8 h-8" />
</div>
<h3 className="text-2xl font-bold text-gray-900">Mi a weboldal célja?</h3>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
{['Cég bemutatása', 'Termék / szolgáltatás értékesítés', 'Időpontfoglalás vagy ügyfélszerzés', 'Hírlevél / e-mail lista építése', 'Közösségépítés', 'Egyéb'].map((goal) => (
<div key={goal}>
<label className={`flex items-center space-x-3 cursor-pointer p-4 rounded-xl border transition-all h-full shadow-sm ${formData.goals.includes(goal) ? 'border-[#4e6bff] bg-blue-50/20' : 'border-gray-200 bg-white hover:border-gray-300'}`}>
<input type="checkbox" checked={formData.goals.includes(goal)} onChange={() => handleCheckboxChange('goals', goal)} className={checkboxClass} />
<span className="text-gray-900 font-medium">{goal}</span>
</label>
{goal === 'Egyéb' && formData.goals.includes('Egyéb') && (
<div className="mt-2 animate-fade-in">
<input type="text" name="goalOther" value={formData.goalOther} onChange={handleInputChange} placeholder="Kérjük fejtse ki..." className={`${inputClass} text-sm py-2`} />
</div>
)}
</div>
))}
</div>
<div className="space-y-2 pt-4">
<label className={labelClass}>Mi számít sikernek a weboldal esetében? <span className="text-gray-400 font-normal">(opcionális)</span></label>
<input type="text" name="successCriteria" value={formData.successCriteria} onChange={handleInputChange} placeholder="Több megrendelés, több hívás, több látogató..." className={inputClass} />
</div>
</div>
)}
{currentStep === 4 && (
<div className="space-y-8 animate-fade-in max-w-3xl mx-auto">
<div className="text-center mb-8">
<div className="w-16 h-16 bg-blue-50 rounded-2xl flex items-center justify-center mx-auto mb-4 text-[#4e6bff]">
<Layout className="w-8 h-8" />
</div>
<h3 className="text-2xl font-bold text-gray-900">Tartalom és szerkezet</h3>
</div>
<div>
<label className={labelClass}>Milyen aloldalakat tervez?</label>
<div className="grid grid-cols-1 sm:grid-cols-2 gap-3">
{['Rólunk', 'Szolgáltatások', 'Termékek', 'Referenciák / Vélemények', 'Blog / Hírek', 'Kapcsolat', 'Egyéb'].map((item) => (
<div key={item}>
<label className={`flex items-center space-x-3 cursor-pointer p-3 rounded-lg border transition-all ${formData.content.includes(item) ? 'border-[#4e6bff] bg-blue-50/20' : 'border-transparent hover:bg-gray-50'}`}>
<input type="checkbox" checked={formData.content.includes(item)} onChange={() => handleCheckboxChange('content', item)} className={checkboxClass} />
<span className="text-gray-800 font-medium">{item}</span>
</label>
{item === 'Egyéb' && formData.content.includes('Egyéb') && (
<input type="text" name="contentOther" value={formData.contentOther} onChange={handleInputChange} placeholder="Egyéb oldalak..." className={`${inputClass} mt-1 py-2 text-sm`} />
)}
</div>
))}
</div>
</div>
<div className="pt-8 border-t border-gray-100">
<label className={labelClass}>Van már meglévő szöveg, kép vagy logó?</label>
<div className="flex flex-wrap gap-4 mt-3">
{['Igen', 'Nem', 'Részben'].map((opt) => (
<label key={opt} className={`flex items-center gap-3 px-5 py-3 rounded-lg border cursor-pointer transition-all ${formData.existingAssets === opt ? 'border-[#4e6bff] bg-blue-50/20 text-[#4e6bff]' : 'border-gray-200 bg-white hover:bg-gray-50 text-gray-700'}`}>
<input type="radio" name="existingAssets" value={opt} checked={formData.existingAssets === opt} onChange={(e) => setFormData({...formData, existingAssets: e.target.value as any})} className="w-4 h-4 text-[#4e6bff] focus:ring-[#4e6bff]" />
<span className="font-semibold">{opt}</span>
</label>
))}
</div>
{/* Content Upload / Link Section */}
{(formData.existingAssets === 'Igen' || formData.existingAssets === 'Részben') && (
<div className="mt-6 bg-blue-50 border border-blue-100 p-6 rounded-xl animate-fade-in">
<h4 className="font-bold text-blue-900 mb-3 flex items-center gap-2">
<Cloud className="w-5 h-5" /> Tartalom megosztása
</h4>
<p className="text-sm text-blue-800 mb-4">
Kérjük, töltse fel a meglévő tartalmakat (képek, szövegek, logó) egy felhő tárhelyre (pl. Google Drive) és ossza meg a linket, vagy töltsön fel fájlt (max 1GB).
</p>
<div className="space-y-4">
<div>
<label className={labelClass}>Felhő mappa linkje</label>
<input
type="url"
name="contentLink"
value={formData.contentLink}
onChange={handleInputChange}
placeholder="https://drive.google.com/..."
className={inputClass}
/>
</div>
<div>
<label className={labelClass}>Vagy fájl feltöltése</label>
<div className="border-2 border-dashed border-blue-200 rounded-lg p-6 bg-white text-center hover:bg-blue-50/50 transition-colors cursor-pointer relative group">
<Upload className="w-8 h-8 text-blue-400 mx-auto mb-2" />
<p className="text-sm text-gray-500">Kattintson a feltöltéshez vagy húzza ide a fájlt</p>
<p className="text-xs text-gray-400 mt-1">(Max 1GB)</p>
<input type="file" className="absolute inset-0 w-full h-full opacity-0 cursor-pointer" title="Fájl feltöltése" />
</div>
</div>
</div>
</div>
)}
</div>
</div>
)}
{currentStep === 5 && (
<div className="space-y-8 animate-fade-in max-w-4xl mx-auto">
<div className="text-center mb-8">
<div className="w-16 h-16 bg-blue-50 rounded-2xl flex items-center justify-center mx-auto mb-4 text-[#4e6bff]">
<Palette className="w-8 h-8" />
</div>
<h3 className="text-2xl font-bold text-gray-900">Design és stílus</h3>
</div>
<div className="grid grid-cols-1 md:grid-cols-3 gap-8 p-8 bg-gray-50/50 rounded-3xl border border-gray-100">
<ColorPickerSection label="Főszín" selected={formData.primaryColor} onChange={(c) => setFormData(prev => ({ ...prev, primaryColor: c }))} />
<ColorPickerSection label="Mellékszín" selected={formData.secondaryColor} onChange={(c) => setFormData(prev => ({ ...prev, secondaryColor: c }))} />
<ColorPickerSection label="Kiegyensúlyozó szín" selected={formData.balanceColor} onChange={(c) => setFormData(prev => ({ ...prev, balanceColor: c }))} />
</div>
<div className="space-y-3 pt-4">
<label className={labelClass}>Weboldal stílusa:</label>
<div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 gap-4">
{styleOptions.map((style) => (
<label key={style} className={`flex items-center space-x-3 cursor-pointer p-4 rounded-xl border shadow-sm transition-all ${formData.style.includes(style) ? 'border-[#4e6bff] bg-blue-50/20' : 'border-gray-200 bg-white hover:border-gray-300'}`}>
<input type="checkbox" checked={formData.style.includes(style)} onChange={() => handleCheckboxChange('style', style)} className={checkboxClass} />
<span className="text-gray-900 text-sm font-semibold">{style}</span>
</label>
))}
</div>
</div>
<div>
<label className={labelClass}>Célcsoport <span className="text-gray-400 font-normal">(opcionális)</span></label>
<input type="text" name="targetAudience" value={formData.targetAudience} onChange={handleInputChange} placeholder="Pl.: 1830 éves fiatalok, középkorú nők, vállalkozók stb." className={inputClass} />
</div>
</div>
)}
{currentStep === 6 && (
<div className="space-y-8 animate-fade-in max-w-3xl mx-auto">
<div className="text-center mb-8">
<div className="w-16 h-16 bg-blue-50 rounded-2xl flex items-center justify-center mx-auto mb-4 text-[#4e6bff]">
<Zap className="w-8 h-8" />
</div>
<h3 className="text-2xl font-bold text-gray-900">Funkciók és technikai igények</h3>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
{['Kapcsolatfelvételi űrlap', 'Időpontfoglalás rendszer', 'Hírlevél-feliratkozás', 'Webshop / fizetési lehetőség', 'Blog / hírek', 'Többnyelvűség', 'Képgaléria / videógaléria', 'Google Térkép integráció', 'Chat gomb (Messenger / WhatsApp)'].map((feat) => (
<label key={feat} className={`flex items-center space-x-3 cursor-pointer p-4 rounded-xl border shadow-sm transition-all ${formData.features.includes(feat) ? 'border-[#4e6bff] bg-blue-50/20' : 'border-gray-200 bg-white hover:border-gray-300'}`}>
<input type="checkbox" checked={formData.features.includes(feat)} onChange={() => handleCheckboxChange('features', feat)} className={checkboxClass} />
<span className="text-gray-900 font-medium">{feat}</span>
</label>
))}
</div>
</div>
)}
{currentStep === 7 && (
<div className="space-y-8 animate-fade-in max-w-3xl mx-auto">
<div className="text-center mb-8">
<div className="w-16 h-16 bg-blue-50 rounded-2xl flex items-center justify-center mx-auto mb-4 text-[#4e6bff]">
<Lightbulb className="w-8 h-8" />
</div>
<h3 className="text-2xl font-bold text-gray-900">Inspirációk</h3>
<p className="text-gray-500 mt-2">Segítsen megérteni az ízlését!</p>
</div>
<div className="bg-blue-50 border border-blue-100 p-5 rounded-xl text-blue-800 text-sm mb-6 flex items-start gap-3">
<div className="bg-white p-1 rounded-full"><Lightbulb className="w-4 h-4 text-[#4e6bff]" /></div>
<p>Osszon meg velünk 3 weboldalt, ami tetszik, és írja le röviden, miért (pl. színek, elrendezés, hangulat).</p>
</div>
<div className="space-y-6">
{[0, 1, 2].map((i) => (
<div key={i} className="grid grid-cols-1 md:grid-cols-2 gap-6 bg-gray-50/50 p-6 rounded-2xl border border-gray-100 hover:shadow-md transition-all">
<div>
<label className="text-xs font-bold text-gray-400 uppercase tracking-wider mb-2 block">Weboldal {i+1} URL</label>
<div className="relative">
<Globe className="absolute left-3 top-3 w-4 h-4 text-gray-400" />
<input
type="url"
value={formData.inspirations[i].url}
onChange={(e) => handleInspirationChange(i, 'url', e.target.value)}
placeholder="https://pelda.hu"
className={`${inputClass} pl-9`}
/>
</div>
</div>
<div>
<label className="text-xs font-bold text-gray-400 uppercase tracking-wider mb-2 block">Mi tetszik benne?</label>
<input
type="text"
value={formData.inspirations[i].comment}
onChange={(e) => handleInspirationChange(i, 'comment', e.target.value)}
placeholder="Színek, elrendezés, animációk..."
className={inputClass}
/>
</div>
</div>
))}
</div>
</div>
)}
{currentStep === 8 && (
<div className="space-y-8 animate-fade-in max-w-3xl mx-auto">
<div className="text-center mb-8">
<div className="w-16 h-16 bg-blue-50 rounded-2xl flex items-center justify-center mx-auto mb-4 text-[#4e6bff]">
<Settings className="w-8 h-8" />
</div>
<h3 className="text-2xl font-bold text-gray-900">Extra szolgáltatások és Megjegyzések</h3>
</div>
<div className="space-y-6">
<label className={labelClass}>Igényelt extra szolgáltatások:</label>
<div className="flex flex-wrap gap-4">
{['SEO optimalizálás', 'Szövegírás', 'Domain ügyintézés', 'Tárhely ügyintézés'].map((extra) => (
<label key={extra} className={`flex items-center space-x-3 cursor-pointer select-none p-3 rounded-lg border transition-all ${formData.extras.includes(extra) ? 'border-[#4e6bff] bg-blue-50/20' : 'border-gray-200 bg-white hover:border-gray-300'}`}>
<input type="checkbox" checked={formData.extras.includes(extra)} onChange={() => handleCheckboxChange('extras', extra)} className={checkboxClass} />
<span className="text-gray-900 font-medium">{extra}</span>
</label>
))}
</div>
{/* Dynamic Content */}
<div className="space-y-4">
{formData.extras.includes('Domain ügyintézés') && (
<div className="bg-blue-50 border border-blue-100 p-6 rounded-xl animate-fade-in">
<label className="text-sm font-semibold text-blue-900 block mb-2">Add meg a kívánt domain nevet:</label>
<div className="relative">
<Globe className="absolute left-3 top-3 w-5 h-5 text-blue-400" />
<input type="text" name="domainName" value={formData.domainName} onChange={handleInputChange} placeholder="azencegem.hu" className={`${inputClass} pl-10 border-blue-200 focus:ring-blue-300`} />
</div>
</div>
)}
{/* Domain Warning */}
{!formData.extras.includes('Domain ügyintézés') && (
<div className="bg-yellow-50 border border-yellow-100 p-6 rounded-xl flex items-start gap-4 animate-fade-in text-yellow-900 mt-2">
<div className="bg-white p-2 rounded-full shadow-sm"><Globe className="w-5 h-5 text-yellow-600" /></div>
<div className="text-sm leading-relaxed">
<strong>Figyelem:</strong> Nem jelölte be a domain ügyintézést. Kérjük győződjön meg róla, hogy rendelkezik saját domain névvel, vagy a későbbiekben tudja biztosítani a DNS beállításokat.
</div>
</div>
)}
{!formData.extras.includes('Tárhely ügyintézés') && (
<div className="bg-orange-50 border border-orange-100 p-6 rounded-xl flex items-start gap-4 animate-fade-in text-orange-900">
<div className="bg-white p-2 rounded-full shadow-sm"><Server className="w-5 h-5 text-orange-500" /></div>
<div className="text-sm leading-relaxed">
<strong>Figyelem:</strong> Mivel nem jelölte be a tárhely ügyintézést, a weboldalt átadjuk (forráskód), de az üzemeltetést és a szerver beállítását nem a MotionWeb végzi.
</div>
</div>
)}
</div>
</div>
<div className="pt-8 border-t border-gray-100">
<label className={labelClass}>Megjegyzések, további kérések</label>
<textarea name="notes" rows={5} value={formData.notes} onChange={handleInputChange} placeholder="További ötletek, kérések, megjegyzések..." className={inputClass}></textarea>
</div>
</div>
)}
{/* Step 9: Összegzés */}
{currentStep === 9 && (
<div className="space-y-8 animate-fade-in max-w-3xl mx-auto">
<div className="text-center mb-8">
<div className="w-16 h-16 bg-blue-50 rounded-2xl flex items-center justify-center mx-auto mb-4 text-[#4e6bff]">
<ClipboardCheck className="w-8 h-8" />
</div>
<h3 className="text-2xl font-bold text-gray-900">Rendelés összesítése</h3>
<p className="text-gray-500">Ellenőrizze az adatokat a véglegesítés előtt.</p>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-6 text-sm">
<div className="bg-gray-50 p-6 rounded-2xl border border-gray-200 space-y-4">
<div>
<h4 className="font-bold text-gray-400 uppercase tracking-wide text-xs mb-2">Kapcsolattartó</h4>
<p className="font-bold text-xl text-gray-900">{formData.name}</p>
<p className="text-gray-600">{formData.email}</p>
<p className="text-gray-600">{formData.phone}</p>
{formData.company && <p className="text-gray-600 font-medium mt-1">{formData.company}</p>}
</div>
<div className="pt-4 border-t border-gray-200">
<h4 className="font-bold text-gray-400 uppercase tracking-wide text-xs mb-2">Választott Csomag</h4>
<span className="inline-block bg-[#4e6bff] text-white px-4 py-1.5 rounded-full font-bold text-sm shadow-sm">{formData.package}</span>
</div>
</div>
<div className="bg-gray-50 p-6 rounded-2xl border border-gray-200 space-y-4">
<div>
<h4 className="font-bold text-gray-400 uppercase tracking-wide text-xs mb-2">Célok</h4>
<div className="flex flex-wrap gap-2">
{formData.goals.length > 0 ? formData.goals.map(g => <span key={g} className="bg-white border border-gray-200 px-3 py-1 rounded-lg text-xs font-semibold text-gray-700 shadow-sm">{g}</span>) : <span className="text-gray-400 italic">Nincs megadva</span>}
</div>
</div>
<div className="pt-4 border-t border-gray-200">
<h4 className="font-bold text-gray-400 uppercase tracking-wide text-xs mb-2">Színvilág</h4>
<div className="flex gap-3">
{formData.primaryColor && <div className="w-8 h-8 rounded-full border border-gray-200 shadow-sm" style={{background: colorOptions.find(c => c.name === formData.primaryColor)?.value}} title="Főszín"></div>}
{formData.secondaryColor && <div className="w-8 h-8 rounded-full border border-gray-200 shadow-sm" style={{background: colorOptions.find(c => c.name === formData.secondaryColor)?.value}} title="Mellékszín"></div>}
{formData.balanceColor && <div className="w-8 h-8 rounded-full border border-gray-200 shadow-sm" style={{background: colorOptions.find(c => c.name === formData.balanceColor)?.value}} title="Kiegyensúlyozó"></div>}
{!formData.primaryColor && !formData.secondaryColor && !formData.balanceColor && <span className="text-gray-400 italic">Nincs színválasztás</span>}
</div>
</div>
</div>
</div>
<div className="bg-[#4e6bff]/10 border border-[#4e6bff]/20 p-6 rounded-xl text-blue-900 text-sm">
<p className="font-bold mb-2">Mi történik a rendelés leadása után?</p>
<p className="opacity-90 leading-relaxed">
A "Rendelés leadása" gombra kattintva átirányítjuk az előlegfizetési oldalra. A sikeres tranzakciót követően kollégáink 48 órán belül felveszik Önnel a kapcsolatot a megadott elérhetőségeken a projekt indításához.
</p>
</div>
<div className="space-y-3">
<div className="flex items-start">
<div className="flex items-center h-5">
<input
id="privacy-order"
name="privacy"
type="checkbox"
required
checked={privacyAccepted}
onChange={(e) => setPrivacyAccepted(e.target.checked)}
className="h-4 w-4 text-primary focus:ring-primary border-gray-300 rounded cursor-pointer"
/>
</div>
<div className="ml-3 text-sm">
<label htmlFor="privacy-order" className="font-medium text-gray-700">
Elfogadom az <Link to="/privacy" target="_blank" className="text-primary hover:underline">Adatkezelési tájékoztatót</Link>
</label>
</div>
</div>
<div className="flex items-start">
<div className="flex items-center h-5">
<input
id="aszf-order"
name="aszf"
type="checkbox"
required
checked={aszfAccepted}
onChange={(e) => setAszfAccepted(e.target.checked)}
className="h-4 w-4 text-primary focus:ring-primary border-gray-300 rounded cursor-pointer"
/>
</div>
<div className="ml-3 text-sm">
<label htmlFor="aszf-order" className="font-medium text-gray-700">
Elfogadom az <Link to="/terms" target="_blank" className="text-primary hover:underline">Általános Szerződési Feltételeket (ÁSZF)</Link>
</label>
</div>
</div>
</div>
</div>
)}
</div>
{/* Footer Navigation */}
<div className="bg-white border-t border-gray-100 p-8 flex justify-between items-center rounded-b-2xl">
<Button
onClick={prevStep}
variant="white"
disabled={currentStep === 1 || isSubmitting}
className={`${currentStep === 1 ? 'opacity-0 pointer-events-none' : ''} border border-gray-200 text-gray-600 hover:bg-gray-50 px-6`}
>
<ArrowLeft className="w-4 h-4 mr-2" /> Vissza
</Button>
{currentStep < totalSteps ? (
<Button onClick={nextStep} className="bg-[#4e6bff] hover:bg-[#3d54cc] text-white shadow-lg shadow-blue-200 px-8 py-3 rounded-full hover:scale-105 transition-all">
Következő <ArrowRight className="w-4 h-4 ml-2" />
</Button>
) : (
<Button onClick={handleSubmit} disabled={isSubmitting} className="bg-gradient-to-r from-[#4e6bff] to-[#7c3aed] text-white shadow-xl hover:shadow-2xl hover:scale-105 transition-all px-10 py-4 text-lg rounded-full disabled:opacity-70 disabled:cursor-not-allowed">
{isSubmitting ? 'Küldés...' : (
<>Rendelés leadása <Send className="w-5 h-5 ml-2" /></>
)}
</Button>
)}
</div>
</div>
);
};

View File

@@ -0,0 +1,97 @@
import React from 'react';
import {
UserPlus,
FileText,
CreditCard,
Layout,
MessageSquare,
CheckCircle,
Globe,
ShieldCheck
} from 'lucide-react';
export const ProcessSection: React.FC = () => {
const steps = [
{
id: 1,
title: 'Regisztráció & fiók létrehozása',
description: 'Első lépésként létrehozod a fiókodat, ahol később minden rendelést és értesítést kezelni tudsz.',
icon: UserPlus
},
{
id: 2,
title: 'Kérdőív kitöltése',
description: 'Kitöltöd az irányított kérdőívet, amely alapján elkezdjük megtervezni a weboldalad.',
icon: FileText
},
{
id: 3,
title: 'Előleg fizetése',
description: 'A kérdőív után kifizeted az előleget Stripe-on keresztül, ezzel indul a fejlesztési folyamat.',
icon: CreditCard
},
{
id: 4,
title: 'Első verzió elkészítése',
description: 'Elkészítjük a bemutató weboldal első verzióját placeholder tartalmakkal.',
icon: Layout
},
{
id: 5,
title: 'Visszajelzés megadása',
description: 'Egy gyors űrlapon jelzed, hogy tetszik-e az irány, vagy milyen módosításokat szeretnél.',
icon: MessageSquare
},
{
id: 6,
title: 'Végleges rendelés & teljes fizetés',
description: 'A jóváhagyás után kifizeted a teljes árat, és elkészítjük a végleges, teljes funkcionalitású weboldalt.',
icon: CheckCircle
},
{
id: 7,
title: 'Domain csatlakoztatása',
description: 'A weboldal rákerül a megadott domainedre, és élesben is elérhetővé válik.',
icon: Globe
},
{
id: 8,
title: 'Fenntartás & támogatás',
description: 'A továbbiakban csak a fenntartási díjat kell fizetned, mi pedig gondoskodunk a stabil működésről.',
icon: ShieldCheck
}
];
return (
<section className="py-24 bg-gray-50">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div className="text-center mb-16">
<h2 className="text-3xl md:text-4xl font-bold text-gray-900 mb-4">Hogyan készül el a weboldalad?</h2>
<p className="text-lg text-gray-600 max-w-3xl mx-auto">
Átlátható folyamat, gyors és professzionális kivitelezés pontosan tudni fogod, mire számíthatsz.
</p>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-8">
{steps.map((step) => (
<div
key={step.id}
className="bg-white rounded-[24px] p-8 shadow-sm hover:shadow-lg hover:-translate-y-1 transition-all duration-300 group border border-gray-100/50"
>
<div className="flex justify-between items-start mb-6">
<div className="text-blue-500 transition-transform duration-300 group-hover:scale-110">
<step.icon className="w-8 h-8 stroke-[1.5]" />
</div>
<div className="w-8 h-8 rounded-full border-2 border-[#A78BFA] flex items-center justify-center text-sm font-bold text-[#A78BFA] group-hover:bg-[#A78BFA] group-hover:text-white transition-colors duration-300">
{step.id}
</div>
</div>
<h3 className="text-lg font-semibold text-gray-900 mb-2">{step.title}</h3>
<p className="text-gray-500 text-sm leading-relaxed">{step.description}</p>
</div>
))}
</div>
</div>
</section>
);
};

View File

@@ -0,0 +1,250 @@
import React, { useEffect, useState } from 'react';
import { useAuth } from '../context/AuthContext';
import { supabase, isSupabaseConfigured } from '../lib/supabaseClient';
import { Button } from './Button';
import { User, Save, AlertCircle, Calendar } from 'lucide-react';
export const ProfileCompleter: React.FC = () => {
const { user, refreshDemoUser } = useAuth();
const [isOpen, setIsOpen] = useState(false);
const [firstName, setFirstName] = useState('');
const [lastName, setLastName] = useState('');
const [dateOfBirth, setDateOfBirth] = useState('');
const [loading, setLoading] = useState(false);
const [checking, setChecking] = useState(false);
const [error, setError] = useState<string | null>(null);
useEffect(() => {
if (!user) {
setIsOpen(false);
return;
}
const checkProfile = async () => {
setChecking(true);
// --- DEMO MODE ---
if (!isSupabaseConfigured) {
const meta = user.user_metadata || {};
if (!meta.first_name || !meta.last_name || !meta.date_of_birth) {
setIsOpen(true);
if (meta.first_name) setFirstName(meta.first_name);
if (meta.last_name) setLastName(meta.last_name);
if (meta.date_of_birth) setDateOfBirth(meta.date_of_birth);
}
setChecking(false);
return;
}
// -----------------
try {
const { data, error } = await supabase
.from('profiles')
.select('first_name, last_name, date_of_birth')
.eq('id', user.id)
.maybeSingle();
if (error) {
console.error('Error checking profile:', error.message || error);
// If DB check fails, fallback to metadata to see if we should block the user.
// This prevents locking the user out if the DB is temporarily unavailable or misconfigured,
// provided they have the data in their auth metadata.
const meta = user.user_metadata || {};
const hasMetadata = meta.first_name && meta.last_name && meta.date_of_birth;
if (!hasMetadata) {
setIsOpen(true);
if (meta.first_name) setFirstName(meta.first_name);
if (meta.last_name) setLastName(meta.last_name);
if (meta.date_of_birth) setDateOfBirth(meta.date_of_birth);
}
return;
}
// If no profile exists, or names/dob are missing
if (!data || !data.first_name || !data.last_name || !data.date_of_birth) {
setIsOpen(true);
// Pre-fill if partial data exists
if (data?.first_name) setFirstName(data.first_name);
else if (user.user_metadata?.first_name) setFirstName(user.user_metadata.first_name);
if (data?.last_name) setLastName(data.last_name);
else if (user.user_metadata?.last_name) setLastName(user.user_metadata.last_name);
if (data?.date_of_birth) setDateOfBirth(data.date_of_birth);
else if (user.user_metadata?.date_of_birth) setDateOfBirth(user.user_metadata.date_of_birth);
}
} catch (err: any) {
console.error('Unexpected error in ProfileCompleter:', err);
} finally {
setChecking(false);
}
};
checkProfile();
}, [user]);
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
setError(null);
setLoading(true);
if (!firstName.trim() || !lastName.trim() || !dateOfBirth) {
setError('Minden mező kitöltése kötelező.');
setLoading(false);
return;
}
try {
// --- DEMO MODE UPDATE ---
if (!isSupabaseConfigured && user) {
await new Promise(resolve => setTimeout(resolve, 800)); // Fake delay
// Update local storage session
const storedSession = localStorage.getItem('demo_user_session');
if (storedSession) {
const parsed = JSON.parse(storedSession);
parsed.user.user_metadata = {
...parsed.user.user_metadata,
first_name: firstName,
last_name: lastName,
date_of_birth: dateOfBirth
};
localStorage.setItem('demo_user_session', JSON.stringify(parsed));
refreshDemoUser(); // Refresh context
}
setIsOpen(false);
return;
}
// ------------------------
if (user) {
// 1. Update Profile Table
const { error: dbError } = await supabase
.from('profiles')
.upsert({
id: user.id,
email: user.email,
first_name: firstName,
last_name: lastName,
date_of_birth: dateOfBirth,
updated_at: new Date().toISOString()
});
if (dbError) throw dbError;
// 2. Update Auth Metadata (optional, but good for consistency)
await supabase.auth.updateUser({
data: {
first_name: firstName,
last_name: lastName,
date_of_birth: dateOfBirth
}
});
setIsOpen(false);
// We do not reload here to avoid infinite loops if checks fail on reload.
// The state close is enough.
}
} catch (err: any) {
console.error('Error updating profile:', err);
setError('Hiba történt a mentés során: ' + (err.message || 'Ismeretlen hiba'));
} finally {
setLoading(false);
}
};
if (!isOpen) return null;
return (
<div className="fixed inset-0 z-[100] flex items-center justify-center p-4 bg-gray-900/70 backdrop-blur-sm">
<div className="bg-white rounded-2xl shadow-2xl w-full max-w-md overflow-hidden border border-gray-100 animate-fade-in-up">
<div className="bg-gradient-to-r from-primary to-secondary p-6 text-white text-center">
<div className="w-16 h-16 bg-white/20 rounded-full flex items-center justify-center mx-auto mb-4 backdrop-blur-md border border-white/30">
<User className="w-8 h-8 text-white" />
</div>
<h2 className="text-2xl font-bold">Hiányzó Adatok</h2>
<p className="text-blue-100 text-sm mt-2">
Kérjük, a folytatáshoz adja meg a hiányzó adatait.
</p>
</div>
<div className="p-8">
<form onSubmit={handleSubmit} className="space-y-6">
{error && (
<div className="bg-red-50 text-red-700 p-3 rounded-lg text-sm flex items-start">
<AlertCircle className="w-5 h-5 mr-2 flex-shrink-0" />
<span>{error}</span>
</div>
)}
<div className="space-y-4">
<div>
<label htmlFor="comp_lastname" className="block text-sm font-medium text-gray-700 mb-1">
Vezetéknév
</label>
<input
id="comp_lastname"
type="text"
value={lastName}
onChange={(e) => setLastName(e.target.value)}
className="w-full px-4 py-3 rounded-lg border border-gray-300 focus:ring-2 focus:ring-primary focus:border-transparent outline-none transition-all bg-white text-gray-900"
placeholder="Kovács"
required
/>
</div>
<div>
<label htmlFor="comp_firstname" className="block text-sm font-medium text-gray-700 mb-1">
Keresztnév
</label>
<input
id="comp_firstname"
type="text"
value={firstName}
onChange={(e) => setFirstName(e.target.value)}
className="w-full px-4 py-3 rounded-lg border border-gray-300 focus:ring-2 focus:ring-primary focus:border-transparent outline-none transition-all bg-white text-gray-900"
placeholder="János"
required
/>
</div>
<div>
<label htmlFor="comp_dob" className="block text-sm font-medium text-gray-700 mb-1">
Születési dátum
</label>
<div className="relative">
<input
id="comp_dob"
type="date"
value={dateOfBirth}
onChange={(e) => setDateOfBirth(e.target.value)}
className="w-full px-4 py-3 rounded-lg border border-gray-300 focus:ring-2 focus:ring-primary focus:border-transparent outline-none transition-all bg-white text-gray-900"
required
/>
<div className="absolute inset-y-0 right-0 pr-3 flex items-center pointer-events-none">
<Calendar className="h-5 w-5 text-gray-400" />
</div>
</div>
</div>
</div>
<div className="pt-2">
<Button type="submit" fullWidth disabled={loading} className="flex justify-center items-center">
{loading ? 'Mentés...' : (
<>
Adatok Mentése <Save className="ml-2 w-4 h-4" />
</>
)}
</Button>
</div>
<p className="text-xs text-center text-gray-500">
Ezekre az adatokra a számlázáshoz és a kapcsolattartáshoz van szükségünk.
</p>
</form>
</div>
</div>
</div>
);
};

View File

@@ -0,0 +1,17 @@
import React from 'react';
import { Navigate } from 'react-router-dom';
import { useAuth } from '../context/AuthContext';
export const ProtectedRoute: React.FC<React.PropsWithChildren> = ({ children }) => {
const { user, loading } = useAuth();
if (loading) {
return <div className="min-h-screen flex items-center justify-center bg-gray-50 text-primary">Betöltés...</div>;
}
if (!user) {
return <Navigate to="/auth/login" replace />;
}
return <>{children}</>;
};

View File

@@ -0,0 +1,22 @@
import { useEffect } from "react";
import { useLocation } from "react-router-dom";
export default function ScrollToTop() {
const { pathname, hash } = useLocation();
useEffect(() => {
if (hash) {
// Use setTimeout to allow DOM to render if navigating from another page
setTimeout(() => {
const element = document.getElementById(hash.replace('#', ''));
if (element) {
element.scrollIntoView({ behavior: 'smooth' });
}
}, 100);
} else {
window.scrollTo(0, 0);
}
}, [pathname, hash]);
return null;
}

View File

@@ -0,0 +1,20 @@
import React from 'react';
import { LucideIcon } from 'lucide-react';
interface ServiceCardProps {
title: string;
description: string;
Icon: LucideIcon;
}
export const ServiceCard: React.FC<ServiceCardProps> = ({ title, description, Icon }) => {
return (
<div className="bg-white p-8 rounded-2xl shadow-lg hover:shadow-2xl transition-all duration-300 border border-gray-100 group">
<div className="w-14 h-14 bg-purple-50 rounded-xl flex items-center justify-center mb-6 group-hover:bg-primary transition-colors duration-300">
<Icon className="w-7 h-7 text-primary group-hover:text-white transition-colors duration-300" />
</div>
<h3 className="text-xl font-bold mb-3 text-gray-900 group-hover:text-primary transition-colors">{title}</h3>
<p className="text-gray-600 leading-relaxed">{description}</p>
</div>
);
};

View File

@@ -0,0 +1,188 @@
import React, { useState, useEffect } from 'react';
import { X, Save, Lock, User, CheckCircle, AlertCircle, Info } from 'lucide-react';
import { Button } from './Button';
import { supabase, isSupabaseConfigured } from '../lib/supabaseClient';
import { useAuth } from '../context/AuthContext';
interface UserProfile {
id: string;
email: string;
first_name?: string;
last_name?: string;
date_of_birth: string;
}
interface SettingsModalProps {
isOpen: boolean;
onClose: () => void;
userProfile: UserProfile | null;
onUpdate: () => void;
}
export const SettingsModal: React.FC<SettingsModalProps> = ({ isOpen, onClose, userProfile, onUpdate }) => {
const { user, refreshDemoUser } = useAuth();
const [activeTab, setActiveTab] = useState<'profile' | 'security'>('profile');
const [firstName, setFirstName] = useState('');
const [lastName, setLastName] = useState('');
const [dateOfBirth, setDateOfBirth] = useState('');
const [newPassword, setNewPassword] = useState('');
const [confirmPassword, setConfirmPassword] = useState('');
const [loading, setLoading] = useState(false);
const [message, setMessage] = useState<{type: 'success' | 'error', text: string} | null>(null);
useEffect(() => {
if (isOpen && userProfile) {
setFirstName(userProfile.first_name || '');
setLastName(userProfile.last_name || '');
setDateOfBirth(userProfile.date_of_birth || '');
setNewPassword('');
setConfirmPassword('');
setMessage(null);
setActiveTab('profile');
}
}, [isOpen, userProfile]);
const commonInputStyles = "w-full px-4 py-3 rounded-xl border border-gray-300 focus:ring-4 focus:ring-primary/10 focus:border-primary outline-none transition-all bg-white text-black font-medium shadow-sm";
const handleUpdateProfile = async (e: React.FormEvent) => {
e.preventDefault();
setLoading(true);
setMessage(null);
if (!user) return;
try {
if (!isSupabaseConfigured) {
await new Promise(resolve => setTimeout(resolve, 800));
setMessage({ type: 'success', text: 'Profil frissítve (Demo Mód).' });
setTimeout(() => onUpdate(), 1000);
return;
}
const { error: dbError } = await supabase.from('profiles').update({ first_name: firstName, last_name: lastName, date_of_birth: dateOfBirth, updated_at: new Date().toISOString() }).eq('id', user.id);
if (dbError) throw dbError;
await supabase.auth.updateUser({ data: { first_name: firstName, last_name: lastName, date_of_birth: dateOfBirth } });
setMessage({ type: 'success', text: 'Adatok sikeresen mentve.' });
setTimeout(() => onUpdate(), 1000);
} catch (err: any) {
setMessage({ type: 'error', text: 'Hiba: ' + err.message });
} finally {
setLoading(false);
}
};
const handleChangePassword = async (e: React.FormEvent) => {
e.preventDefault();
setLoading(true);
setMessage(null);
if (newPassword.length < 6) { setMessage({ type: 'error', text: 'Túl rövid jelszó.' }); setLoading(false); return; }
if (newPassword !== confirmPassword) { setMessage({ type: 'error', text: 'A jelszavak nem egyeznek.' }); setLoading(false); return; }
try {
if (!isSupabaseConfigured) {
await new Promise(resolve => setTimeout(resolve, 800));
setMessage({ type: 'success', text: 'Jelszó frissítve (Demo Mód).' });
return;
}
const { error } = await supabase.auth.updateUser({ password: newPassword });
if (error) throw error;
setMessage({ type: 'success', text: 'Jelszó sikeresen módosítva.' });
setNewPassword(''); setConfirmPassword('');
} catch (err: any) {
setMessage({ type: 'error', text: 'Hiba: ' + err.message });
} finally {
setLoading(false);
}
};
if (!isOpen) return null;
return (
<div className="fixed inset-0 z-[100] flex items-center justify-center p-4 bg-gray-900/60 backdrop-blur-sm animate-fade-in">
<div className="bg-white rounded-3xl shadow-2xl w-full max-w-2xl overflow-hidden flex flex-col max-h-[90vh] border border-white/20">
<div className="p-8 border-b border-gray-100 flex justify-between items-center bg-gray-50/50">
<h2 className="text-2xl font-black text-gray-900 tracking-tighter">Fiók Beállítások</h2>
<button onClick={onClose} className="p-2 hover:bg-gray-200 rounded-full transition-colors bg-white shadow-sm border border-gray-100">
<X className="w-5 h-5 text-gray-500" />
</button>
</div>
<div className="flex border-b border-gray-100 bg-white">
<button onClick={() => { setActiveTab('profile'); setMessage(null); }} className={`flex-1 py-4 text-xs font-black uppercase tracking-widest flex items-center justify-center gap-2 transition-all ${activeTab === 'profile' ? 'text-primary border-b-4 border-primary bg-purple-50/30' : 'text-gray-400 hover:text-gray-600 hover:bg-gray-50'}`}>
<User className="w-4 h-4" /> Profil
</button>
<button onClick={() => { setActiveTab('security'); setMessage(null); }} className={`flex-1 py-4 text-xs font-black uppercase tracking-widest flex items-center justify-center gap-2 transition-all ${activeTab === 'security' ? 'text-primary border-b-4 border-primary bg-purple-50/30' : 'text-gray-400 hover:text-gray-600 hover:bg-gray-50'}`}>
<Lock className="w-4 h-4" /> Biztonság
</button>
</div>
<div className="p-10 overflow-y-auto bg-white">
{message && (
<div className={`mb-8 p-5 rounded-2xl flex items-start gap-4 animate-fade-in ${message.type === 'success' ? 'bg-green-50 text-green-800 border-2 border-green-100' : 'bg-red-50 text-red-800 border-2 border-red-100'}`}>
{message.type === 'success' ? <CheckCircle className="w-6 h-6 mt-0.5" /> : <AlertCircle className="w-6 h-6 mt-0.5" />}
<span className="font-bold text-sm">{message.text}</span>
</div>
)}
{activeTab === 'profile' && (
<form onSubmit={handleUpdateProfile} className="space-y-8">
<div className="grid grid-cols-1 md:grid-cols-2 gap-8">
<div>
<label className="block text-xs font-black text-gray-400 uppercase tracking-widest mb-3">Vezetéknév</label>
<input type="text" value={lastName} onChange={(e) => setLastName(e.target.value)} className={commonInputStyles} />
</div>
<div>
<label className="block text-xs font-black text-gray-400 uppercase tracking-widest mb-3">Keresztnév</label>
<input type="text" value={firstName} onChange={(e) => setFirstName(e.target.value)} className={commonInputStyles} />
</div>
</div>
<div>
<label className="block text-xs font-black text-gray-400 uppercase tracking-widest mb-3">Születési Dátum</label>
<input type="date" value={dateOfBirth} onChange={(e) => setDateOfBirth(e.target.value)} className={commonInputStyles} />
</div>
<div>
<label className="block text-xs font-black text-gray-400 uppercase tracking-widest mb-3">E-mail cím</label>
<input type="email" value={user?.email || ''} disabled className="w-full px-4 py-3 rounded-xl border border-gray-200 bg-gray-50 text-gray-400 cursor-not-allowed font-medium" />
</div>
<div className="pt-6 border-t border-gray-100 flex justify-end">
<Button type="submit" disabled={loading} className="font-black uppercase tracking-widest px-10 shadow-lg shadow-primary/20">
{loading ? 'Mentés...' : 'Adatok Frissítése'}
</Button>
</div>
</form>
)}
{activeTab === 'security' && (
<form onSubmit={handleChangePassword} className="space-y-8">
<div>
<label className="block text-xs font-black text-gray-400 uppercase tracking-widest mb-3">Új Jelszó</label>
<input type="password" value={newPassword} onChange={(e) => setNewPassword(e.target.value)} placeholder="Minimum 6 karakter" className={commonInputStyles} />
</div>
<div>
<label className="block text-xs font-black text-gray-400 uppercase tracking-widest mb-3">Jelszó Megerősítése</label>
<input type="password" value={confirmPassword} onChange={(e) => setConfirmPassword(e.target.value)} placeholder="Jelszó újra" className={commonInputStyles} />
</div>
<div className="bg-blue-50 p-5 rounded-2xl text-sm text-blue-900 border-2 border-blue-100 flex items-start gap-4">
<Info className="w-6 h-6 flex-shrink-0 text-blue-500" />
<p className="font-bold leading-relaxed">Biztonsági okokból a jelszó megváltoztatása után javasolt az újra-bejelentkezés minden eszközön.</p>
</div>
<div className="pt-6 border-t border-gray-100 flex justify-end">
<Button type="submit" disabled={loading} className="font-black uppercase tracking-widest px-10 shadow-lg shadow-primary/20">
{loading ? 'Folyamatban...' : 'Jelszó Mentése'}
</Button>
</div>
</form>
)}
</div>
</div>
</div>
);
};