forked from CarrotSkin/carrotskin
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.
144 lines
5.2 KiB
TypeScript
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>
|
|
);
|
|
}
|