Initial commit
This commit is contained in:
18
src_frontend_orig/components/Button.jsx
Normal file
18
src_frontend_orig/components/Button.jsx
Normal file
@@ -0,0 +1,18 @@
|
||||
import { motion, useReducedMotion } from 'framer-motion';
|
||||
|
||||
export function Button({ variant = 'primary', className = '', children, ...props }) {
|
||||
const reduceMotion = useReducedMotion();
|
||||
const variantClass =
|
||||
variant === 'ghost' ? 'btn-ghost' : variant === 'primary' ? 'btn-primary' : '';
|
||||
const combined = ['btn', variantClass, className].filter(Boolean).join(' ');
|
||||
return (
|
||||
<motion.button
|
||||
className={combined}
|
||||
whileHover={reduceMotion ? undefined : { y: -1, boxShadow: '0 4px 12px rgba(0,0,0,0.2)' }}
|
||||
whileTap={reduceMotion ? undefined : { scale: 0.98 }}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</motion.button>
|
||||
);
|
||||
}
|
||||
9
src_frontend_orig/components/Card.jsx
Normal file
9
src_frontend_orig/components/Card.jsx
Normal file
@@ -0,0 +1,9 @@
|
||||
export function Card({ title, subtitle, children }) {
|
||||
return (
|
||||
<div className="card space-y-2">
|
||||
{title && <h2 className="text-lg font-semibold">{title}</h2>}
|
||||
{subtitle && <p className="subtle">{subtitle}</p>}
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
20
src_frontend_orig/components/CountUp.jsx
Normal file
20
src_frontend_orig/components/CountUp.jsx
Normal file
@@ -0,0 +1,20 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
|
||||
export function CountUp({ value }) {
|
||||
const [v, setV] = useState(0);
|
||||
useEffect(() => {
|
||||
const start = v;
|
||||
const end = value;
|
||||
const dur = 600;
|
||||
const t0 = performance.now();
|
||||
let raf;
|
||||
const step = (t) => {
|
||||
const p = Math.min(1, (t - t0) / dur);
|
||||
setV(Math.round(start + (end - start) * p));
|
||||
if (p < 1) raf = requestAnimationFrame(step);
|
||||
};
|
||||
raf = requestAnimationFrame(step);
|
||||
return () => cancelAnimationFrame(raf);
|
||||
}, [value]);
|
||||
return <span>{v}</span>;
|
||||
}
|
||||
69
src_frontend_orig/components/Header.jsx
Normal file
69
src_frontend_orig/components/Header.jsx
Normal file
@@ -0,0 +1,69 @@
|
||||
import { CountUp } from './CountUp.jsx';
|
||||
import { ArrowPathIcon, HomeIcon } from '@heroicons/react/24/outline';
|
||||
import { motion, useReducedMotion } from 'framer-motion';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import logo from '../assets/logolang.png';
|
||||
import { Button } from './Button.jsx';
|
||||
|
||||
export function Header({ count, title = 'Kenteken Generator', onRestart, onHome }) {
|
||||
const navigate = useNavigate();
|
||||
const reduceMotion = useReducedMotion();
|
||||
return (
|
||||
<motion.header
|
||||
className="header"
|
||||
initial={reduceMotion ? { opacity: 1 } : { y: -20, opacity: 0 }}
|
||||
animate={{ y: 0, opacity: 1 }}
|
||||
transition={{ duration: reduceMotion ? 0 : 0.28 }}
|
||||
>
|
||||
<div className="header-inner">
|
||||
<div
|
||||
className="flex flex-col items-center cursor-pointer"
|
||||
onClick={() => navigate('/generate')}
|
||||
>
|
||||
<motion.img
|
||||
src={logo}
|
||||
alt="CarKiddo"
|
||||
className="h-10 w-auto"
|
||||
whileHover={reduceMotion ? undefined : { rotate: 2, y: -1 }}
|
||||
transition={{ type: 'spring', stiffness: 300, damping: 20 }}
|
||||
/>
|
||||
<motion.div
|
||||
className="header-title mt-1"
|
||||
initial={reduceMotion ? false : { opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
transition={{ duration: 0.3 }}
|
||||
>
|
||||
{title}
|
||||
</motion.div>
|
||||
</div>
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="subtle flex items-center gap-1 text-sm">
|
||||
🚗 <CountUp value={count} /> kinderauto’s
|
||||
</div>
|
||||
{onHome && (
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
className="px-3 py-1.5 text-xs flex items-center gap-1"
|
||||
onClick={onHome}
|
||||
>
|
||||
<HomeIcon className="w-4 h-4" />
|
||||
Home
|
||||
</Button>
|
||||
)}
|
||||
{onRestart && (
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
className="px-3 py-1.5 text-xs flex items-center gap-1"
|
||||
onClick={onRestart}
|
||||
>
|
||||
<ArrowPathIcon className="w-4 h-4" />
|
||||
Opnieuw
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</motion.header>
|
||||
);
|
||||
}
|
||||
15
src_frontend_orig/components/PageTransition.jsx
Normal file
15
src_frontend_orig/components/PageTransition.jsx
Normal file
@@ -0,0 +1,15 @@
|
||||
import { motion, useReducedMotion } from 'framer-motion';
|
||||
|
||||
export function PageTransition({ children }) {
|
||||
const reduce = useReducedMotion();
|
||||
return (
|
||||
<motion.div
|
||||
initial={reduce ? { opacity: 1 } : { opacity: 0, y: 8 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
exit={reduce ? { opacity: 1 } : { opacity: 0, y: -8 }}
|
||||
transition={{ duration: reduce ? 0 : 0.28 }}
|
||||
>
|
||||
{children}
|
||||
</motion.div>
|
||||
);
|
||||
}
|
||||
7
src_frontend_orig/components/PlateForm.jsx
Normal file
7
src_frontend_orig/components/PlateForm.jsx
Normal file
@@ -0,0 +1,7 @@
|
||||
export function PlateForm({ children, onSubmit }) {
|
||||
return (
|
||||
<form onSubmit={onSubmit} className="space-y-4">
|
||||
{children}
|
||||
</form>
|
||||
);
|
||||
}
|
||||
14
src_frontend_orig/components/PreviewCard.jsx
Normal file
14
src_frontend_orig/components/PreviewCard.jsx
Normal file
@@ -0,0 +1,14 @@
|
||||
import { motion } from 'framer-motion';
|
||||
|
||||
export function PreviewCard({ children }) {
|
||||
return (
|
||||
<motion.div
|
||||
className="preview-card"
|
||||
initial={{ opacity: 0, scale: 0.98 }}
|
||||
animate={{ opacity: 1, scale: 1 }}
|
||||
transition={{ duration: 0.25 }}
|
||||
>
|
||||
{children}
|
||||
</motion.div>
|
||||
);
|
||||
}
|
||||
10
src_frontend_orig/components/StatCard.jsx
Normal file
10
src_frontend_orig/components/StatCard.jsx
Normal file
@@ -0,0 +1,10 @@
|
||||
import { CountUp } from './CountUp.jsx';
|
||||
|
||||
export function StatCard({ value, label }) {
|
||||
return (
|
||||
<div className="card stat-card">
|
||||
<div className="stat-value"><CountUp value={value} /></div>
|
||||
<div className="stat-label subtle">{label}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
24
src_frontend_orig/components/Toast.jsx
Normal file
24
src_frontend_orig/components/Toast.jsx
Normal file
@@ -0,0 +1,24 @@
|
||||
import { useEffect } from 'react';
|
||||
import { motion, AnimatePresence } from 'framer-motion';
|
||||
|
||||
export function Toast({ message, onClose }) {
|
||||
useEffect(() => {
|
||||
if (!message) return;
|
||||
const t = setTimeout(() => onClose && onClose(), 3000);
|
||||
return () => clearTimeout(t);
|
||||
}, [message, onClose]);
|
||||
return (
|
||||
<AnimatePresence>
|
||||
{message && (
|
||||
<motion.div
|
||||
className="toast"
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
exit={{ opacity: 0, y: 20 }}
|
||||
>
|
||||
{message}
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user