delete two unreferenced components from another codebase
MarkdownEditor and ui/start-confetti-button had no importers anywhere in the repo and both carried a different product's content: MarkdownEditor hardcoded https://app.pertento.ai/logo.png with alt="Pertento", and start-confetti-button spoke about 'the experiment' and 'Draft/Created' statuses. Translating either would have put feature words into ui.*, which D6 forbids — a shared namespace may not name a feature. todo/inputs-outside-forms named MarkdownEditor as deliberately-unwrapped, so that entry is updated rather than left pointing at a deleted file.
This commit is contained in:
@@ -1,115 +0,0 @@
|
||||
import { useState } from 'react';
|
||||
import md from 'markdown-it';
|
||||
import ReactMarkdown from 'react-markdown';
|
||||
import remarkGfm from 'remark-gfm';
|
||||
import rehypeRaw from 'rehype-raw';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Code, Eye } from 'lucide-react';
|
||||
|
||||
type MarkdownEditorProps = {
|
||||
content: string;
|
||||
onChange: (content: string) => void;
|
||||
placeholder?: string;
|
||||
greeting?: string;
|
||||
firstName?: string;
|
||||
lastName?: string;
|
||||
};
|
||||
|
||||
export function MarkdownEditor({
|
||||
content,
|
||||
onChange,
|
||||
placeholder = 'Enter markdown...',
|
||||
greeting,
|
||||
firstName,
|
||||
lastName,
|
||||
}: MarkdownEditorProps) {
|
||||
const [showPreview, setShowPreview] = useState(false);
|
||||
|
||||
const markdown = md({
|
||||
html: true,
|
||||
breaks: true,
|
||||
linkify: true,
|
||||
});
|
||||
|
||||
const processGreeting = (text: string) => {
|
||||
return text
|
||||
.replace(/\{\{firstName\}\}/g, firstName || '')
|
||||
.replace(/\{\{fullName\}\}/g, `${firstName || ''} ${lastName || ''}`.trim());
|
||||
};
|
||||
|
||||
const processedGreeting = greeting ? processGreeting(greeting) : '';
|
||||
const greetingHtml = processedGreeting ? `<p>${processedGreeting}</p>` : '';
|
||||
const preview = markdown.render(content);
|
||||
|
||||
return (
|
||||
<div className="flex flex-col rounded-lg border border-border bg-background overflow-hidden">
|
||||
{/* Toolbar */}
|
||||
<div className="flex items-center gap-2 border-b border-border bg-muted px-4 py-2">
|
||||
<Button size="sm" variant={!showPreview ? 'default' : 'outline'} onClick={() => setShowPreview(false)}>
|
||||
<Code className="h-4 w-4 mr-2" />
|
||||
Markdown
|
||||
</Button>
|
||||
<Button size="sm" variant={showPreview ? 'default' : 'outline'} onClick={() => setShowPreview(true)}>
|
||||
<Eye className="h-4 w-4 mr-2" />
|
||||
Preview
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* Content Area */}
|
||||
<div className="flex-1 overflow-hidden">
|
||||
{!showPreview ? (
|
||||
<textarea
|
||||
rows={15}
|
||||
name="content"
|
||||
value={content}
|
||||
onChange={(e: React.ChangeEvent<HTMLTextAreaElement>) => onChange(e.target.value)}
|
||||
placeholder={placeholder}
|
||||
className="w-full h-full resize-none p-4 font-mono text-sm focus:outline-none bg-background text-foreground placeholder:text-muted-foreground overflow-auto"
|
||||
/>
|
||||
) : (
|
||||
<div className="w-full h-full overflow-auto bg-slate-50 dark:bg-slate-900 p-4">
|
||||
{/* Email wrapper simulation */}
|
||||
<div className="mx-auto max-w-2xl rounded-lg bg-background p-8 shadow-lg">
|
||||
{/* Logo */}
|
||||
<div className="mb-8 text-center">
|
||||
<img src="https://app.pertento.ai/logo.png" alt="Pertento" className="h-16 mx-auto" />
|
||||
</div>
|
||||
|
||||
{/* Content */}
|
||||
<div className="space-y-4">
|
||||
{processedGreeting && <p className="text-base leading-relaxed">{processedGreeting}</p>}
|
||||
<style>{`
|
||||
.markdown-preview h1 { font-size: 2rem; font-weight: bold; margin: 1.5rem 0 1rem 0; }
|
||||
.markdown-preview h2 { font-size: 1.5rem; font-weight: bold; margin: 1.25rem 0 0.75rem 0; }
|
||||
.markdown-preview h3 { font-size: 1.25rem; font-weight: bold; margin: 1rem 0 0.5rem 0; }
|
||||
.markdown-preview p { margin: 0.75rem 0; line-height: 1.6; }
|
||||
/* list-style is restored explicitly — tailwind preflight strips it, which leaves an
|
||||
ordered list indented but unnumbered. */
|
||||
.markdown-preview ul, .markdown-preview ol { margin: 1rem 0; padding-left: 2rem; list-style-position: outside; }
|
||||
.markdown-preview ul { list-style-type: disc; }
|
||||
.markdown-preview ol { list-style-type: decimal; }
|
||||
.markdown-preview ul ul { list-style-type: circle; }
|
||||
.markdown-preview ol ol { list-style-type: lower-alpha; }
|
||||
.markdown-preview li { margin: 0.5rem 0; line-height: 1.6; }
|
||||
.markdown-preview code { background-color: rgb(229, 229, 229); color: rgb(31, 41, 55); padding: 0.25rem 0.5rem; border-radius: 0.25rem; font-size: 0.875rem; }
|
||||
.markdown-preview pre { background-color: rgb(17, 24, 39); color: rgb(243, 244, 246); padding: 1rem; border-radius: 0.5rem; overflow-x: auto; margin: 1rem 0; }
|
||||
.markdown-preview pre code { background-color: transparent; color: inherit; padding: 0; }
|
||||
.markdown-preview a { color: rgb(37, 99, 235); text-decoration: underline; }
|
||||
.markdown-preview blockquote { border-left: 4px solid rgb(209, 213, 219); padding-left: 1rem; font-style: italic; margin: 1rem 0; }
|
||||
.dark .markdown-preview code { background-color: rgb(55, 65, 81); color: rgb(243, 244, 246); }
|
||||
.dark .markdown-preview a { color: rgb(96, 165, 250); }
|
||||
.dark .markdown-preview blockquote { border-left-color: rgb(75, 85, 99); }
|
||||
`}</style>
|
||||
<div className="markdown-preview">
|
||||
<ReactMarkdown remarkPlugins={[remarkGfm]} rehypePlugins={[rehypeRaw]}>
|
||||
{content}
|
||||
</ReactMarkdown>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,212 +0,0 @@
|
||||
import React, { useEffect, useRef, useState, useCallback } from 'react';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Play } from 'lucide-react';
|
||||
|
||||
export default function StartConfettiButton({
|
||||
isDraftLike,
|
||||
onStart,
|
||||
className,
|
||||
label = 'Start',
|
||||
}: {
|
||||
isDraftLike: boolean;
|
||||
onStart?: () => void;
|
||||
className?: string;
|
||||
label?: string;
|
||||
}) {
|
||||
const [showConfetti, setShowConfetti] = useState(false);
|
||||
const canvasRef = useRef<HTMLCanvasElement | null>(null);
|
||||
const rafRef = useRef<number | null>(null);
|
||||
|
||||
// ---- Confetti Engine (inline, light) ----
|
||||
const runConfetti = useCallback(() => {
|
||||
const canvas = canvasRef.current!;
|
||||
const ctx = canvas.getContext('2d')!;
|
||||
const DPR = Math.max(1, Math.min(2, window.devicePixelRatio || 1));
|
||||
|
||||
// size canvas
|
||||
const resize = () => {
|
||||
const { innerWidth: w, innerHeight: h } = window;
|
||||
canvas.width = Math.floor(w * DPR);
|
||||
canvas.height = Math.floor(h * DPR);
|
||||
canvas.style.width = `${w}px`;
|
||||
canvas.style.height = `${h}px`;
|
||||
ctx.setTransform(DPR, 0, 0, DPR, 0, 0);
|
||||
};
|
||||
resize();
|
||||
|
||||
const onResize = () => resize();
|
||||
window.addEventListener('resize', onResize);
|
||||
|
||||
const colors = [
|
||||
'#ef4444', // red-500
|
||||
'#f59e0b', // amber-500
|
||||
'#10b981', // emerald-500
|
||||
'#3b82f6', // blue-500
|
||||
'#8b5cf6', // violet-500
|
||||
'#ec4899', // pink-500
|
||||
'#22c55e', // green-500
|
||||
'#eab308', // yellow-500
|
||||
];
|
||||
|
||||
type Particle = {
|
||||
x: number;
|
||||
y: number;
|
||||
w: number;
|
||||
h: number;
|
||||
vx: number;
|
||||
vy: number;
|
||||
rot: number;
|
||||
rotSpeed: number;
|
||||
tilt: number;
|
||||
tiltSpeed: number;
|
||||
color: string;
|
||||
alpha: number;
|
||||
decay: number;
|
||||
shape: 'rect' | 'triangle';
|
||||
};
|
||||
|
||||
const rand = (min: number, max: number) => Math.random() * (max - min) + min;
|
||||
|
||||
const particles: Particle[] = [];
|
||||
const bursts = 5;
|
||||
const perBurst = 80;
|
||||
const spread = Math.PI / 3;
|
||||
const gravity = 0.25;
|
||||
const air = 0.002;
|
||||
const floorFriction = 0.96;
|
||||
|
||||
for (let b = 0; b < bursts; b++) {
|
||||
const originX = (window.innerWidth / (bursts + 1)) * (b + 1);
|
||||
const originY = window.innerHeight * 0.15;
|
||||
const angleBase = -Math.PI / 2;
|
||||
for (let i = 0; i < perBurst; i++) {
|
||||
const angle = angleBase + rand(-spread, spread);
|
||||
const speed = rand(6, 12);
|
||||
particles.push({
|
||||
x: originX + rand(-8, 8),
|
||||
y: originY + rand(-8, 8),
|
||||
w: rand(4, 8),
|
||||
h: rand(6, 14),
|
||||
vx: Math.cos(angle) * speed,
|
||||
vy: Math.sin(angle) * speed,
|
||||
rot: rand(0, Math.PI * 2),
|
||||
rotSpeed: rand(-0.2, 0.2),
|
||||
tilt: rand(-0.8, 0.8),
|
||||
tiltSpeed: rand(-0.02, 0.02),
|
||||
color: colors[(Math.random() * colors.length) | 0] ?? '#ef4444',
|
||||
alpha: 1,
|
||||
decay: rand(0.004, 0.01),
|
||||
shape: Math.random() < 0.3 ? 'triangle' : 'rect',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const tick = () => {
|
||||
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
||||
|
||||
for (const p of particles) {
|
||||
// physics
|
||||
p.vy += gravity;
|
||||
p.vx *= 1 - air;
|
||||
p.vy *= 1 - air * 0.5;
|
||||
p.x += p.vx;
|
||||
p.y += p.vy;
|
||||
p.rot += p.rotSpeed;
|
||||
p.tilt += p.tiltSpeed;
|
||||
p.alpha -= p.decay;
|
||||
|
||||
// floor bounce & friction
|
||||
const floor = window.innerHeight - 6;
|
||||
if (p.y > floor) {
|
||||
p.y = floor;
|
||||
p.vy *= -0.45;
|
||||
p.vx *= floorFriction;
|
||||
p.rotSpeed *= floorFriction;
|
||||
p.tiltSpeed *= floorFriction;
|
||||
}
|
||||
|
||||
// draw
|
||||
if (p.alpha > 0) {
|
||||
ctx.globalAlpha = Math.max(0, p.alpha);
|
||||
ctx.save();
|
||||
ctx.translate(p.x, p.y);
|
||||
ctx.rotate(p.rot + p.tilt);
|
||||
ctx.fillStyle = p.color;
|
||||
|
||||
if (p.shape === 'rect') {
|
||||
ctx.fillRect(-p.w / 2, -p.h / 2, p.w, p.h);
|
||||
} else {
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(0, -p.h / 2);
|
||||
ctx.lineTo(p.w / 2, p.h / 2);
|
||||
ctx.lineTo(-p.w / 2, p.h / 2);
|
||||
ctx.closePath();
|
||||
ctx.fill();
|
||||
}
|
||||
ctx.restore();
|
||||
}
|
||||
}
|
||||
|
||||
const alive = particles.some((p) => p.alpha > 0.02);
|
||||
if (alive) {
|
||||
rafRef.current = requestAnimationFrame(tick);
|
||||
} else {
|
||||
// cleanup
|
||||
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
||||
window.removeEventListener('resize', onResize);
|
||||
}
|
||||
};
|
||||
|
||||
tick();
|
||||
}, []);
|
||||
|
||||
const handleStart = () => {
|
||||
onStart?.();
|
||||
|
||||
if (isDraftLike) {
|
||||
setShowConfetti(true);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (!showConfetti) return;
|
||||
|
||||
runConfetti();
|
||||
|
||||
const t = setTimeout(() => {
|
||||
setShowConfetti(false);
|
||||
}, 2200);
|
||||
|
||||
return () => {
|
||||
clearTimeout(t);
|
||||
if (rafRef.current) cancelAnimationFrame(rafRef.current);
|
||||
rafRef.current = null;
|
||||
};
|
||||
}, [showConfetti, runConfetti]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Button
|
||||
size="sm"
|
||||
onClick={handleStart}
|
||||
className={`min-w-24 ${isDraftLike ? 'bg-green-600 text-white hover:bg-green-700' : ''} ${className ?? ''}`}
|
||||
disabled={!isDraftLike}
|
||||
title={isDraftLike ? 'Start the experiment' : 'Only available for Draft/Created'}
|
||||
>
|
||||
<Play className="w-4 h-4 mr-2" />
|
||||
{label}
|
||||
</Button>
|
||||
|
||||
{/* Confetti Overlay */}
|
||||
{showConfetti && (
|
||||
<div
|
||||
className="fixed inset-0 z-100 pointer-events-none mask-[radial-gradient(80%_80%_at_50%_40%,#000,transparent)]"
|
||||
aria-hidden
|
||||
>
|
||||
<canvas ref={canvasRef} className="absolute inset-0 w-full h-full" />
|
||||
<div className="absolute inset-x-0 top-0 h-32 bg-linear-to-b from-white/60 to-transparent" />
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user