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(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(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:*)",
|
"Bash(pkill:*)",
|
||||||
"Skill(shadcn-ui)"
|
"Skill(shadcn-ui)",
|
||||||
|
"Bash(find:*)"
|
||||||
],
|
],
|
||||||
"deny": [],
|
"deny": [],
|
||||||
"ask": []
|
"ask": []
|
||||||
|
|
|
||||||
|
|
@ -62,8 +62,10 @@ export default function MarketingLayout({
|
||||||
<button
|
<button
|
||||||
className="md:hidden text-gray-900"
|
className="md:hidden text-gray-900"
|
||||||
onClick={() => setMobileMenuOpen(!mobileMenuOpen)}
|
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 ? (
|
{mobileMenuOpen ? (
|
||||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
|
<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 }) => {
|
export const FAQ: React.FC<FAQProps> = ({ t }) => {
|
||||||
const [openIndex, setOpenIndex] = useState<number | null>(null);
|
const [openIndex, setOpenIndex] = useState<number | null>(null);
|
||||||
|
|
||||||
const questions = [
|
const questions = [
|
||||||
'account',
|
'account',
|
||||||
'static_vs_dynamic',
|
'static_vs_dynamic',
|
||||||
|
|
@ -40,6 +40,7 @@ export const FAQ: React.FC<FAQProps> = ({ t }) => {
|
||||||
fill="none"
|
fill="none"
|
||||||
stroke="currentColor"
|
stroke="currentColor"
|
||||||
viewBox="0 0 24 24"
|
viewBox="0 0 24 24"
|
||||||
|
aria-hidden="true"
|
||||||
>
|
>
|
||||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 9l-7 7-7-7" />
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 9l-7 7-7-7" />
|
||||||
</svg>
|
</svg>
|
||||||
|
|
|
||||||
|
|
@ -12,7 +12,7 @@ export const Features: React.FC<FeaturesProps> = ({ t }) => {
|
||||||
{
|
{
|
||||||
key: 'analytics',
|
key: 'analytics',
|
||||||
icon: (
|
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" />
|
<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>
|
</svg>
|
||||||
),
|
),
|
||||||
|
|
@ -21,7 +21,7 @@ export const Features: React.FC<FeaturesProps> = ({ t }) => {
|
||||||
{
|
{
|
||||||
key: 'customization',
|
key: 'customization',
|
||||||
icon: (
|
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" />
|
<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>
|
</svg>
|
||||||
),
|
),
|
||||||
|
|
@ -30,7 +30,7 @@ export const Features: React.FC<FeaturesProps> = ({ t }) => {
|
||||||
{
|
{
|
||||||
key: 'unlimited',
|
key: 'unlimited',
|
||||||
icon: (
|
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" />
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||||
</svg>
|
</svg>
|
||||||
),
|
),
|
||||||
|
|
|
||||||
|
|
@ -86,7 +86,7 @@ export const Hero: React.FC<HeroProps> = ({ t }) => {
|
||||||
{templateCards.map((card, index) => (
|
{templateCards.map((card, index) => (
|
||||||
<Card key={index} className={`${card.color} border-0 p-6 text-center hover:scale-105 transition-transform`}>
|
<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>
|
<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>
|
</Card>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -45,7 +45,7 @@ export const InstantGenerator: React.FC<InstantGeneratorProps> = ({ t }) => {
|
||||||
const svgData = new XMLSerializer().serializeToString(svg);
|
const svgData = new XMLSerializer().serializeToString(svg);
|
||||||
const blob = new Blob([svgData], { type: 'image/svg+xml' });
|
const blob = new Blob([svgData], { type: 'image/svg+xml' });
|
||||||
const url = URL.createObjectURL(blob);
|
const url = URL.createObjectURL(blob);
|
||||||
|
|
||||||
img.onload = () => {
|
img.onload = () => {
|
||||||
canvas.width = size;
|
canvas.width = size;
|
||||||
canvas.height = size;
|
canvas.height = size;
|
||||||
|
|
@ -93,50 +93,59 @@ export const InstantGenerator: React.FC<InstantGeneratorProps> = ({ t }) => {
|
||||||
|
|
||||||
<div className="grid grid-cols-2 gap-4">
|
<div className="grid grid-cols-2 gap-4">
|
||||||
<div>
|
<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}
|
{t.generator.foreground}
|
||||||
</label>
|
</label>
|
||||||
<div className="flex items-center space-x-2">
|
<div className="flex items-center space-x-2">
|
||||||
<input
|
<input
|
||||||
|
id="foreground-color"
|
||||||
type="color"
|
type="color"
|
||||||
value={foregroundColor}
|
value={foregroundColor}
|
||||||
onChange={(e) => setForegroundColor(e.target.value)}
|
onChange={(e) => setForegroundColor(e.target.value)}
|
||||||
className="w-12 h-10 rounded border border-gray-300"
|
className="w-12 h-10 rounded border border-gray-300"
|
||||||
|
aria-label="Foreground color picker"
|
||||||
/>
|
/>
|
||||||
<Input
|
<Input
|
||||||
|
id="foreground-color-text"
|
||||||
value={foregroundColor}
|
value={foregroundColor}
|
||||||
onChange={(e) => setForegroundColor(e.target.value)}
|
onChange={(e) => setForegroundColor(e.target.value)}
|
||||||
className="flex-1"
|
className="flex-1"
|
||||||
|
aria-label="Foreground color hex value"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</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}
|
{t.generator.background}
|
||||||
</label>
|
</label>
|
||||||
<div className="flex items-center space-x-2">
|
<div className="flex items-center space-x-2">
|
||||||
<input
|
<input
|
||||||
|
id="background-color"
|
||||||
type="color"
|
type="color"
|
||||||
value={backgroundColor}
|
value={backgroundColor}
|
||||||
onChange={(e) => setBackgroundColor(e.target.value)}
|
onChange={(e) => setBackgroundColor(e.target.value)}
|
||||||
className="w-12 h-10 rounded border border-gray-300"
|
className="w-12 h-10 rounded border border-gray-300"
|
||||||
|
aria-label="Background color picker"
|
||||||
/>
|
/>
|
||||||
<Input
|
<Input
|
||||||
|
id="background-color-text"
|
||||||
value={backgroundColor}
|
value={backgroundColor}
|
||||||
onChange={(e) => setBackgroundColor(e.target.value)}
|
onChange={(e) => setBackgroundColor(e.target.value)}
|
||||||
className="flex-1"
|
className="flex-1"
|
||||||
|
aria-label="Background color hex value"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="grid grid-cols-2 gap-4">
|
<div className="grid grid-cols-2 gap-4">
|
||||||
<div>
|
<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}
|
{t.generator.corners}
|
||||||
</label>
|
</label>
|
||||||
<select
|
<select
|
||||||
|
id="corner-style"
|
||||||
value={cornerStyle}
|
value={cornerStyle}
|
||||||
onChange={(e) => setCornerStyle(e.target.value)}
|
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"
|
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>
|
||||||
|
|
||||||
<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}
|
{t.generator.size}
|
||||||
</label>
|
</label>
|
||||||
<input
|
<input
|
||||||
|
id="qr-size"
|
||||||
type="range"
|
type="range"
|
||||||
min="100"
|
min="100"
|
||||||
max="400"
|
max="400"
|
||||||
value={size}
|
value={size}
|
||||||
onChange={(e) => setSize(Number(e.target.value))}
|
onChange={(e) => setSize(Number(e.target.value))}
|
||||||
className="w-full"
|
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>
|
</div>
|
||||||
|
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<Badge variant={hasGoodContrast ? 'success' : 'warning'}>
|
<Badge variant={hasGoodContrast ? 'success' : 'warning'}>
|
||||||
{hasGoodContrast ? t.generator.contrast_good : 'Low contrast'}
|
{hasGoodContrast ? t.generator.contrast_good : 'Low contrast'}
|
||||||
|
|
@ -201,7 +212,7 @@ export const InstantGenerator: React.FC<InstantGeneratorProps> = ({ t }) => {
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div
|
<div
|
||||||
className="bg-gray-200 flex items-center justify-center text-gray-500"
|
className="bg-gray-200 flex items-center justify-center text-gray-500"
|
||||||
style={{ width: 200, height: 200 }}
|
style={{ width: 200, height: 200 }}
|
||||||
>
|
>
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,10 @@ interface InputProps extends React.InputHTMLAttributes<HTMLInputElement> {
|
||||||
}
|
}
|
||||||
|
|
||||||
export const Input = React.forwardRef<HTMLInputElement, InputProps>(
|
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
|
// Default English validation message
|
||||||
const handleInvalid = (e: React.InvalidEvent<HTMLInputElement>) => {
|
const handleInvalid = (e: React.InvalidEvent<HTMLInputElement>) => {
|
||||||
e.target.setCustomValidity('Please fill out this field.');
|
e.target.setCustomValidity('Please fill out this field.');
|
||||||
|
|
@ -21,11 +24,12 @@ export const Input = React.forwardRef<HTMLInputElement, InputProps>(
|
||||||
return (
|
return (
|
||||||
<div className="space-y-1">
|
<div className="space-y-1">
|
||||||
{label && (
|
{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}
|
||||||
</label>
|
</label>
|
||||||
)}
|
)}
|
||||||
<input
|
<input
|
||||||
|
id={inputId}
|
||||||
type={type}
|
type={type}
|
||||||
className={cn(
|
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',
|
'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"
|
stroke="currentColor"
|
||||||
viewBox="0 0 24 24"
|
viewBox="0 0 24 24"
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
aria-hidden="true"
|
||||||
>
|
>
|
||||||
<path
|
<path
|
||||||
strokeLinecap="round"
|
strokeLinecap="round"
|
||||||
|
|
|
||||||
|
|
@ -8,15 +8,19 @@ interface SelectProps extends React.SelectHTMLAttributes<HTMLSelectElement> {
|
||||||
}
|
}
|
||||||
|
|
||||||
export const Select = React.forwardRef<HTMLSelectElement, SelectProps>(
|
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 (
|
return (
|
||||||
<div className="space-y-1">
|
<div className="space-y-1">
|
||||||
{label && (
|
{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}
|
||||||
</label>
|
</label>
|
||||||
)}
|
)}
|
||||||
<select
|
<select
|
||||||
|
id={selectId}
|
||||||
className={cn(
|
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',
|
'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',
|
error && 'border-red-500 focus-visible:ring-red-500',
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue