feat: Add 19 free QR code tools with SEO optimization
- Added PayPal, Zoom, Teams QR generators - Added lazy loading for html-to-image (performance) - Created 19 OG images for social sharing - Added robots.txt and updated sitemap - Fixed mobile navigation with accordion menu - Added 7 color options per generator - Fixed crypto QR with universal/wallet mode toggle - Hero QR codes all point to qrmaster.net
This commit is contained in:
@@ -74,88 +74,74 @@ END:VCARD`;
|
||||
console.log(`DYNAMIC QR [${qr.title}]: ${qrUrl}`);
|
||||
}
|
||||
|
||||
const downloadQR = (format: 'png' | 'svg') => {
|
||||
const svg = document.querySelector(`#qr-${qr.id} svg`);
|
||||
if (!svg) return;
|
||||
const downloadQR = async (format: 'png' | 'svg') => {
|
||||
// Use the tight download wrapper
|
||||
const container = document.querySelector(`#qr-download-${qr.id}`);
|
||||
if (!container) return;
|
||||
|
||||
if (format === 'svg') {
|
||||
let svgData = new XMLSerializer().serializeToString(svg);
|
||||
// Dynamic import of html-to-image
|
||||
const { toPng } = await import('html-to-image');
|
||||
|
||||
// If rounded corners, wrap in a clipped SVG
|
||||
if (qr.style?.cornerStyle === 'rounded') {
|
||||
const width = svg.getAttribute('width') || '96';
|
||||
const height = svg.getAttribute('height') || '96';
|
||||
const borderRadius = 10; // Smaller radius for dashboard
|
||||
try {
|
||||
if (format === 'png') {
|
||||
const dataUrl = await toPng(container as HTMLElement, {
|
||||
cacheBust: true,
|
||||
pixelRatio: 3,
|
||||
backgroundColor: '#ffffff' // White background for clean export
|
||||
});
|
||||
const link = document.createElement('a');
|
||||
link.download = `${qr.title.replace(/\s+/g, '-').toLowerCase()}.png`;
|
||||
link.href = dataUrl;
|
||||
link.click();
|
||||
} else {
|
||||
// For SVG, if no frame, export just the QR code SVG for vector quality
|
||||
// If frame exists, use toPng as fallback since HTML-to-SVG is complex
|
||||
if (qr.style?.frameType && qr.style.frameType !== 'none') {
|
||||
// Frame exists - use PNG for full capture
|
||||
const dataUrl = await toPng(container as HTMLElement, {
|
||||
cacheBust: true,
|
||||
pixelRatio: 3,
|
||||
backgroundColor: '#ffffff'
|
||||
});
|
||||
const link = document.createElement('a');
|
||||
link.download = `${qr.title.replace(/\s+/g, '-').toLowerCase()}.png`;
|
||||
link.href = dataUrl;
|
||||
link.click();
|
||||
} else {
|
||||
// No frame - export clean SVG from the svg wrapper
|
||||
const svgContainer = document.querySelector(`#qr-svg-${qr.id}`);
|
||||
const svg = svgContainer?.querySelector('svg');
|
||||
if (svg) {
|
||||
let svgData = new XMLSerializer().serializeToString(svg);
|
||||
|
||||
svgData = `<svg xmlns="http://www.w3.org/2000/svg" width="${width}" height="${height}" viewBox="0 0 ${width} ${height}">
|
||||
<defs>
|
||||
<clipPath id="rounded-corners-${qr.id}">
|
||||
<rect x="0" y="0" width="${width}" height="${height}" rx="${borderRadius}" ry="${borderRadius}"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
<g clip-path="url(#rounded-corners-${qr.id})">
|
||||
${svgData}
|
||||
</g>
|
||||
</svg>`;
|
||||
}
|
||||
if (qr.style?.cornerStyle === 'rounded') {
|
||||
const width = svg.getAttribute('width') || '96';
|
||||
const height = svg.getAttribute('height') || '96';
|
||||
const borderRadius = 10;
|
||||
svgData = `<svg xmlns="http://www.w3.org/2000/svg" width="${width}" height="${height}" viewBox="0 0 ${width} ${height}">
|
||||
<defs>
|
||||
<clipPath id="rounded-corners-${qr.id}">
|
||||
<rect x="0" y="0" width="${width}" height="${height}" rx="${borderRadius}" ry="${borderRadius}"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
<g clip-path="url(#rounded-corners-${qr.id})">
|
||||
${svgData}
|
||||
</g>
|
||||
</svg>`;
|
||||
}
|
||||
|
||||
const blob = new Blob([svgData], { type: 'image/svg+xml' });
|
||||
const url = URL.createObjectURL(blob);
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
a.download = `${qr.title.replace(/\s+/g, '-').toLowerCase()}.svg`;
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
document.body.removeChild(a);
|
||||
URL.revokeObjectURL(url);
|
||||
} else {
|
||||
// Convert SVG to PNG
|
||||
const canvas = document.createElement('canvas');
|
||||
const ctx = canvas.getContext('2d');
|
||||
if (!ctx) return;
|
||||
|
||||
const img = new Image();
|
||||
const svgData = new XMLSerializer().serializeToString(svg);
|
||||
const blob = new Blob([svgData], { type: 'image/svg+xml' });
|
||||
const url = URL.createObjectURL(blob);
|
||||
|
||||
img.onload = () => {
|
||||
canvas.width = 300;
|
||||
canvas.height = 300;
|
||||
|
||||
// Apply rounded corners if needed
|
||||
if (qr.style?.cornerStyle === 'rounded') {
|
||||
const borderRadius = 30; // Scale up for 300px canvas
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(borderRadius, 0);
|
||||
ctx.lineTo(300 - borderRadius, 0);
|
||||
ctx.quadraticCurveTo(300, 0, 300, borderRadius);
|
||||
ctx.lineTo(300, 300 - borderRadius);
|
||||
ctx.quadraticCurveTo(300, 300, 300 - borderRadius, 300);
|
||||
ctx.lineTo(borderRadius, 300);
|
||||
ctx.quadraticCurveTo(0, 300, 0, 300 - borderRadius);
|
||||
ctx.lineTo(0, borderRadius);
|
||||
ctx.quadraticCurveTo(0, 0, borderRadius, 0);
|
||||
ctx.closePath();
|
||||
ctx.clip();
|
||||
}
|
||||
|
||||
ctx.drawImage(img, 0, 0, 300, 300);
|
||||
canvas.toBlob((blob) => {
|
||||
if (blob) {
|
||||
const blob = new Blob([svgData], { type: 'image/svg+xml' });
|
||||
const url = URL.createObjectURL(blob);
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
a.download = `${qr.title.replace(/\s+/g, '-').toLowerCase()}.png`;
|
||||
document.body.appendChild(a);
|
||||
a.download = `${qr.title.replace(/\s+/g, '-').toLowerCase()}.svg`;
|
||||
a.click();
|
||||
document.body.removeChild(a);
|
||||
URL.revokeObjectURL(url);
|
||||
}
|
||||
});
|
||||
URL.revokeObjectURL(url);
|
||||
};
|
||||
img.src = url;
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Download failed:', err);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -193,21 +179,48 @@ END:VCARD`;
|
||||
</Dropdown>
|
||||
</div>
|
||||
|
||||
<div id={`qr-${qr.id}`} className="flex items-center justify-center bg-gray-50 rounded-lg p-4 mb-3">
|
||||
<div className={qr.style?.cornerStyle === 'rounded' ? 'rounded-lg overflow-hidden' : ''}>
|
||||
<QRCodeSVG
|
||||
value={qrUrl}
|
||||
size={96}
|
||||
fgColor={qr.style?.foregroundColor || '#000000'}
|
||||
bgColor={qr.style?.backgroundColor || '#FFFFFF'}
|
||||
level="H"
|
||||
imageSettings={qr.style?.imageSettings ? {
|
||||
src: qr.style.imageSettings.src,
|
||||
height: qr.style.imageSettings.height * (96 / 200), // Scale logo for smaller QR
|
||||
width: qr.style.imageSettings.width * (96 / 200),
|
||||
excavate: qr.style.imageSettings.excavate,
|
||||
} : undefined}
|
||||
/>
|
||||
<div className="flex flex-col items-center justify-center bg-gray-50 rounded-lg p-4 mb-3">
|
||||
{/* Download wrapper - tightly wraps content */}
|
||||
<div id={`qr-download-${qr.id}`} className="inline-flex flex-col items-center bg-white p-4 rounded-xl">
|
||||
{/* Frame Label */}
|
||||
{qr.style?.frameType && qr.style.frameType !== 'none' && (
|
||||
<div
|
||||
className="mb-3 px-4 py-1.5 rounded-full font-bold text-xs tracking-widest uppercase shadow-sm text-white"
|
||||
style={{ backgroundColor: qr.style?.foregroundColor || '#000000' }}
|
||||
>
|
||||
{qr.style.frameType === 'scanme' ? 'Scan Me' :
|
||||
qr.style.frameType === 'website' ? 'Website' :
|
||||
qr.style.frameType === 'visit' ? 'Visit' :
|
||||
qr.style.frameType === 'callme' ? 'Call Me' :
|
||||
qr.style.frameType === 'call' ? 'Call' :
|
||||
qr.style.frameType === 'findus' ? 'Find Us' :
|
||||
qr.style.frameType === 'navigate' ? 'Navigate' :
|
||||
qr.style.frameType === 'contact' ? 'Contact' :
|
||||
qr.style.frameType === 'save' ? 'Save' :
|
||||
qr.style.frameType === 'textme' ? 'Text Me' :
|
||||
qr.style.frameType === 'message' ? 'Message' :
|
||||
qr.style.frameType === 'chatme' ? 'Chat Me' :
|
||||
qr.style.frameType === 'whatsapp' ? 'WhatsApp' :
|
||||
qr.style.frameType === 'read' ? 'Read' :
|
||||
qr.style.frameType === 'info' ? 'Info' :
|
||||
qr.style.frameType.charAt(0).toUpperCase() + qr.style.frameType.slice(1)}
|
||||
</div>
|
||||
)}
|
||||
<div id={`qr-svg-${qr.id}`} className={qr.style?.cornerStyle === 'rounded' ? 'rounded-lg overflow-hidden' : ''}>
|
||||
<QRCodeSVG
|
||||
value={qrUrl}
|
||||
size={96}
|
||||
fgColor={qr.style?.foregroundColor || '#000000'}
|
||||
bgColor={qr.style?.backgroundColor || '#FFFFFF'}
|
||||
level="H"
|
||||
imageSettings={qr.style?.imageSettings ? {
|
||||
src: qr.style.imageSettings.src,
|
||||
height: qr.style.imageSettings.height * (96 / 200), // Scale logo for smaller QR
|
||||
width: qr.style.imageSettings.width * (96 / 200),
|
||||
excavate: qr.style.imageSettings.excavate,
|
||||
} : undefined}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
253
src/components/marketing/FreeToolsGrid.tsx
Normal file
253
src/components/marketing/FreeToolsGrid.tsx
Normal file
@@ -0,0 +1,253 @@
|
||||
'use client';
|
||||
|
||||
import React from 'react';
|
||||
import Link from 'next/link';
|
||||
import { motion, Variants } from 'framer-motion';
|
||||
import {
|
||||
Link as LinkIcon,
|
||||
User,
|
||||
Mail,
|
||||
Calendar,
|
||||
Facebook,
|
||||
Instagram,
|
||||
Phone,
|
||||
MessageSquare,
|
||||
Type,
|
||||
Music,
|
||||
Twitter,
|
||||
MessageCircle,
|
||||
Wifi,
|
||||
Youtube,
|
||||
Bitcoin,
|
||||
MapPin,
|
||||
CreditCard,
|
||||
Video,
|
||||
Users
|
||||
} from 'lucide-react';
|
||||
|
||||
const TOOLS = [
|
||||
{
|
||||
icon: LinkIcon,
|
||||
name: 'URL',
|
||||
description: 'Open any website',
|
||||
href: '/tools/url-qr-code',
|
||||
color: 'text-blue-500',
|
||||
bg: 'bg-blue-50'
|
||||
},
|
||||
{
|
||||
icon: User,
|
||||
name: 'vCard',
|
||||
description: 'Share contact details',
|
||||
href: '/tools/vcard-qr-code',
|
||||
color: 'text-rose-500',
|
||||
bg: 'bg-rose-50'
|
||||
},
|
||||
{
|
||||
icon: Type,
|
||||
name: 'Text',
|
||||
description: 'Display plain text',
|
||||
href: '/tools/text-qr-code',
|
||||
color: 'text-slate-500',
|
||||
bg: 'bg-slate-50'
|
||||
},
|
||||
{
|
||||
icon: Mail,
|
||||
name: 'Email',
|
||||
description: 'Send an email',
|
||||
href: '/tools/email-qr-code',
|
||||
color: 'text-red-500',
|
||||
bg: 'bg-red-50'
|
||||
},
|
||||
{
|
||||
icon: MessageSquare,
|
||||
name: 'SMS',
|
||||
description: 'Send a text message',
|
||||
href: '/tools/sms-qr-code',
|
||||
color: 'text-green-500',
|
||||
bg: 'bg-green-50'
|
||||
},
|
||||
{
|
||||
icon: Wifi,
|
||||
name: 'WiFi',
|
||||
description: 'Connect to WiFi',
|
||||
href: '/tools/wifi-qr-code',
|
||||
color: 'text-indigo-500',
|
||||
bg: 'bg-indigo-50'
|
||||
},
|
||||
{
|
||||
icon: Bitcoin,
|
||||
name: 'Crypto',
|
||||
description: 'Receive payments',
|
||||
href: '/tools/crypto-qr-code',
|
||||
color: 'text-orange-500',
|
||||
bg: 'bg-orange-50'
|
||||
},
|
||||
{
|
||||
icon: Calendar,
|
||||
name: 'Event',
|
||||
description: 'Save calendar event',
|
||||
href: '/tools/event-qr-code',
|
||||
color: 'text-violet-500',
|
||||
bg: 'bg-violet-50'
|
||||
},
|
||||
{
|
||||
icon: Facebook,
|
||||
name: 'Facebook',
|
||||
description: 'Open Facebook page',
|
||||
href: '/tools/facebook-qr-code',
|
||||
color: 'text-blue-600',
|
||||
bg: 'bg-blue-50'
|
||||
},
|
||||
{
|
||||
icon: Instagram,
|
||||
name: 'Instagram',
|
||||
description: 'Open Instagram profile',
|
||||
href: '/tools/instagram-qr-code',
|
||||
color: 'text-pink-500',
|
||||
bg: 'bg-pink-50'
|
||||
},
|
||||
{
|
||||
icon: Twitter,
|
||||
name: 'Twitter',
|
||||
description: 'Open Twitter profile',
|
||||
href: '/tools/twitter-qr-code',
|
||||
color: 'text-sky-500',
|
||||
bg: 'bg-sky-50'
|
||||
},
|
||||
{
|
||||
icon: Youtube,
|
||||
name: 'YouTube',
|
||||
description: 'Open YouTube video',
|
||||
href: '/tools/youtube-qr-code',
|
||||
color: 'text-red-600',
|
||||
bg: 'bg-red-50'
|
||||
},
|
||||
{
|
||||
icon: MessageCircle,
|
||||
name: 'WhatsApp',
|
||||
description: 'Send WhatsApp message',
|
||||
href: '/tools/whatsapp-qr-code',
|
||||
color: 'text-green-600',
|
||||
bg: 'bg-green-50'
|
||||
},
|
||||
{
|
||||
icon: Music,
|
||||
name: 'TikTok',
|
||||
description: 'Open TikTok profile',
|
||||
href: '/tools/tiktok-qr-code',
|
||||
color: 'text-pink-600',
|
||||
bg: 'bg-pink-50'
|
||||
},
|
||||
{
|
||||
icon: MapPin,
|
||||
name: 'Location',
|
||||
description: 'Share GPS coordinates',
|
||||
href: '/tools/geolocation-qr-code',
|
||||
color: 'text-emerald-500',
|
||||
bg: 'bg-emerald-50'
|
||||
},
|
||||
{
|
||||
icon: Phone,
|
||||
name: 'Phone',
|
||||
description: 'Call phone number',
|
||||
href: '/tools/phone-qr-code',
|
||||
color: 'text-blue-400',
|
||||
bg: 'bg-blue-50'
|
||||
},
|
||||
{
|
||||
icon: CreditCard,
|
||||
name: 'PayPal',
|
||||
description: 'Receive PayPal payments',
|
||||
href: '/tools/paypal-qr-code',
|
||||
color: 'text-blue-700',
|
||||
bg: 'bg-blue-50'
|
||||
},
|
||||
{
|
||||
icon: Video,
|
||||
name: 'Zoom',
|
||||
description: 'Join Zoom meeting',
|
||||
href: '/tools/zoom-qr-code',
|
||||
color: 'text-sky-500',
|
||||
bg: 'bg-sky-50'
|
||||
},
|
||||
{
|
||||
icon: Users,
|
||||
name: 'Teams',
|
||||
description: 'Join Teams meeting',
|
||||
href: '/tools/teams-qr-code',
|
||||
color: 'text-violet-500',
|
||||
bg: 'bg-violet-50'
|
||||
}
|
||||
];
|
||||
|
||||
// Animation variants
|
||||
const containerVariants: Variants = {
|
||||
hidden: { opacity: 0 },
|
||||
visible: {
|
||||
opacity: 1,
|
||||
transition: {
|
||||
staggerChildren: 0.05
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const itemVariants: Variants = {
|
||||
hidden: { opacity: 0, y: 20 },
|
||||
visible: {
|
||||
opacity: 1,
|
||||
y: 0,
|
||||
transition: {
|
||||
duration: 0.4
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export function FreeToolsGrid() {
|
||||
return (
|
||||
<section id="tools" className="py-24 bg-slate-50/50 border-t border-slate-100">
|
||||
<div className="container mx-auto px-4 sm:px-6 lg:px-8 max-w-7xl">
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true, margin: "-100px" }}
|
||||
transition={{ duration: 0.5 }}
|
||||
className="text-center mb-16"
|
||||
>
|
||||
<h2 className="text-3xl lg:text-4xl font-bold text-slate-900 mb-4">
|
||||
More Free QR Code Tools
|
||||
</h2>
|
||||
<p className="text-lg text-slate-600 max-w-2xl mx-auto">
|
||||
Create specialized QR codes for every need. Completely free and no signup required.
|
||||
</p>
|
||||
</motion.div>
|
||||
|
||||
<motion.div
|
||||
variants={containerVariants}
|
||||
initial="hidden"
|
||||
whileInView="visible"
|
||||
viewport={{ once: true, margin: "-50px" }}
|
||||
className="grid grid-cols-2 md:grid-cols-4 lg:grid-cols-4 gap-4 md:gap-6"
|
||||
>
|
||||
{TOOLS.map((tool) => (
|
||||
<motion.div key={tool.name} variants={itemVariants}>
|
||||
<Link
|
||||
href={tool.href}
|
||||
className="group flex flex-col items-center p-5 md:p-6 rounded-2xl border border-slate-200/80 bg-white hover:border-primary-200 hover:shadow-xl hover:shadow-primary-500/10 transition-all duration-300"
|
||||
>
|
||||
<div className={`w-12 h-12 md:w-14 md:h-14 rounded-xl ${tool.bg} flex items-center justify-center mb-3 md:mb-4 group-hover:scale-110 transition-transform duration-300`}>
|
||||
<tool.icon className={`w-6 h-6 md:w-7 md:h-7 ${tool.color}`} />
|
||||
</div>
|
||||
<h3 className="text-base md:text-lg font-semibold text-slate-900 mb-0.5">
|
||||
{tool.name}
|
||||
</h3>
|
||||
<p className="text-xs md:text-sm text-slate-500 text-center">
|
||||
{tool.description}
|
||||
</p>
|
||||
</Link>
|
||||
</motion.div>
|
||||
))}
|
||||
</motion.div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
@@ -12,6 +12,7 @@ import { Pricing } from '@/components/marketing/Pricing';
|
||||
import { FAQ } from '@/components/marketing/FAQ';
|
||||
import { Button } from '@/components/ui/Button';
|
||||
import { ScrollToTop } from '@/components/ui/ScrollToTop';
|
||||
import { FreeToolsGrid } from '@/components/marketing/FreeToolsGrid';
|
||||
import en from '@/i18n/en.json';
|
||||
|
||||
export default function HomePageClient() {
|
||||
@@ -36,6 +37,9 @@ export default function HomePageClient() {
|
||||
|
||||
<AIComingSoonBanner />
|
||||
|
||||
{/* Free Tools Grid */}
|
||||
<FreeToolsGrid />
|
||||
|
||||
<StaticVsDynamic t={t} />
|
||||
<Features t={t} />
|
||||
|
||||
|
||||
81
src/components/seo/BreadcrumbSchema.tsx
Normal file
81
src/components/seo/BreadcrumbSchema.tsx
Normal file
@@ -0,0 +1,81 @@
|
||||
import React from 'react';
|
||||
import Link from 'next/link';
|
||||
import { ChevronRight, Home } from 'lucide-react';
|
||||
|
||||
interface BreadcrumbItem {
|
||||
name: string;
|
||||
url: string;
|
||||
}
|
||||
|
||||
interface BreadcrumbSchemaProps {
|
||||
items: BreadcrumbItem[];
|
||||
showUI?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates JSON-LD BreadcrumbList schema for SEO
|
||||
* Optionally renders visible breadcrumb navigation
|
||||
*/
|
||||
export function BreadcrumbSchema({ items, showUI = false }: BreadcrumbSchemaProps) {
|
||||
const breadcrumbSchema = {
|
||||
'@context': 'https://schema.org',
|
||||
'@type': 'BreadcrumbList',
|
||||
itemListElement: items.map((item, index) => ({
|
||||
'@type': 'ListItem',
|
||||
position: index + 1,
|
||||
name: item.name,
|
||||
item: item.url,
|
||||
})),
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<script
|
||||
type="application/ld+json"
|
||||
dangerouslySetInnerHTML={{ __html: JSON.stringify(breadcrumbSchema) }}
|
||||
/>
|
||||
{showUI && (
|
||||
<nav aria-label="Breadcrumb" className="bg-white/95 backdrop-blur-sm border-b border-slate-200">
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<ol className="flex items-center gap-1 py-3 text-sm">
|
||||
{items.map((item, index) => (
|
||||
<li key={item.url} className="flex items-center">
|
||||
{index > 0 && (
|
||||
<ChevronRight className="w-4 h-4 text-slate-400 mx-1" />
|
||||
)}
|
||||
{index === items.length - 1 ? (
|
||||
<span className="text-slate-600 font-medium truncate max-w-[200px]">
|
||||
{item.name}
|
||||
</span>
|
||||
) : (
|
||||
<Link
|
||||
href={item.url.replace('https://qrmaster.io', '')}
|
||||
className="text-slate-500 hover:text-indigo-600 transition-colors flex items-center gap-1"
|
||||
>
|
||||
{index === 0 && <Home className="w-4 h-4" />}
|
||||
<span className="hidden sm:inline">{item.name}</span>
|
||||
</Link>
|
||||
)}
|
||||
</li>
|
||||
))}
|
||||
</ol>
|
||||
</div>
|
||||
</nav>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Pre-configured breadcrumb for tool pages
|
||||
* Renders: Home > Free QR Code Tools > [Tool Name]
|
||||
*/
|
||||
export function ToolBreadcrumb({ toolName, toolSlug }: { toolName: string; toolSlug: string }) {
|
||||
const items: BreadcrumbItem[] = [
|
||||
{ name: 'Home', url: 'https://qrmaster.io/' },
|
||||
{ name: 'Free QR Code Tools', url: 'https://qrmaster.io/#tools' },
|
||||
{ name: toolName, url: `https://qrmaster.io/tools/${toolSlug}` },
|
||||
];
|
||||
|
||||
return <BreadcrumbSchema items={items} showUI={true} />;
|
||||
}
|
||||
24
src/components/ui/Textarea.tsx
Normal file
24
src/components/ui/Textarea.tsx
Normal file
@@ -0,0 +1,24 @@
|
||||
import * as React from "react"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
export interface TextareaProps
|
||||
extends React.TextareaHTMLAttributes<HTMLTextAreaElement> { }
|
||||
|
||||
const Textarea = React.forwardRef<HTMLTextAreaElement, TextareaProps>(
|
||||
({ className, ...props }, ref) => {
|
||||
return (
|
||||
<textarea
|
||||
className={cn(
|
||||
"flex min-h-[80px] w-full rounded-md border border-slate-200 bg-white px-3 py-2 text-sm ring-offset-white placeholder:text-slate-500 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-slate-950 focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50",
|
||||
className
|
||||
)}
|
||||
ref={ref}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
)
|
||||
Textarea.displayName = "Textarea"
|
||||
|
||||
export { Textarea }
|
||||
Reference in New Issue
Block a user