Some checks failed
Build & Deploy / 🔍 Prepare (push) Successful in 5s
Build & Deploy / 🏗️ Build (push) Failing after 14s
Build & Deploy / 🧪 QA (push) Failing after 1m48s
Build & Deploy / 🚀 Deploy (push) Has been skipped
Build & Deploy / 🩺 Health Check (push) Has been skipped
Build & Deploy / 🔔 Notify (push) Successful in 2s
249 lines
8.2 KiB
TypeScript
249 lines
8.2 KiB
TypeScript
import React from 'react';
|
|
import Prism from 'prismjs';
|
|
import { ComponentShareButton } from './ComponentShareButton';
|
|
import { Reveal } from './Reveal';
|
|
import 'prismjs/components/prism-python';
|
|
import 'prismjs/components/prism-typescript';
|
|
import 'prismjs/components/prism-javascript';
|
|
import 'prismjs/components/prism-jsx';
|
|
import 'prismjs/components/prism-tsx';
|
|
import 'prismjs/components/prism-docker';
|
|
import 'prismjs/components/prism-yaml';
|
|
import 'prismjs/components/prism-json';
|
|
import 'prismjs/components/prism-markup';
|
|
import 'prismjs/components/prism-css';
|
|
import 'prismjs/components/prism-sql';
|
|
import 'prismjs/components/prism-bash';
|
|
import 'prismjs/components/prism-markdown';
|
|
|
|
interface BlockquoteProps {
|
|
children: React.ReactNode;
|
|
className?: string;
|
|
}
|
|
|
|
export const ArticleBlockquote: React.FC<BlockquoteProps> = ({ children, className = '' }) => {
|
|
// Generate a quick stable hash based on content length/chars for ID
|
|
const shareId = `blockquote-${Math.random().toString(36).substring(7).toUpperCase()}`;
|
|
|
|
return (
|
|
<Reveal direction="up" delay={0.1}>
|
|
<figure
|
|
id={shareId}
|
|
className={`not-prose my-16 group relative transition-all duration-500 ease-out z-10 ${className}`}
|
|
>
|
|
<div className="absolute -inset-1 bg-gradient-to-r from-emerald-100/50 to-emerald-50/50 rounded-[2rem] blur opacity-20 group-hover:opacity-40 transition duration-1000 -z-10" />
|
|
|
|
<div className="glass bg-white/80 backdrop-blur-xl border border-slate-100 rounded-2xl shadow-sm group-hover:shadow-md group-hover:border-slate-200 transition-all duration-500 overflow-hidden relative p-8 md:p-12">
|
|
|
|
{/* Subtle top shine */}
|
|
<div className="absolute inset-x-0 top-0 h-px bg-gradient-to-r from-transparent via-white to-transparent opacity-80" />
|
|
|
|
{/* Share Button top right */}
|
|
<div className="absolute top-4 right-4 md:opacity-0 group-hover:opacity-100 transition-opacity duration-500">
|
|
<ComponentShareButton targetId={shareId} title="Zitat" />
|
|
</div>
|
|
|
|
<div className="flex flex-col md:flex-row gap-6 md:gap-8 items-start relative z-10">
|
|
<div className="shrink-0">
|
|
<div className="w-12 h-12 md:w-16 md:h-16 bg-slate-50 border border-slate-100 rounded-xl flex items-center justify-center group-hover:scale-110 group-hover:bg-white group-hover:border-emerald-200 transition-all duration-500 shadow-sm relative overflow-hidden">
|
|
{/* Small emerald glow effect inside the icon box on hover */}
|
|
<div className="absolute inset-0 bg-emerald-500/0 group-hover:bg-emerald-500/5 transition-colors duration-500" />
|
|
<svg className="w-6 h-6 md:w-8 md:h-8 text-slate-300 group-hover:text-emerald-500 transition-colors duration-500 relative z-10" viewBox="0 0 24 24" fill="currentColor">
|
|
<path d="M14.017 21v-7.391c0-5.704 3.731-9.57 8.983-10.609l.995 2.151c-2.432.917-3.995 3.638-3.995 5.849h4v10h-9.983zm-14.017 0v-7.391c0-5.704 3.748-9.57 9-10.609l.996 2.151c-2.433.917-3.996 3.638-3.996 5.849h3.983v10h-9.983z" />
|
|
</svg>
|
|
</div>
|
|
</div>
|
|
|
|
<blockquote className="flex-1 relative">
|
|
<div className="text-xl md:text-2xl lg:text-3xl font-serif text-slate-800 italic leading-relaxed md:leading-[1.4] tracking-tight m-0">
|
|
{children}
|
|
</div>
|
|
</blockquote>
|
|
</div>
|
|
</div>
|
|
</figure>
|
|
</Reveal>
|
|
);
|
|
};
|
|
|
|
interface CodeBlockProps {
|
|
code?: string;
|
|
children?: React.ReactNode;
|
|
language?: string;
|
|
showLineNumbers?: boolean;
|
|
className?: string;
|
|
}
|
|
|
|
|
|
// Language mapping for Prism.js
|
|
const prismLanguageMap: Record<string, string> = {
|
|
py: 'python',
|
|
ts: 'typescript',
|
|
tsx: 'tsx',
|
|
js: 'javascript',
|
|
jsx: 'jsx',
|
|
dockerfile: 'docker',
|
|
docker: 'docker',
|
|
yml: 'yaml',
|
|
yaml: 'yaml',
|
|
json: 'json',
|
|
html: 'markup',
|
|
css: 'css',
|
|
sql: 'sql',
|
|
sh: 'bash',
|
|
bash: 'bash',
|
|
md: 'markdown',
|
|
astro: 'markup', // Fallback for Astro
|
|
};
|
|
|
|
// Highlight code using Prism.js
|
|
const highlightCode = (code: string, language: string): { html: string; prismLanguage: string } => {
|
|
const prismLanguage = prismLanguageMap[language] || language || 'markup';
|
|
|
|
try {
|
|
const highlighted = Prism.highlight(
|
|
code.trim(),
|
|
Prism.languages[prismLanguage] || Prism.languages.markup,
|
|
prismLanguage,
|
|
);
|
|
return { html: highlighted, prismLanguage };
|
|
} catch (error) {
|
|
console.warn('Prism highlighting failed:', error);
|
|
return { html: code.trim(), prismLanguage: 'text' };
|
|
}
|
|
};
|
|
|
|
export const CodeBlock: React.FC<CodeBlockProps> = ({
|
|
code,
|
|
children,
|
|
language = 'text',
|
|
showLineNumbers = false,
|
|
className = ''
|
|
}) => {
|
|
const codeContent = code || (typeof children === 'string' ? children : String(children || '')).trim();
|
|
const { html: highlightedCode, prismLanguage } = language !== 'text' ? highlightCode(codeContent, language) : { html: codeContent, prismLanguage: 'text' };
|
|
const lines = codeContent.split('\n');
|
|
|
|
return (
|
|
<>
|
|
<style dangerouslySetInnerHTML={{ __html: syntaxHighlightingStyles }} />
|
|
<div className="relative my-6">
|
|
{language !== 'text' && (
|
|
<div className="absolute top-3 right-3 text-[10px] font-bold uppercase tracking-widest bg-white text-slate-500 px-2 py-1 rounded-md z-10 border border-slate-100 shadow-sm">
|
|
{language}
|
|
</div>
|
|
)}
|
|
<pre
|
|
className={`not-prose m-0 p-6 overflow-x-auto overflow-y-auto text-[13px] leading-[1.65] font-mono text-slate-800 hide-scrollbar border border-slate-200 bg-white rounded-2xl ${className} ${showLineNumbers ? 'pl-12' : ''}`}
|
|
style={{ fontFamily: "ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New', monospace", maxHeight: "22rem" }}
|
|
>
|
|
{showLineNumbers ? (
|
|
<div className="relative">
|
|
<div className="absolute left-0 top-0 bottom-0 w-8 text-right text-slate-600 select-none pr-3 border-r border-slate-700">
|
|
{lines.map((_, i) => (
|
|
<div key={i} className="leading-relaxed">{i + 1}</div>
|
|
))}
|
|
</div>
|
|
<div className="pl-10">
|
|
<code className={`language-${prismLanguage}`} dangerouslySetInnerHTML={{ __html: highlightedCode }} />
|
|
</div>
|
|
</div>
|
|
) : (
|
|
<code className={`language-${prismLanguage}`} dangerouslySetInnerHTML={{ __html: highlightedCode }} />
|
|
)}
|
|
</pre>
|
|
</div>
|
|
</>
|
|
);
|
|
};
|
|
|
|
export const InlineCode: React.FC<{ children: React.ReactNode; className?: string }> = ({ children, className = '' }) => (
|
|
<code className={`not-prose bg-white text-slate-800 px-1.5 py-0.5 rounded-md font-mono text-[0.9em] border border-slate-200 ${className}`}>
|
|
{children}
|
|
</code>
|
|
);
|
|
|
|
// Prism.js syntax highlighting styles (matching FileExample.astro)
|
|
const syntaxHighlightingStyles = `
|
|
code[class*='language-'],
|
|
pre[class*='language-'],
|
|
pre:has(code[class*='language-']) {
|
|
color: #0f172a;
|
|
background: transparent;
|
|
text-shadow: none;
|
|
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New', monospace;
|
|
font-size: 0.8125rem;
|
|
line-height: 1.65;
|
|
direction: ltr;
|
|
text-align: left;
|
|
white-space: pre;
|
|
word-spacing: normal;
|
|
word-break: normal;
|
|
tab-size: 2;
|
|
hyphens: none;
|
|
}
|
|
|
|
.token.comment,
|
|
.token.prolog,
|
|
.token.doctype,
|
|
.token.cdata {
|
|
color: #64748b;
|
|
font-style: italic;
|
|
}
|
|
|
|
.token.punctuation {
|
|
color: #94a3b8;
|
|
}
|
|
|
|
.token.operator {
|
|
color: #64748b;
|
|
}
|
|
|
|
.token.property,
|
|
.token.tag,
|
|
.token.constant,
|
|
.token.symbol,
|
|
.token.deleted {
|
|
color: #c2410c;
|
|
}
|
|
|
|
.token.boolean,
|
|
.token.number {
|
|
color: #a16207;
|
|
}
|
|
|
|
.token.selector,
|
|
.token.attr-name,
|
|
.token.string,
|
|
.token.char,
|
|
.token.builtin,
|
|
.token.inserted {
|
|
color: #059669;
|
|
}
|
|
|
|
.token.operator,
|
|
.token.entity,
|
|
.token.url,
|
|
.language-css .token.string,
|
|
.style .token.string {
|
|
color: #475569;
|
|
}
|
|
|
|
.token.atrule,
|
|
.token.attr-value,
|
|
.token.keyword {
|
|
color: #7c3aed;
|
|
font-weight: 500;
|
|
}
|
|
|
|
.token.function,
|
|
.token.class-name {
|
|
color: #2563eb;
|
|
}
|
|
|
|
.token.regex,
|
|
.token.important,
|
|
.token.variable {
|
|
color: #db2777;
|
|
}
|
|
`; |