mirror of
https://github.com/Dictionarry-Hub/profilarr.git
synced 2026-01-30 14:20:59 +01:00
246 lines
10 KiB
JavaScript
246 lines
10 KiB
JavaScript
import React from 'react';
|
|
import PropTypes from 'prop-types';
|
|
import {Copy, Check} from 'lucide-react';
|
|
import Tooltip from '@ui/Tooltip';
|
|
|
|
function FormatCard({
|
|
format,
|
|
onEdit,
|
|
onClone,
|
|
sortBy,
|
|
isSelectionMode,
|
|
isSelected,
|
|
willBeSelected,
|
|
onSelect
|
|
}) {
|
|
const {content} = format;
|
|
const totalTests = content.tests?.length || 0;
|
|
const passedTests = content.tests?.filter(t => t.passes)?.length || 0;
|
|
const passRate = Math.round((passedTests / totalTests) * 100) || 0;
|
|
|
|
const getConditionStyle = condition => {
|
|
if (condition.negate) {
|
|
return 'bg-red-100 border-red-200 text-red-700 dark:bg-red-900/30 dark:border-red-700 dark:text-red-300';
|
|
}
|
|
if (condition.required) {
|
|
return 'bg-green-100 border-green-200 text-green-700 dark:bg-green-900/30 dark:border-green-700 dark:text-green-300';
|
|
}
|
|
return 'bg-blue-100 border-blue-200 text-blue-700 dark:bg-blue-900/30 dark:border-blue-700 dark:text-blue-300';
|
|
};
|
|
|
|
const getDisplayConditions = conditions => {
|
|
if (!conditions?.length) return [];
|
|
|
|
// Sort conditions: required first, then non-required
|
|
const sortedConditions = [...conditions].sort((a, b) => {
|
|
if (a.required && !b.required) return -1;
|
|
if (!a.required && b.required) return 1;
|
|
return 0;
|
|
});
|
|
|
|
if (sortedConditions.length <= 5) return sortedConditions;
|
|
|
|
// Take first 6 conditions and add a count of remaining ones
|
|
const displayConditions = sortedConditions.slice(0, 6);
|
|
const remainingCount = sortedConditions.length - 6;
|
|
|
|
// Add a virtual condition for the count
|
|
displayConditions.push({
|
|
name: `+${remainingCount} more...`,
|
|
isCounter: true
|
|
});
|
|
|
|
return displayConditions;
|
|
};
|
|
|
|
const handleClick = e => {
|
|
if (isSelectionMode) {
|
|
onSelect(e);
|
|
} else {
|
|
onEdit();
|
|
}
|
|
};
|
|
|
|
const handleCloneClick = e => {
|
|
e.stopPropagation();
|
|
onClone(format);
|
|
};
|
|
|
|
const handleMouseDown = e => {
|
|
// Prevent text selection when shift-clicking
|
|
if (e.shiftKey) {
|
|
e.preventDefault();
|
|
}
|
|
};
|
|
|
|
return (
|
|
<div
|
|
className={`h-full w-full bg-white dark:bg-gray-800 border ${
|
|
isSelected
|
|
? 'border-blue-500 dark:border-blue-400'
|
|
: willBeSelected
|
|
? 'border-blue-300 dark:border-blue-600'
|
|
: 'border-gray-200 dark:border-gray-700'
|
|
} rounded-lg shadow hover:shadow-lg ${
|
|
isSelectionMode
|
|
? isSelected
|
|
? 'hover:border-blue-400'
|
|
: 'hover:border-gray-400'
|
|
: 'hover:border-blue-400'
|
|
} dark:hover:border-blue-500 transition-all cursor-pointer`}
|
|
onClick={handleClick}
|
|
onMouseDown={handleMouseDown}>
|
|
<div className='flex flex-col p-6 gap-3 h-full'>
|
|
{/* Header Section */}
|
|
<div className='flex justify-between items-start gap-4'>
|
|
<div className='flex-1'>
|
|
<h3 className='text-xl font-semibold text-gray-900 dark:text-gray-100 truncate'>
|
|
{content.name}
|
|
</h3>
|
|
{sortBy === 'dateModified' && format.modified_date && (
|
|
<p className='text-xs text-gray-500 dark:text-gray-400 mt-1'>
|
|
Modified:{' '}
|
|
{new Date(
|
|
format.modified_date
|
|
).toLocaleString()}
|
|
</p>
|
|
)}
|
|
</div>
|
|
<div className='w-8 h-8 flex items-center justify-center'>
|
|
{isSelectionMode ? (
|
|
<Tooltip
|
|
content={
|
|
isSelected
|
|
? 'Selected'
|
|
: willBeSelected
|
|
? 'Will be selected'
|
|
: 'Select'
|
|
}>
|
|
<div
|
|
className={`w-6 h-6 rounded-full flex items-center justify-center ${
|
|
isSelected
|
|
? 'bg-blue-500'
|
|
: willBeSelected
|
|
? 'bg-blue-200 dark:bg-blue-800'
|
|
: 'bg-gray-200 dark:bg-gray-700'
|
|
} transition-colors hover:bg-blue-600`}>
|
|
{isSelected && (
|
|
<Check
|
|
size={14}
|
|
className='text-white'
|
|
/>
|
|
)}
|
|
{willBeSelected && !isSelected && (
|
|
<div className='w-1.5 h-1.5 rounded-full bg-blue-400' />
|
|
)}
|
|
</div>
|
|
</Tooltip>
|
|
) : (
|
|
<button
|
|
onClick={handleCloneClick}
|
|
className='w-8 h-8 rounded-full transition-colors hover:bg-gray-100 dark:hover:bg-gray-700 flex items-center justify-center'>
|
|
<Copy className='w-5 h-5 text-gray-500 dark:text-gray-400' />
|
|
</button>
|
|
)}
|
|
</div>
|
|
</div>
|
|
|
|
{/* Description */}
|
|
{content.description && (
|
|
<p className='text-gray-600 dark:text-gray-300 text-sm line-clamp-2'>
|
|
{content.description}
|
|
</p>
|
|
)}
|
|
|
|
{/* Conditions and Test Results */}
|
|
<div className='flex justify-between items-start gap-4'>
|
|
<div className='flex flex-wrap gap-2 flex-1'>
|
|
{getDisplayConditions(content.conditions)?.map(
|
|
(condition, index) => (
|
|
<span
|
|
key={index}
|
|
className={`px-2 py-1 rounded text-xs font-medium ${
|
|
condition.isCounter
|
|
? 'bg-gray-100 text-gray-600 dark:bg-gray-700 dark:text-gray-300'
|
|
: getConditionStyle(condition)
|
|
}`}>
|
|
{condition.name}
|
|
</span>
|
|
)
|
|
)}
|
|
</div>
|
|
|
|
{totalTests > 0 && (
|
|
<div className='flex items-center gap-2 bg-gray-100 dark:bg-gray-700 px-2 py-1 rounded shrink-0'>
|
|
<span
|
|
className={`text-sm font-medium ${
|
|
passRate === 100
|
|
? 'text-green-600 dark:text-green-400'
|
|
: passRate >= 80
|
|
? 'text-yellow-600 dark:text-yellow-400'
|
|
: 'text-red-600 dark:text-red-400'
|
|
}`}>
|
|
{passRate}% Pass Rate
|
|
</span>
|
|
<span className='text-gray-500 dark:text-gray-400 text-xs'>
|
|
({passedTests}/{totalTests})
|
|
</span>
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
{/* Tags */}
|
|
{content.tags?.length > 0 && (
|
|
<div className='flex flex-wrap gap-2 pt-2 border-t border-gray-200 dark:border-gray-700 mt-auto'>
|
|
{content.tags.map(tag => (
|
|
<span
|
|
key={tag}
|
|
className='bg-blue-100 dark:bg-blue-900 text-blue-800 dark:text-blue-300 px-2 py-1 rounded text-xs'>
|
|
{tag}
|
|
</span>
|
|
))}
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
FormatCard.propTypes = {
|
|
format: PropTypes.shape({
|
|
file_name: PropTypes.string.isRequired,
|
|
modified_date: PropTypes.string.isRequired,
|
|
content: PropTypes.shape({
|
|
name: PropTypes.string.isRequired,
|
|
description: PropTypes.string,
|
|
conditions: PropTypes.arrayOf(
|
|
PropTypes.shape({
|
|
name: PropTypes.string.isRequired,
|
|
type: PropTypes.string.isRequired,
|
|
pattern: PropTypes.string,
|
|
required: PropTypes.bool,
|
|
negate: PropTypes.bool
|
|
})
|
|
),
|
|
tags: PropTypes.arrayOf(PropTypes.string),
|
|
tests: PropTypes.arrayOf(
|
|
PropTypes.shape({
|
|
id: PropTypes.number.isRequired,
|
|
input: PropTypes.string.isRequired,
|
|
expected: PropTypes.bool.isRequired,
|
|
passes: PropTypes.bool.isRequired
|
|
})
|
|
)
|
|
}).isRequired
|
|
}).isRequired,
|
|
onEdit: PropTypes.func.isRequired,
|
|
onClone: PropTypes.func.isRequired,
|
|
sortBy: PropTypes.string.isRequired,
|
|
isSelectionMode: PropTypes.bool.isRequired,
|
|
isSelected: PropTypes.bool.isRequired,
|
|
willBeSelected: PropTypes.bool,
|
|
onSelect: PropTypes.func.isRequired
|
|
};
|
|
|
|
export default FormatCard;
|