refactor: reorganize repo docs and tooling layout
Consolidate the repository into clearer apps, tools, and layered docs areas so contributors can navigate and maintain it more reliably. Align validation, metadata sync, and CI around the same canonical workflow to reduce drift across local checks and GitHub Actions.
This commit is contained in:
66
apps/web-app/src/components/SkillCard.tsx
Normal file
66
apps/web-app/src/components/SkillCard.tsx
Normal file
@@ -0,0 +1,66 @@
|
||||
import React from 'react';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { Book, ArrowRight } from 'lucide-react';
|
||||
import { motion } from 'framer-motion';
|
||||
import { SkillStarButton } from './SkillStarButton';
|
||||
import type { Skill } from '../types';
|
||||
|
||||
interface SkillCardProps {
|
||||
skill: Skill;
|
||||
starCount: number;
|
||||
}
|
||||
|
||||
export const SkillCard = React.memo(({ skill, starCount }: SkillCardProps) => {
|
||||
return (
|
||||
<motion.div
|
||||
layout
|
||||
initial={{ opacity: 0, scale: 0.95 }}
|
||||
animate={{ opacity: 1, scale: 1 }}
|
||||
exit={{ opacity: 0, scale: 0.95 }}
|
||||
transition={{ duration: 0.2 }}
|
||||
className="h-full"
|
||||
>
|
||||
<Link
|
||||
to={`/skill/${skill.id}`}
|
||||
className="group flex flex-col h-full rounded-lg border border-slate-200 bg-white p-6 shadow-sm transition-all hover:bg-slate-50 hover:shadow-md dark:border-slate-800 dark:bg-slate-900 dark:hover:border-indigo-500/50"
|
||||
>
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<div className="flex items-center space-x-2">
|
||||
<div className="p-2 bg-indigo-50 dark:bg-indigo-950/30 rounded-md">
|
||||
<Book className="h-5 w-5 text-indigo-600 dark:text-indigo-400" />
|
||||
</div>
|
||||
<span className="text-xs font-medium px-2 py-1 rounded-full bg-slate-100 text-slate-600 dark:bg-slate-800 dark:text-slate-400">
|
||||
{skill.category || 'Uncategorized'}
|
||||
</span>
|
||||
</div>
|
||||
<SkillStarButton
|
||||
skillId={skill.id}
|
||||
initialCount={starCount}
|
||||
variant="default"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<h3 className="text-lg font-bold text-slate-900 dark:text-slate-50 group-hover:text-indigo-600 dark:group-hover:text-indigo-400 transition-colors mb-2 line-clamp-1">
|
||||
@{skill.name}
|
||||
</h3>
|
||||
|
||||
<p className="text-sm text-slate-500 dark:text-slate-400 line-clamp-3 mb-4 flex-grow">
|
||||
{skill.description}
|
||||
</p>
|
||||
|
||||
<div className="flex items-center justify-between text-xs text-slate-400 dark:text-slate-500 mb-3 pb-3 border-b border-slate-100 dark:border-slate-800">
|
||||
<span>Risk: <span className="font-semibold text-slate-600 dark:text-slate-300">{skill.risk || 'unknown'}</span></span>
|
||||
{skill.date_added && (
|
||||
<span className="ml-2">📅 {skill.date_added}</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="flex items-center text-sm font-medium text-indigo-600 dark:text-indigo-400 pt-2 mt-auto group-hover:translate-x-1 transition-transform">
|
||||
Read Skill <ArrowRight className="ml-1 h-4 w-4" />
|
||||
</div>
|
||||
</Link>
|
||||
</motion.div>
|
||||
);
|
||||
});
|
||||
|
||||
SkillCard.displayName = 'SkillCard';
|
||||
64
apps/web-app/src/components/SkillStarButton.tsx
Normal file
64
apps/web-app/src/components/SkillStarButton.tsx
Normal file
@@ -0,0 +1,64 @@
|
||||
import React from 'react';
|
||||
import { Star } from 'lucide-react';
|
||||
import { useSkillStars } from '../hooks/useSkillStars';
|
||||
|
||||
interface SkillStarButtonProps {
|
||||
skillId: string;
|
||||
initialCount?: number;
|
||||
onStarClick?: () => void;
|
||||
variant?: 'default' | 'compact';
|
||||
}
|
||||
|
||||
/**
|
||||
* Star button component for skills
|
||||
* Uses useSkillStars hook for state management
|
||||
*/
|
||||
export function SkillStarButton({
|
||||
skillId,
|
||||
initialCount = 0,
|
||||
onStarClick,
|
||||
variant = 'default'
|
||||
}: SkillStarButtonProps): React.ReactElement {
|
||||
const { starCount, hasStarred, handleStarClick, isLoading } = useSkillStars(skillId);
|
||||
|
||||
// Use optimistic count from hook, fall back to initial
|
||||
const displayCount = starCount || initialCount;
|
||||
|
||||
const handleClick = async (e: React.MouseEvent) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
if (hasStarred || isLoading) return;
|
||||
|
||||
await handleStarClick();
|
||||
onStarClick?.();
|
||||
};
|
||||
|
||||
if (variant === 'compact') {
|
||||
return (
|
||||
<button
|
||||
onClick={handleClick}
|
||||
className="flex items-center space-x-1.5 px-3 py-1 bg-yellow-50 dark:bg-yellow-900/10 hover:bg-yellow-100 dark:hover:bg-yellow-900/30 text-yellow-700 dark:text-yellow-500 rounded-full text-xs font-bold border border-yellow-200 dark:border-yellow-700/50 transition-colors disabled:opacity-50"
|
||||
disabled={hasStarred || isLoading}
|
||||
title={hasStarred ? 'You already upvoted' : 'Upvote skill'}
|
||||
>
|
||||
<Star className={`h-3.5 w-3.5 ${hasStarred ? 'fill-yellow-500 stroke-yellow-500' : ''}`} />
|
||||
<span>{displayCount} Upvotes</span>
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<button
|
||||
onClick={handleClick}
|
||||
className="flex items-center space-x-1 px-2 py-1 rounded-md bg-slate-50 dark:bg-slate-800/50 hover:bg-yellow-50 dark:hover:bg-yellow-900/20 text-slate-500 hover:text-yellow-600 dark:hover:text-yellow-500 transition-colors border border-slate-200 dark:border-slate-800 z-10 disabled:opacity-50"
|
||||
disabled={hasStarred || isLoading}
|
||||
title={hasStarred ? 'You already upvoted' : 'Upvote skill'}
|
||||
>
|
||||
<Star className={`h-4 w-4 ${hasStarred ? 'fill-yellow-400 stroke-yellow-400' : ''} ${isLoading ? 'animate-pulse' : ''}`} />
|
||||
<span className="text-xs font-semibold">{displayCount}</span>
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
export default SkillStarButton;
|
||||
Reference in New Issue
Block a user