feat: Implement marketing page layout, core sections, and shared UI components.

This commit is contained in:
Timo Knuth 2026-01-06 12:53:57 +01:00
parent 170c2e9c80
commit 2a057ae3e3
9 changed files with 44 additions and 20 deletions

View File

@ -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": []

View File

@ -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" />
) : (

View File

@ -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>

View File

@ -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>
),

View File

@ -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>

View File

@ -93,39 +93,47 @@ 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>
@ -133,10 +141,11 @@ 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="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,18 +156,20 @@ 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>

View File

@ -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',

View File

@ -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"

View File

@ -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',