feat: Implement marketing page layout, core sections, and shared UI components.
This commit is contained in:
parent
170c2e9c80
commit
2a057ae3e3
|
|
@ -24,7 +24,8 @@
|
|||
"Bash(curl:*)",
|
||||
"Bash(echo \"\n\n## CSRF Debug aktiviert!\n\nBitte teste jetzt:\n1. Browser zu http://localhost:3050/create\n2. Dynamic QR Code erstellen versuchen\n3. Server-Logs zeigen jetzt [CSRF Debug] Output\n\nIch sehe dann:\n- Ob headerToken vorhanden ist\n- Ob cookieToken vorhanden ist \n- Ob sie übereinstimmen\n\n---\n\nStripe Portal 500 Error ist separates Problem:\nhttps://dashboard.stripe.com/test/settings/billing/portal\n→ Customer Portal Configuration muss erstellt werden\n\")",
|
||||
"Bash(pkill:*)",
|
||||
"Skill(shadcn-ui)"
|
||||
"Skill(shadcn-ui)",
|
||||
"Bash(find:*)"
|
||||
],
|
||||
"deny": [],
|
||||
"ask": []
|
||||
|
|
|
|||
|
|
@ -62,8 +62,10 @@ export default function MarketingLayout({
|
|||
<button
|
||||
className="md:hidden text-gray-900"
|
||||
onClick={() => setMobileMenuOpen(!mobileMenuOpen)}
|
||||
aria-label={mobileMenuOpen ? 'Close menu' : 'Open menu'}
|
||||
aria-expanded={mobileMenuOpen}
|
||||
>
|
||||
<svg className="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<svg className="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24" aria-hidden="true">
|
||||
{mobileMenuOpen ? (
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
|
||||
) : (
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ interface FAQProps {
|
|||
|
||||
export const FAQ: React.FC<FAQProps> = ({ t }) => {
|
||||
const [openIndex, setOpenIndex] = useState<number | null>(null);
|
||||
|
||||
|
||||
const questions = [
|
||||
'account',
|
||||
'static_vs_dynamic',
|
||||
|
|
@ -40,6 +40,7 @@ export const FAQ: React.FC<FAQProps> = ({ t }) => {
|
|||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 9l-7 7-7-7" />
|
||||
</svg>
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ export const Features: React.FC<FeaturesProps> = ({ t }) => {
|
|||
{
|
||||
key: 'analytics',
|
||||
icon: (
|
||||
<svg className="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<svg className="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24" aria-hidden="true">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z" />
|
||||
</svg>
|
||||
),
|
||||
|
|
@ -21,7 +21,7 @@ export const Features: React.FC<FeaturesProps> = ({ t }) => {
|
|||
{
|
||||
key: 'customization',
|
||||
icon: (
|
||||
<svg className="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<svg className="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24" aria-hidden="true">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M7 21a4 4 0 01-4-4V5a2 2 0 012-2h4a2 2 0 012 2v12a4 4 0 01-4 4zm0 0h12a2 2 0 002-2v-4a2 2 0 00-2-2h-2.343M11 7.343l1.657-1.657a2 2 0 012.828 0l2.829 2.829a2 2 0 010 2.828l-8.486 8.485M7 17h.01" />
|
||||
</svg>
|
||||
),
|
||||
|
|
@ -30,7 +30,7 @@ export const Features: React.FC<FeaturesProps> = ({ t }) => {
|
|||
{
|
||||
key: 'unlimited',
|
||||
icon: (
|
||||
<svg className="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<svg className="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24" aria-hidden="true">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||
</svg>
|
||||
),
|
||||
|
|
|
|||
|
|
@ -86,7 +86,7 @@ export const Hero: React.FC<HeroProps> = ({ t }) => {
|
|||
{templateCards.map((card, index) => (
|
||||
<Card key={index} className={`${card.color} border-0 p-6 text-center hover:scale-105 transition-transform`}>
|
||||
<div className="text-3xl mb-2">{card.icon}</div>
|
||||
<h3 className="font-semibold text-gray-800">{card.title}</h3>
|
||||
<p className="font-semibold text-gray-800">{card.title}</p>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -45,7 +45,7 @@ export const InstantGenerator: React.FC<InstantGeneratorProps> = ({ t }) => {
|
|||
const svgData = new XMLSerializer().serializeToString(svg);
|
||||
const blob = new Blob([svgData], { type: 'image/svg+xml' });
|
||||
const url = URL.createObjectURL(blob);
|
||||
|
||||
|
||||
img.onload = () => {
|
||||
canvas.width = size;
|
||||
canvas.height = size;
|
||||
|
|
@ -93,50 +93,59 @@ export const InstantGenerator: React.FC<InstantGeneratorProps> = ({ t }) => {
|
|||
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||||
<label htmlFor="foreground-color" className="block text-sm font-medium text-gray-700 mb-2">
|
||||
{t.generator.foreground}
|
||||
</label>
|
||||
<div className="flex items-center space-x-2">
|
||||
<input
|
||||
id="foreground-color"
|
||||
type="color"
|
||||
value={foregroundColor}
|
||||
onChange={(e) => setForegroundColor(e.target.value)}
|
||||
className="w-12 h-10 rounded border border-gray-300"
|
||||
aria-label="Foreground color picker"
|
||||
/>
|
||||
<Input
|
||||
id="foreground-color-text"
|
||||
value={foregroundColor}
|
||||
onChange={(e) => setForegroundColor(e.target.value)}
|
||||
className="flex-1"
|
||||
aria-label="Foreground color hex value"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||||
<label htmlFor="background-color" className="block text-sm font-medium text-gray-700 mb-2">
|
||||
{t.generator.background}
|
||||
</label>
|
||||
<div className="flex items-center space-x-2">
|
||||
<input
|
||||
id="background-color"
|
||||
type="color"
|
||||
value={backgroundColor}
|
||||
onChange={(e) => setBackgroundColor(e.target.value)}
|
||||
className="w-12 h-10 rounded border border-gray-300"
|
||||
aria-label="Background color picker"
|
||||
/>
|
||||
<Input
|
||||
id="background-color-text"
|
||||
value={backgroundColor}
|
||||
onChange={(e) => setBackgroundColor(e.target.value)}
|
||||
className="flex-1"
|
||||
aria-label="Background color hex value"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||||
<label htmlFor="corner-style" className="block text-sm font-medium text-gray-700 mb-2">
|
||||
{t.generator.corners}
|
||||
</label>
|
||||
<select
|
||||
id="corner-style"
|
||||
value={cornerStyle}
|
||||
onChange={(e) => setCornerStyle(e.target.value)}
|
||||
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-primary-500"
|
||||
|
|
@ -147,21 +156,23 @@ export const InstantGenerator: React.FC<InstantGeneratorProps> = ({ t }) => {
|
|||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||||
<label htmlFor="qr-size" className="block text-sm font-medium text-gray-700 mb-2">
|
||||
{t.generator.size}
|
||||
</label>
|
||||
<input
|
||||
id="qr-size"
|
||||
type="range"
|
||||
min="100"
|
||||
max="400"
|
||||
value={size}
|
||||
onChange={(e) => setSize(Number(e.target.value))}
|
||||
className="w-full"
|
||||
aria-label={`QR code size: ${size} pixels`}
|
||||
/>
|
||||
<div className="text-sm text-gray-500 text-center mt-1">{size}px</div>
|
||||
<div className="text-sm text-gray-500 text-center mt-1" aria-hidden="true">{size}px</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div className="flex items-center justify-between">
|
||||
<Badge variant={hasGoodContrast ? 'success' : 'warning'}>
|
||||
{hasGoodContrast ? t.generator.contrast_good : 'Low contrast'}
|
||||
|
|
@ -201,7 +212,7 @@ export const InstantGenerator: React.FC<InstantGeneratorProps> = ({ t }) => {
|
|||
/>
|
||||
</div>
|
||||
) : (
|
||||
<div
|
||||
<div
|
||||
className="bg-gray-200 flex items-center justify-center text-gray-500"
|
||||
style={{ width: 200, height: 200 }}
|
||||
>
|
||||
|
|
|
|||
|
|
@ -7,7 +7,10 @@ interface InputProps extends React.InputHTMLAttributes<HTMLInputElement> {
|
|||
}
|
||||
|
||||
export const Input = React.forwardRef<HTMLInputElement, InputProps>(
|
||||
({ className, type, label, error, onInvalid, ...props }, ref) => {
|
||||
({ className, type, label, error, onInvalid, id, ...props }, ref) => {
|
||||
// Generate a unique id for accessibility if not provided
|
||||
const inputId = id || (label ? label.toLowerCase().replace(/\s+/g, '-') : undefined);
|
||||
|
||||
// Default English validation message
|
||||
const handleInvalid = (e: React.InvalidEvent<HTMLInputElement>) => {
|
||||
e.target.setCustomValidity('Please fill out this field.');
|
||||
|
|
@ -21,11 +24,12 @@ export const Input = React.forwardRef<HTMLInputElement, InputProps>(
|
|||
return (
|
||||
<div className="space-y-1">
|
||||
{label && (
|
||||
<label className="block text-sm font-medium text-gray-700">
|
||||
<label htmlFor={inputId} className="block text-sm font-medium text-gray-700">
|
||||
{label}
|
||||
</label>
|
||||
)}
|
||||
<input
|
||||
id={inputId}
|
||||
type={type}
|
||||
className={cn(
|
||||
'flex h-10 w-full rounded-lg border border-gray-300 bg-white px-3 py-2 text-sm ring-offset-white file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-gray-500 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary-500 focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50',
|
||||
|
|
|
|||
|
|
@ -41,6 +41,7 @@ export function ScrollToTop() {
|
|||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
|
|
|
|||
|
|
@ -8,15 +8,19 @@ interface SelectProps extends React.SelectHTMLAttributes<HTMLSelectElement> {
|
|||
}
|
||||
|
||||
export const Select = React.forwardRef<HTMLSelectElement, SelectProps>(
|
||||
({ className, label, error, options, ...props }, ref) => {
|
||||
({ className, label, error, options, id, ...props }, ref) => {
|
||||
// Generate a unique id for accessibility if not provided
|
||||
const selectId = id || (label ? `select-${label.toLowerCase().replace(/\s+/g, '-')}` : undefined);
|
||||
|
||||
return (
|
||||
<div className="space-y-1">
|
||||
{label && (
|
||||
<label className="block text-sm font-medium text-gray-700">
|
||||
<label htmlFor={selectId} className="block text-sm font-medium text-gray-700">
|
||||
{label}
|
||||
</label>
|
||||
)}
|
||||
<select
|
||||
id={selectId}
|
||||
className={cn(
|
||||
'flex h-10 w-full rounded-lg border border-gray-300 bg-white px-3 py-2 text-sm ring-offset-white focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary-500 focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50',
|
||||
error && 'border-red-500 focus-visible:ring-red-500',
|
||||
|
|
|
|||
Loading…
Reference in New Issue