Files
carrotskin/src/components/profile/CharacterCard.tsx
lan 2124790c8d feat: add Docker support and texture deletion functionality
Add Docker configuration with standalone output mode for containerized
deployment. Implement texture deletion API with proper error handling
and user feedback. Fix skin viewer sizing issues by using explicit
dimensions and removing conflicting layout properties. Add captcha
ID parameter to registration flow. Improve profile page UX including
Yggdrasil password reset display and character card editing controls.
2026-02-24 11:09:37 +08:00

144 lines
5.2 KiB
TypeScript

'use client';
import { motion } from 'framer-motion';
import { UserIcon, PencilIcon, TrashIcon, CheckIcon } from '@heroicons/react/24/outline';
import SkinViewer from '@/components/SkinViewer';
import type { Profile } from '@/lib/api';
interface CharacterCardProps {
profile: Profile;
skinUrl?: string;
isSlim?: boolean;
isEditing?: boolean;
editName?: string;
onEdit: (uuid: string, currentName: string) => void;
onSave: (uuid: string) => void;
onCancel: () => void;
onDelete: (uuid: string) => void;
onSetActive: (uuid: string) => void;
onSelectSkin: (uuid: string) => void;
onEditNameChange: (name: string) => void;
}
export default function CharacterCard({
profile,
skinUrl,
isSlim,
isEditing,
editName,
onEdit,
onSave,
onCancel,
onDelete,
onSetActive,
onSelectSkin,
onEditNameChange
}: CharacterCardProps) {
return (
<motion.div
key={profile.uuid}
className="bg-white/50 dark:bg-gray-800/50 backdrop-blur-lg rounded-2xl p-6 border border-white/20 dark:border-gray-700/50 shadow-lg"
whileHover={{ scale: 1.02, y: -5 }}
transition={{ duration: 0.2 }}
>
<div className="flex items-center justify-between mb-4">
<div className="flex items-center gap-2 flex-1">
{isEditing ? (
<input
type="text"
value={editName}
onChange={(e) => onEditNameChange(e.target.value)}
className="text-lg font-semibold bg-transparent border-b border-orange-500 focus:outline-none text-gray-900 dark:text-white flex-1"
onBlur={() => onSave(profile.uuid)}
onKeyPress={(e) => e.key === 'Enter' && onSave(profile.uuid)}
autoFocus
/>
) : (
<>
<h3 className="text-lg font-semibold text-gray-900 dark:text-white truncate">{profile.name}</h3>
<motion.button
onClick={() => onEdit(profile.uuid, profile.name)}
className="text-gray-500 hover:text-orange-500 transition-colors"
whileHover={{ scale: 1.1 }}
whileTap={{ scale: 0.9 }}
title="改名"
>
<PencilIcon className="w-4 h-4" />
</motion.button>
</>
)}
</div>
{profile.is_active && (
<span className="px-2 py-1 bg-gradient-to-r from-green-500 to-emerald-500 text-white text-xs rounded-full flex items-center space-x-1">
<CheckIcon className="w-3 h-3" />
<span>使</span>
</span>
)}
</div>
<div className="aspect-square bg-gradient-to-br from-orange-100 to-amber-100 dark:from-gray-700 dark:to-gray-600 rounded-xl mb-4 flex items-center justify-center relative overflow-hidden">
{skinUrl ? (
<SkinViewer
skinUrl={skinUrl}
isSlim={isSlim}
width={180}
height={180}
autoRotate={false}
/>
) : (
<motion.div
className="w-20 h-20 bg-gradient-to-br from-orange-400 to-amber-500 rounded-lg shadow-lg flex items-center justify-center"
whileHover={{ scale: 1.1, rotate: 5 }}
transition={{ type: 'spring', stiffness: 300 }}
>
<UserIcon className="w-10 h-10 text-white" />
</motion.div>
)}
</div>
{/* 操作按钮 */}
<div className="flex gap-2">
{isEditing ? (
<>
<motion.button
onClick={() => onSave(profile.uuid)}
className="flex-1 bg-gradient-to-r from-green-500 to-emerald-500 hover:from-green-600 hover:to-emerald-600 text-white text-sm py-2 px-3 rounded-lg transition-all duration-200"
whileHover={{ scale: 1.02 }}
whileTap={{ scale: 0.98 }}
>
</motion.button>
<motion.button
onClick={onCancel}
className="flex-1 border border-gray-300 dark:border-gray-600 text-gray-700 dark:text-gray-300 hover:bg-gray-50 dark:hover:bg-gray-700 text-sm py-2 px-3 rounded-lg transition-all duration-200"
whileHover={{ scale: 1.02 }}
whileTap={{ scale: 0.98 }}
>
</motion.button>
</>
) : (
<>
<motion.button
onClick={() => onSelectSkin(profile.uuid)}
className="flex-1 bg-gradient-to-r from-orange-500 to-amber-500 hover:from-orange-600 hover:to-amber-600 text-white text-sm py-2 px-3 rounded-lg transition-all duration-200"
whileHover={{ scale: 1.02 }}
whileTap={{ scale: 0.98 }}
>
</motion.button>
<motion.button
onClick={() => onDelete(profile.uuid)}
className="px-3 py-2 border border-red-500 text-red-500 hover:bg-red-500 hover:text-white rounded-lg transition-all duration-200"
whileHover={{ scale: 1.02 }}
whileTap={{ scale: 0.98 }}
>
<TrashIcon className="w-4 h-4" />
</motion.button>
</>
)}
</div>
</motion.div>
);
}