feat(dashboard): reusable Meet Kevin components (ActionChip, ConvictionBar, YouTubeEmbed)
This commit is contained in:
parent
83b18b43cf
commit
a4d75e37c4
4 changed files with 69 additions and 0 deletions
17
dashboard/src/components/meetKevin/ActionChip.tsx
Normal file
17
dashboard/src/components/meetKevin/ActionChip.tsx
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
import type { TickerAction } from '../../types/meetKevin';
|
||||
|
||||
const COLORS: Record<TickerAction, string> = {
|
||||
buy: 'bg-green-600/30 text-green-300 border-green-500/40',
|
||||
sell: 'bg-red-600/30 text-red-300 border-red-500/40',
|
||||
hold: 'bg-slate-600/30 text-slate-300 border-slate-500/40',
|
||||
watch: 'bg-yellow-600/30 text-yellow-200 border-yellow-500/40',
|
||||
avoid: 'bg-rose-600/40 text-rose-200 border-rose-500/40',
|
||||
};
|
||||
|
||||
export function ActionChip({ action }: { action: TickerAction }) {
|
||||
return (
|
||||
<span className={`px-2 py-0.5 text-xs font-semibold uppercase rounded-md border ${COLORS[action]}`}>
|
||||
{action}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
8
dashboard/src/components/meetKevin/ConvictionBar.tsx
Normal file
8
dashboard/src/components/meetKevin/ConvictionBar.tsx
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
export function ConvictionBar({ value }: { value: number }) {
|
||||
const pct = Math.max(0, Math.min(1, value)) * 100;
|
||||
return (
|
||||
<div className="w-full h-1.5 bg-slate-700 rounded-full overflow-hidden">
|
||||
<div className="h-full bg-blue-400" style={{ width: `${pct}%` }} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
40
dashboard/src/components/meetKevin/YouTubeEmbed.tsx
Normal file
40
dashboard/src/components/meetKevin/YouTubeEmbed.tsx
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
import { useRef, useImperativeHandle, forwardRef } from 'react';
|
||||
|
||||
export interface YouTubeEmbedHandle {
|
||||
seekTo: (seconds: number) => void;
|
||||
}
|
||||
|
||||
interface Props {
|
||||
videoId: string;
|
||||
startSeconds?: number;
|
||||
}
|
||||
|
||||
export const YouTubeEmbed = forwardRef<YouTubeEmbedHandle, Props>(
|
||||
({ videoId, startSeconds = 0 }, ref) => {
|
||||
const iframeRef = useRef<HTMLIFrameElement>(null);
|
||||
|
||||
useImperativeHandle(ref, () => ({
|
||||
seekTo: (seconds: number) => {
|
||||
const win = iframeRef.current?.contentWindow;
|
||||
win?.postMessage(
|
||||
JSON.stringify({ event: 'command', func: 'seekTo', args: [seconds, true] }),
|
||||
'*',
|
||||
);
|
||||
},
|
||||
}));
|
||||
|
||||
return (
|
||||
<div className="relative pb-[56.25%] h-0 bg-black rounded-xl overflow-hidden">
|
||||
<iframe
|
||||
ref={iframeRef}
|
||||
className="absolute inset-0 w-full h-full"
|
||||
src={`https://www.youtube-nocookie.com/embed/${videoId}?start=${startSeconds}&enablejsapi=1`}
|
||||
title={`YouTube video ${videoId}`}
|
||||
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
|
||||
allowFullScreen
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
);
|
||||
YouTubeEmbed.displayName = 'YouTubeEmbed';
|
||||
4
dashboard/src/components/meetKevin/index.ts
Normal file
4
dashboard/src/components/meetKevin/index.ts
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
export { ActionChip } from './ActionChip';
|
||||
export { ConvictionBar } from './ConvictionBar';
|
||||
export { YouTubeEmbed } from './YouTubeEmbed';
|
||||
export type { YouTubeEmbedHandle } from './YouTubeEmbed';
|
||||
Loading…
Add table
Add a link
Reference in a new issue