14 blog post schedule
This commit is contained in:
23
src/components/aeo/AnswerBox.tsx
Normal file
23
src/components/aeo/AnswerBox.tsx
Normal file
@@ -0,0 +1,23 @@
|
||||
"use client";
|
||||
|
||||
import React from 'react';
|
||||
import sanitizeHtml from 'sanitize-html';
|
||||
|
||||
type Props = { html: string };
|
||||
|
||||
export function AnswerBox({ html }: Props) {
|
||||
const cleanHtml = sanitizeHtml(html, {
|
||||
allowedTags: ['p', 'strong', 'em', 'ul', 'ol', 'li', 'a', 'br', 'code', 'pre', 'blockquote', 'h3', 'h4'],
|
||||
allowedAttributes: { 'a': ['href', 'class'], 'code': ['class'], 'pre': ['class'] },
|
||||
allowedClasses: {
|
||||
'a': ['text-primary-600', 'hover:underline'],
|
||||
}
|
||||
});
|
||||
|
||||
return (
|
||||
<section className="rounded-xl border border-blue-100 bg-blue-50/50 p-6 my-8">
|
||||
<div className="text-sm font-semibold text-blue-800 uppercase tracking-wider mb-2">Quick Answer</div>
|
||||
<div className="prose prose-blue max-w-none text-gray-800" dangerouslySetInnerHTML={{ __html: cleanHtml }} />
|
||||
</section>
|
||||
);
|
||||
}
|
||||
35
src/components/aeo/FAQSection.tsx
Normal file
35
src/components/aeo/FAQSection.tsx
Normal file
@@ -0,0 +1,35 @@
|
||||
"use client";
|
||||
|
||||
import React from 'react';
|
||||
import sanitizeHtml from 'sanitize-html';
|
||||
import type { FAQItem } from "@/lib/types";
|
||||
|
||||
type Props = { items: FAQItem[]; title?: string };
|
||||
|
||||
export function FAQSection({ items, title = "Frequently Asked Questions" }: Props) {
|
||||
if (!items?.length) return null;
|
||||
|
||||
return (
|
||||
<section className="rounded-xl border border-gray-100 bg-gray-50/50 p-6 my-8">
|
||||
<h2 className="text-xl font-bold text-gray-900 mb-6">{title}</h2>
|
||||
<div className="space-y-4">
|
||||
{items.map((f) => {
|
||||
const cleanAnswer = sanitizeHtml(f.answer, {
|
||||
allowedTags: ['p', 'strong', 'em', 'ul', 'ol', 'li', 'a', 'br', 'code'],
|
||||
allowedAttributes: { 'a': ['href'] }
|
||||
});
|
||||
|
||||
return (
|
||||
<details key={f.question} className="group rounded-lg border border-gray-200 bg-white p-4 open:shadow-sm open:border-blue-200 transition-all">
|
||||
<summary className="cursor-pointer font-semibold text-gray-800 flex justify-between items-center group-open:text-blue-700">
|
||||
{f.question}
|
||||
<span className="text-gray-400 group-open:rotate-180 transition-transform">▼</span>
|
||||
</summary>
|
||||
<div className="prose max-w-none mt-3 text-gray-600 border-t border-gray-100 pt-3" dangerouslySetInnerHTML={{ __html: cleanAnswer }} />
|
||||
</details>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
18
src/components/aeo/StepList.tsx
Normal file
18
src/components/aeo/StepList.tsx
Normal file
@@ -0,0 +1,18 @@
|
||||
import React from 'react';
|
||||
|
||||
type Props = { steps: string[]; title?: string };
|
||||
|
||||
export function StepList({ steps, title = "How to do it" }: Props) {
|
||||
if (!steps?.length) return null;
|
||||
|
||||
return (
|
||||
<section className="rounded-xl border border-gray-200 bg-white p-6 my-8 shadow-sm">
|
||||
<h2 className="text-xl font-bold text-gray-900 mb-4">{title}</h2>
|
||||
<ol className="list-decimal pl-6 space-y-3">
|
||||
{steps.map((s, i) => (
|
||||
<li key={`step-${i}`} className="text-gray-700 font-medium pl-2">{s}</li>
|
||||
))}
|
||||
</ol>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
52
src/components/author/AuthorCard.tsx
Normal file
52
src/components/author/AuthorCard.tsx
Normal file
@@ -0,0 +1,52 @@
|
||||
import React from 'react';
|
||||
import Link from "next/link";
|
||||
import Image from "next/image";
|
||||
import type { AuthorProfile } from "@/lib/types";
|
||||
|
||||
export function AuthorCard({ author }: { author: AuthorProfile }) {
|
||||
return (
|
||||
<aside className="rounded-xl border border-gray-200 bg-white p-6 my-8 flex gap-6 items-start shadow-sm">
|
||||
{author.image ? (
|
||||
<div className="relative w-16 h-16 flex-shrink-0">
|
||||
<Image
|
||||
src={author.image}
|
||||
alt={author.name}
|
||||
fill
|
||||
className="rounded-full object-cover border-2 border-white shadow-md"
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<div className="w-16 h-16 rounded-full bg-blue-100 flex items-center justify-center text-blue-600 font-bold text-xl flex-shrink-0">
|
||||
{author.name.charAt(0)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="min-w-0">
|
||||
<div className="font-bold text-lg text-gray-900">
|
||||
<Link href={`/authors/${author.slug}`} className="hover:text-blue-600 transition-colors">
|
||||
{author.name}
|
||||
</Link>
|
||||
</div>
|
||||
<div className="text-sm font-medium text-blue-600 mb-2">{author.role}</div>
|
||||
<p className="text-sm text-gray-600 leading-relaxed mb-3">{author.bio}</p>
|
||||
|
||||
{!!author.sameAs?.length && (
|
||||
<div className="flex flex-wrap gap-3">
|
||||
{author.sameAs.map((url) => {
|
||||
let label = "Website";
|
||||
if (url.includes('linkedin')) label = "LinkedIn";
|
||||
if (url.includes('github')) label = "GitHub";
|
||||
if (url.includes('twitter') || url.includes('x.com')) label = "Twitter";
|
||||
|
||||
return (
|
||||
<a key={url} href={url} target="_blank" rel="noreferrer" className="text-xs font-semibold text-gray-500 hover:text-gray-900 uppercase tracking-wide">
|
||||
{label}
|
||||
</a>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</aside>
|
||||
);
|
||||
}
|
||||
25
src/components/blog/RelatedPosts.tsx
Normal file
25
src/components/blog/RelatedPosts.tsx
Normal file
@@ -0,0 +1,25 @@
|
||||
import React from 'react';
|
||||
import Link from "next/link";
|
||||
import type { BlogPost } from "@/lib/types";
|
||||
|
||||
export function RelatedPosts({ posts }: { posts: BlogPost[] }) {
|
||||
if (!posts?.length) return null;
|
||||
|
||||
return (
|
||||
<section className="rounded-xl border border-gray-100 bg-gray-50 p-6 my-8">
|
||||
<h2 className="text-lg font-bold text-gray-900 mb-4">Related Guides</h2>
|
||||
<ul className="grid gap-4 sm:grid-cols-2">
|
||||
{posts.map(p => (
|
||||
<li key={p.slug} className="group">
|
||||
<Link href={`/blog/${p.slug}`} className="block h-full p-4 rounded-lg bg-white border border-gray-200 shadow-sm hover:shadow-md hover:border-blue-300 transition-all">
|
||||
<h3 className="font-semibold text-gray-900 group-hover:text-blue-600 mb-2 line-clamp-2">
|
||||
{p.title}
|
||||
</h3>
|
||||
<p className="text-sm text-gray-500 line-clamp-2">{p.description}</p>
|
||||
</Link>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
import Link from 'next/link';
|
||||
import en from '@/i18n/en.json';
|
||||
import { Instagram, Twitter, Linkedin, Facebook } from 'lucide-react';
|
||||
|
||||
interface FooterProps {
|
||||
variant?: 'marketing' | 'dashboard';
|
||||
@@ -23,6 +24,19 @@ export function Footer({ variant = 'marketing', t }: FooterProps) {
|
||||
<p className={isDashboard ? 'text-gray-500' : 'text-gray-400'}>
|
||||
{translations.tagline}
|
||||
</p>
|
||||
{!isDashboard && (
|
||||
<div className="flex space-x-4 mt-6">
|
||||
<a href="https://www.linkedin.com/in/qr-master-44b6863a2/" target="_blank" rel="noopener noreferrer" className="text-gray-400 hover:text-white transition-colors">
|
||||
<Linkedin className="w-5 h-5" />
|
||||
</a>
|
||||
<a href="https://x.com/TIMO_QRMASTER" target="_blank" rel="noopener noreferrer" className="text-gray-400 hover:text-white transition-colors">
|
||||
<Twitter className="w-5 h-5" />
|
||||
</a>
|
||||
<a href="https://www.instagram.com/qrmaster_net/" target="_blank" rel="noopener noreferrer" className="text-gray-400 hover:text-white transition-colors">
|
||||
<Instagram className="w-5 h-5" />
|
||||
</a>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
@@ -30,6 +44,7 @@ export function Footer({ variant = 'marketing', t }: FooterProps) {
|
||||
<ul className={`space-y-2 ${isDashboard ? 'text-gray-500' : 'text-gray-400'}`}>
|
||||
<li><Link href="/features" className={isDashboard ? 'hover:text-primary-600' : 'hover:text-white'}>{translations.features}</Link></li>
|
||||
<li><Link href="/about" className={isDashboard ? 'hover:text-primary-600' : 'hover:text-white'}>About</Link></li>
|
||||
<li><Link href="/authors/timo" className={isDashboard ? 'hover:text-primary-600' : 'hover:text-white'}>Timo Knuth (Author)</Link></li>
|
||||
<li><Link href="/#pricing" className={isDashboard ? 'hover:text-primary-600' : 'hover:text-white'}>{translations.pricing}</Link></li>
|
||||
<li><Link href="/qr-code-tracking" className={isDashboard ? 'hover:text-primary-600' : 'hover:text-white'}>QR Analytics</Link></li>
|
||||
<li><Link href="/faq" className={isDashboard ? 'hover:text-primary-600' : 'hover:text-white'}>{translations.faq}</Link></li>
|
||||
@@ -40,6 +55,7 @@ export function Footer({ variant = 'marketing', t }: FooterProps) {
|
||||
<div>
|
||||
<h3 className={`font-semibold mb-4 ${isDashboard ? 'text-gray-900' : ''}`}>{translations.resources}</h3>
|
||||
<ul className={`space-y-2 ${isDashboard ? 'text-gray-500' : 'text-gray-400'}`}>
|
||||
<li><Link href="/learn" className={isDashboard ? 'hover:text-primary-600' : 'hover:text-white'}>{translations.learn}</Link></li>
|
||||
<li><Link href="/#pricing" className={isDashboard ? 'hover:text-primary-600' : 'hover:text-white'}>{translations.full_pricing}</Link></li>
|
||||
<li><Link href="/faq" className={isDashboard ? 'hover:text-primary-600' : 'hover:text-white'}>{translations.all_questions}</Link></li>
|
||||
<li><Link href="/blog" className={isDashboard ? 'hover:text-primary-600' : 'hover:text-white'}>{translations.all_articles}</Link></li>
|
||||
|
||||
Reference in New Issue
Block a user