568 lines
25 KiB
TypeScript
Executable File
568 lines
25 KiB
TypeScript
Executable File
'use client';
|
|
|
|
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
|
import { Badge } from '@/components/ui/badge';
|
|
import { Button } from '@/components/ui/button';
|
|
import { ScrollArea } from '@/components/ui/scroll-area';
|
|
import { RotateCcw, Save, Trash2, Star, Flame, Clock, AlertCircle } from 'lucide-react';
|
|
import { useGameContext } from '../GameContext';
|
|
import { GUARDIANS, PRESTIGE_DEF, SKILLS_DEF, ELEMENTS } from '@/lib/game/constants';
|
|
import { getTierMultiplier, getBaseSkillId } from '@/lib/game/skill-evolution';
|
|
import { fmt, fmtDec, getBoonBonuses } from '@/lib/game/stores';
|
|
import type { Memory } from '@/lib/game/types';
|
|
import { useMemo, useState } from 'react';
|
|
|
|
export function GrimoireTab() {
|
|
const { store } = useGameContext();
|
|
const [showMemoryPicker, setShowMemoryPicker] = useState(false);
|
|
|
|
// Get all skills that can be saved to memory
|
|
const saveableSkills = useMemo(() => {
|
|
const skills: { skillId: string; level: number; tier: number; upgrades: string[]; name: string }[] = [];
|
|
|
|
for (const [skillId, level] of Object.entries(store.skills)) {
|
|
if (level && level > 0) {
|
|
const baseSkillId = getBaseSkillId(skillId);
|
|
const tier = store.skillTiers?.[baseSkillId] || 1;
|
|
const tieredSkillId = tier > 1 ? `${baseSkillId}_t${tier}` : baseSkillId;
|
|
const upgrades = store.skillUpgrades?.[tieredSkillId] || [];
|
|
const skillDef = SKILLS_DEF[baseSkillId];
|
|
|
|
if (skillId === baseSkillId || skillId.includes('_t')) {
|
|
const actualLevel = store.skills[tieredSkillId] || store.skills[baseSkillId] || 0;
|
|
if (actualLevel > 0) {
|
|
skills.push({
|
|
skillId: baseSkillId,
|
|
level: actualLevel,
|
|
tier,
|
|
upgrades,
|
|
name: skillDef?.name || baseSkillId,
|
|
});
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
const uniqueSkills = new Map<string, typeof skills[0]>();
|
|
for (const skill of skills) {
|
|
const existing = uniqueSkills.get(skill.skillId);
|
|
if (!existing || skill.tier > existing.tier || (skill.tier === existing.tier && skill.level > existing.level)) {
|
|
uniqueSkills.set(skill.skillId, skill);
|
|
}
|
|
}
|
|
|
|
return Array.from(uniqueSkills.values()).sort((a, b) => {
|
|
if (a.tier !== b.tier) return b.tier - a.tier;
|
|
if (a.level !== b.level) return b.level - a.level;
|
|
return a.name.localeCompare(b.name);
|
|
});
|
|
}, [store.skills, store.skillTiers, store.skillUpgrades]);
|
|
|
|
const isSkillInMemory = (skillId: string) => store.memories.some(m => m.skillId === skillId);
|
|
const canAddMore = store.memories.length < store.memorySlots;
|
|
|
|
const addToMemory = (skill: typeof saveableSkills[0]) => {
|
|
const memory: Memory = {
|
|
skillId: skill.skillId,
|
|
level: skill.level,
|
|
tier: skill.tier,
|
|
upgrades: skill.upgrades,
|
|
};
|
|
store.addMemory(memory);
|
|
};
|
|
|
|
// Calculate total boons from active pacts
|
|
const activeBoons = useMemo(() => {
|
|
return getBoonBonuses(store.signedPacts);
|
|
}, [store.signedPacts]);
|
|
|
|
// Check if player can sign a pact
|
|
const canSignPact = (floor: number) => {
|
|
const guardian = GUARDIANS[floor];
|
|
if (!guardian) return false;
|
|
if (!store.defeatedGuardians.includes(floor)) return false;
|
|
if (store.signedPacts.includes(floor)) return false;
|
|
if (store.signedPacts.length >= store.pactSlots) return false;
|
|
if (store.rawMana < guardian.pactCost) return false;
|
|
if (store.pactRitualFloor !== null) return false;
|
|
return true;
|
|
};
|
|
|
|
// Get pact affinity bonus for display
|
|
const pactAffinityBonus = (store.prestigeUpgrades.pactAffinity || 0) * 10;
|
|
|
|
return (
|
|
<div className="grid grid-cols-1 lg:grid-cols-2 gap-4">
|
|
{/* Current Status */}
|
|
<Card className="bg-gray-900/80 border-gray-700">
|
|
<CardHeader className="pb-2">
|
|
<CardTitle className="text-amber-400 game-panel-title text-xs">Loop Status</CardTitle>
|
|
</CardHeader>
|
|
<CardContent className="space-y-3">
|
|
<div className="grid grid-cols-2 gap-3">
|
|
<div className="p-3 bg-gray-800/50 rounded">
|
|
<div className="text-2xl font-bold text-amber-400 game-mono">{store.loopCount}</div>
|
|
<div className="text-xs text-gray-400">Loops Completed</div>
|
|
</div>
|
|
<div className="p-3 bg-gray-800/50 rounded">
|
|
<div className="text-2xl font-bold text-purple-400 game-mono">{fmt(store.insight)}</div>
|
|
<div className="text-xs text-gray-400">Current Insight</div>
|
|
</div>
|
|
<div className="p-3 bg-gray-800/50 rounded">
|
|
<div className="text-2xl font-bold text-blue-400 game-mono">{fmt(store.totalInsight)}</div>
|
|
<div className="text-xs text-gray-400">Total Insight</div>
|
|
</div>
|
|
<div className="p-3 bg-gray-800/50 rounded">
|
|
<div className="text-2xl font-bold text-green-400 game-mono">{store.memorySlots}</div>
|
|
<div className="text-xs text-gray-400">Memory Slots</div>
|
|
</div>
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
|
|
{/* Pact Slots & Active Ritual */}
|
|
<Card className="bg-gray-900/80 border-gray-700">
|
|
<CardHeader className="pb-2">
|
|
<CardTitle className="text-amber-400 game-panel-title text-xs flex items-center gap-2">
|
|
<Flame className="w-4 h-4" />
|
|
Pact Slots ({store.signedPacts.length}/{store.pactSlots})
|
|
</CardTitle>
|
|
</CardHeader>
|
|
<CardContent>
|
|
{/* Active Ritual Progress */}
|
|
{store.pactRitualFloor !== null && (
|
|
<div className="mb-4 p-3 rounded border-2 border-amber-500/50 bg-amber-900/20">
|
|
{(() => {
|
|
const guardian = GUARDIANS[store.pactRitualFloor];
|
|
if (!guardian) return null;
|
|
const requiredTime = guardian.pactTime * (1 - (store.prestigeUpgrades.pactAffinity || 0) * 0.1);
|
|
const progress = Math.min(100, (store.pactRitualProgress / requiredTime) * 100);
|
|
return (
|
|
<>
|
|
<div className="flex items-center justify-between mb-2">
|
|
<span className="text-amber-300 font-semibold text-sm">Signing Pact with {guardian.name}</span>
|
|
<span className="text-xs text-gray-400">{fmtDec(progress, 1)}%</span>
|
|
</div>
|
|
<div className="w-full h-2 bg-gray-700 rounded overflow-hidden mb-2">
|
|
<div
|
|
className="h-full bg-amber-500 transition-all duration-300"
|
|
style={{ width: `${progress}%` }}
|
|
/>
|
|
</div>
|
|
<div className="flex items-center justify-between">
|
|
<div className="text-xs text-gray-400">
|
|
<Clock className="w-3 h-3 inline mr-1" />
|
|
{fmtDec(store.pactRitualProgress, 1)}h / {fmtDec(requiredTime, 1)}h
|
|
</div>
|
|
<Button
|
|
size="sm"
|
|
variant="outline"
|
|
className="h-6 text-xs border-red-500/50 text-red-400 hover:bg-red-900/20"
|
|
onClick={() => store.cancelPactRitual()}
|
|
>
|
|
Cancel Ritual
|
|
</Button>
|
|
</div>
|
|
</>
|
|
);
|
|
})()}
|
|
</div>
|
|
)}
|
|
|
|
{/* Active Pacts */}
|
|
{store.signedPacts.length > 0 ? (
|
|
<div className="space-y-2">
|
|
{store.signedPacts.map((floor) => {
|
|
const guardian = GUARDIANS[floor];
|
|
if (!guardian) return null;
|
|
return (
|
|
<div
|
|
key={floor}
|
|
className="p-3 rounded border"
|
|
style={{ borderColor: guardian.color, backgroundColor: `${guardian.color}15` }}
|
|
>
|
|
<div className="flex items-start justify-between">
|
|
<div>
|
|
<div className="font-semibold text-sm" style={{ color: guardian.color }}>
|
|
{guardian.name}
|
|
</div>
|
|
<div className="text-xs text-gray-400">Floor {floor} Guardian</div>
|
|
<div className="text-xs text-amber-300 mt-1 italic">"{guardian.uniquePerk}"</div>
|
|
</div>
|
|
<Button
|
|
size="sm"
|
|
variant="ghost"
|
|
className="h-6 w-6 p-0 text-red-400 hover:text-red-300"
|
|
onClick={() => store.removePact(floor)}
|
|
title="Break Pact"
|
|
>
|
|
<Trash2 className="w-3 h-3" />
|
|
</Button>
|
|
</div>
|
|
<div className="mt-2 flex flex-wrap gap-1">
|
|
{guardian.boons.map((boon, idx) => (
|
|
<Badge key={idx} className="bg-gray-700/50 text-gray-200 text-xs">
|
|
{boon.desc}
|
|
</Badge>
|
|
))}
|
|
</div>
|
|
</div>
|
|
);
|
|
})}
|
|
</div>
|
|
) : (
|
|
<div className="text-gray-500 text-sm text-center py-4">
|
|
No active pacts. Defeat guardians and sign pacts to gain boons.
|
|
</div>
|
|
)}
|
|
</CardContent>
|
|
</Card>
|
|
|
|
{/* Available Guardians for Pacts */}
|
|
<Card className="bg-gray-900/80 border-gray-700">
|
|
<CardHeader className="pb-2">
|
|
<CardTitle className="text-amber-400 game-panel-title text-xs flex items-center gap-2">
|
|
<AlertCircle className="w-4 h-4" />
|
|
Available Guardians ({store.defeatedGuardians.length})
|
|
</CardTitle>
|
|
</CardHeader>
|
|
<CardContent>
|
|
{store.defeatedGuardians.length === 0 ? (
|
|
<div className="text-gray-500 text-sm text-center py-4">
|
|
Defeat guardians in the Spire to make them available for pacts.
|
|
</div>
|
|
) : (
|
|
<ScrollArea className="h-64">
|
|
<div className="space-y-2 pr-2">
|
|
{store.defeatedGuardians
|
|
.sort((a, b) => a - b)
|
|
.map((floor) => {
|
|
const guardian = GUARDIANS[floor];
|
|
if (!guardian) return null;
|
|
const canSign = canSignPact(floor);
|
|
const notEnoughMana = store.rawMana < guardian.pactCost;
|
|
const atCapacity = store.signedPacts.length >= store.pactSlots;
|
|
|
|
return (
|
|
<div
|
|
key={floor}
|
|
className="p-3 rounded border border-gray-700 bg-gray-800/50"
|
|
>
|
|
<div className="flex items-start justify-between mb-2">
|
|
<div>
|
|
<div className="font-semibold text-sm" style={{ color: guardian.color }}>
|
|
{guardian.name}
|
|
</div>
|
|
<div className="text-xs text-gray-400">
|
|
Floor {floor} • {ELEMENTS[guardian.element]?.name || guardian.element}
|
|
</div>
|
|
</div>
|
|
<div className="text-right">
|
|
<div className="text-xs text-amber-300">{fmt(guardian.pactCost)} mana</div>
|
|
<div className="text-xs text-gray-400">{guardian.pactTime}h ritual</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="text-xs text-purple-300 italic mb-2">"{guardian.uniquePerk}"</div>
|
|
|
|
<div className="flex flex-wrap gap-1 mb-2">
|
|
{guardian.boons.map((boon, idx) => (
|
|
<Badge key={idx} className="bg-gray-700/50 text-gray-200 text-xs">
|
|
{boon.desc}
|
|
</Badge>
|
|
))}
|
|
</div>
|
|
|
|
<Button
|
|
size="sm"
|
|
variant={canSign ? 'default' : 'outline'}
|
|
className="w-full"
|
|
disabled={!canSign}
|
|
onClick={() => store.startPactRitual(floor, store.rawMana)}
|
|
>
|
|
{atCapacity
|
|
? 'Pact Slots Full'
|
|
: notEnoughMana
|
|
? 'Not Enough Mana'
|
|
: store.pactRitualFloor !== null
|
|
? 'Ritual in Progress'
|
|
: 'Start Pact Ritual'}
|
|
</Button>
|
|
</div>
|
|
);
|
|
})}
|
|
</div>
|
|
</ScrollArea>
|
|
)}
|
|
</CardContent>
|
|
</Card>
|
|
|
|
{/* Memory Slots */}
|
|
<Card className="bg-gray-900/80 border-gray-700">
|
|
<CardHeader className="pb-2">
|
|
<CardTitle className="text-amber-400 game-panel-title text-xs flex items-center gap-2">
|
|
<Save className="w-4 h-4" />
|
|
Memory Slots ({store.memories.length}/{store.memorySlots})
|
|
</CardTitle>
|
|
</CardHeader>
|
|
<CardContent>
|
|
<p className="text-xs text-gray-400 mb-3">
|
|
Skills saved to memory will retain their level, tier, and upgrades when you start a new loop.
|
|
</p>
|
|
|
|
{/* Saved Memories */}
|
|
{store.memories.length > 0 ? (
|
|
<div className="space-y-1 mb-3">
|
|
{store.memories.map((memory) => {
|
|
const skillDef = SKILLS_DEF[memory.skillId];
|
|
const tierMult = getTierMultiplier(memory.tier > 1 ? `${memory.skillId}_t${memory.tier}` : memory.skillId);
|
|
return (
|
|
<div
|
|
key={memory.skillId}
|
|
className="flex items-center justify-between p-2 rounded border border-amber-600/30 bg-amber-900/10"
|
|
>
|
|
<div className="flex items-center gap-2">
|
|
<span className="font-semibold text-sm text-amber-300">{skillDef?.name || memory.skillId}</span>
|
|
{memory.tier > 1 && (
|
|
<Badge className="bg-purple-600/50 text-purple-200 text-xs">
|
|
T{memory.tier} ({tierMult}x)
|
|
</Badge>
|
|
)}
|
|
<span className="text-purple-400 text-xs">Lv.{memory.level}</span>
|
|
{memory.upgrades.length > 0 && (
|
|
<Badge className="bg-amber-700/50 text-amber-200 text-xs flex items-center gap-1">
|
|
<Star className="w-3 h-3" />
|
|
{memory.upgrades.length}
|
|
</Badge>
|
|
)}
|
|
</div>
|
|
<Button
|
|
size="sm"
|
|
variant="ghost"
|
|
className="h-6 w-6 p-0 text-red-400 hover:text-red-300"
|
|
onClick={() => store.removeMemory(memory.skillId)}
|
|
>
|
|
<Trash2 className="w-3 h-3" />
|
|
</Button>
|
|
</div>
|
|
);
|
|
})}
|
|
</div>
|
|
) : (
|
|
<div className="text-gray-500 text-sm mb-3 text-center py-2">
|
|
No memories saved. Add skills below.
|
|
</div>
|
|
)}
|
|
|
|
{/* Add Memory Button */}
|
|
{canAddMore && (
|
|
<Button
|
|
size="sm"
|
|
variant="outline"
|
|
className="w-full"
|
|
onClick={() => setShowMemoryPicker(!showMemoryPicker)}
|
|
>
|
|
{showMemoryPicker ? 'Hide Skills' : 'Add Skill to Memory'}
|
|
</Button>
|
|
)}
|
|
|
|
{/* Skill Picker */}
|
|
{showMemoryPicker && canAddMore && (
|
|
<ScrollArea className="h-48 mt-2">
|
|
<div className="space-y-1 pr-2">
|
|
{saveableSkills.length === 0 ? (
|
|
<div className="text-gray-500 text-xs text-center py-4">
|
|
No skills with progress to save
|
|
</div>
|
|
) : (
|
|
saveableSkills.map((skill) => {
|
|
const isInMemory = isSkillInMemory(skill.skillId);
|
|
const tierMult = getTierMultiplier(skill.tier > 1 ? `${skill.skillId}_t${skill.tier}` : skill.skillId);
|
|
|
|
return (
|
|
<div
|
|
key={skill.skillId}
|
|
className={`p-2 rounded border cursor-pointer transition-all ${
|
|
isInMemory
|
|
? 'border-amber-500 bg-amber-900/30 opacity-50'
|
|
: 'border-gray-700 bg-gray-800/50 hover:border-amber-500/50'
|
|
}`}
|
|
onClick={() => !isInMemory && addToMemory(skill)}
|
|
>
|
|
<div className="flex items-center justify-between">
|
|
<div className="flex items-center gap-2">
|
|
<span className="font-semibold text-sm">{skill.name}</span>
|
|
{skill.tier > 1 && (
|
|
<Badge className="bg-purple-600/50 text-purple-200 text-xs">
|
|
T{skill.tier} ({tierMult}x)
|
|
</Badge>
|
|
)}
|
|
</div>
|
|
<div className="flex items-center gap-2">
|
|
<span className="text-purple-400 text-sm">Lv.{skill.level}</span>
|
|
{skill.upgrades.length > 0 && (
|
|
<Badge className="bg-amber-700/50 text-amber-200 text-xs flex items-center gap-1">
|
|
<Star className="w-3 h-3" />
|
|
{skill.upgrades.length}
|
|
</Badge>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
})
|
|
)}
|
|
</div>
|
|
</ScrollArea>
|
|
)}
|
|
</CardContent>
|
|
</Card>
|
|
|
|
{/* Active Boons Summary */}
|
|
{store.signedPacts.length > 0 && (
|
|
<Card className="bg-gray-900/80 border-gray-700">
|
|
<CardHeader className="pb-2">
|
|
<CardTitle className="text-amber-400 game-panel-title text-xs">Active Boons Summary</CardTitle>
|
|
</CardHeader>
|
|
<CardContent>
|
|
<div className="grid grid-cols-2 gap-2 text-xs">
|
|
{activeBoons.maxMana > 0 && (
|
|
<div className="flex justify-between">
|
|
<span className="text-gray-400">Max Mana:</span>
|
|
<span className="text-blue-300">+{activeBoons.maxMana}</span>
|
|
</div>
|
|
)}
|
|
{activeBoons.manaRegen > 0 && (
|
|
<div className="flex justify-between">
|
|
<span className="text-gray-400">Mana Regen:</span>
|
|
<span className="text-blue-300">+{activeBoons.manaRegen}/h</span>
|
|
</div>
|
|
)}
|
|
{activeBoons.castingSpeed > 0 && (
|
|
<div className="flex justify-between">
|
|
<span className="text-gray-400">Cast Speed:</span>
|
|
<span className="text-amber-300">+{activeBoons.castingSpeed}%</span>
|
|
</div>
|
|
)}
|
|
{activeBoons.elementalDamage > 0 && (
|
|
<div className="flex justify-between">
|
|
<span className="text-gray-400">Elem. Damage:</span>
|
|
<span className="text-red-300">+{activeBoons.elementalDamage}%</span>
|
|
</div>
|
|
)}
|
|
{activeBoons.rawDamage > 0 && (
|
|
<div className="flex justify-between">
|
|
<span className="text-gray-400">Raw Damage:</span>
|
|
<span className="text-red-300">+{activeBoons.rawDamage}%</span>
|
|
</div>
|
|
)}
|
|
{activeBoons.critChance > 0 && (
|
|
<div className="flex justify-between">
|
|
<span className="text-gray-400">Crit Chance:</span>
|
|
<span className="text-yellow-300">+{activeBoons.critChance}%</span>
|
|
</div>
|
|
)}
|
|
{activeBoons.critDamage > 0 && (
|
|
<div className="flex justify-between">
|
|
<span className="text-gray-400">Crit Damage:</span>
|
|
<span className="text-yellow-300">+{activeBoons.critDamage}%</span>
|
|
</div>
|
|
)}
|
|
{activeBoons.spellEfficiency > 0 && (
|
|
<div className="flex justify-between">
|
|
<span className="text-gray-400">Spell Cost:</span>
|
|
<span className="text-green-300">-{activeBoons.spellEfficiency}%</span>
|
|
</div>
|
|
)}
|
|
{activeBoons.manaGain > 0 && (
|
|
<div className="flex justify-between">
|
|
<span className="text-gray-400">Mana Gain:</span>
|
|
<span className="text-blue-300">+{activeBoons.manaGain}%</span>
|
|
</div>
|
|
)}
|
|
{activeBoons.insightGain > 0 && (
|
|
<div className="flex justify-between">
|
|
<span className="text-gray-400">Insight Gain:</span>
|
|
<span className="text-purple-300">+{activeBoons.insightGain}%</span>
|
|
</div>
|
|
)}
|
|
{activeBoons.studySpeed > 0 && (
|
|
<div className="flex justify-between">
|
|
<span className="text-gray-400">Study Speed:</span>
|
|
<span className="text-cyan-300">+{activeBoons.studySpeed}%</span>
|
|
</div>
|
|
)}
|
|
{activeBoons.prestigeInsight > 0 && (
|
|
<div className="flex justify-between">
|
|
<span className="text-gray-400">Prestige Insight:</span>
|
|
<span className="text-purple-300">+{activeBoons.prestigeInsight}/loop</span>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
)}
|
|
|
|
{/* Prestige Upgrades */}
|
|
<Card className="bg-gray-900/80 border-gray-700 lg:col-span-2">
|
|
<CardHeader className="pb-2">
|
|
<CardTitle className="text-amber-400 game-panel-title text-xs">Insight Upgrades (Permanent)</CardTitle>
|
|
</CardHeader>
|
|
<CardContent>
|
|
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-3">
|
|
{Object.entries(PRESTIGE_DEF).map(([id, def]) => {
|
|
const level = store.prestigeUpgrades[id] || 0;
|
|
const maxed = level >= def.max;
|
|
const canBuy = !maxed && store.insight >= def.cost;
|
|
|
|
return (
|
|
<div key={id} className="p-3 rounded border border-gray-700 bg-gray-800/50">
|
|
<div className="flex items-center justify-between mb-2">
|
|
<div className="font-semibold text-amber-400 text-sm">{def.name}</div>
|
|
<Badge variant="outline" className="text-xs">
|
|
{level}/{def.max}
|
|
</Badge>
|
|
</div>
|
|
<div className="text-xs text-gray-400 italic mb-2">{def.desc}</div>
|
|
<Button
|
|
size="sm"
|
|
variant={canBuy ? 'default' : 'outline'}
|
|
className="w-full"
|
|
disabled={!canBuy}
|
|
onClick={() => store.doPrestige(id)}
|
|
>
|
|
{maxed ? 'Maxed' : `Upgrade (${fmt(def.cost)} insight)`}
|
|
</Button>
|
|
</div>
|
|
);
|
|
})}
|
|
</div>
|
|
|
|
{/* Reset Game Button */}
|
|
<div className="mt-4 pt-4 border-t border-gray-700">
|
|
<div className="flex items-center justify-between">
|
|
<div>
|
|
<div className="text-sm text-gray-400">Reset All Progress</div>
|
|
<div className="text-xs text-gray-500">Clear all data and start fresh</div>
|
|
</div>
|
|
<Button
|
|
size="sm"
|
|
variant="outline"
|
|
className="border-red-600/50 text-red-400 hover:bg-red-900/20"
|
|
onClick={() => {
|
|
if (confirm('Are you sure you want to reset ALL progress? This cannot be undone!')) {
|
|
store.resetGame();
|
|
}
|
|
}}
|
|
>
|
|
<RotateCcw className="w-4 h-4 mr-1" />
|
|
Reset
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
</div>
|
|
);
|
|
}
|