More text improvements.
This commit is contained in:
+1
-1
@@ -12,7 +12,7 @@
|
||||
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Cinzel:wght@400;600;700;900&family=EB+Garamond:ital,wght@0,400;0,500;0,600;1,400&display=swap" rel="stylesheet">
|
||||
<link href="https://fonts.googleapis.com/css2?family=Cinzel:wght@400;600;700;900&family=Alegreya+Sans:ital,wght@0,400;0,500;0,700;1,400&display=swap" rel="stylesheet">
|
||||
<title>TTRPG Initiative Tracker</title>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
+126
-123
@@ -44,7 +44,7 @@ if (typeof document !== 'undefined') {
|
||||
// CONSTANTS
|
||||
// ============================================================================
|
||||
|
||||
const APP_VERSION = 'v0.2.2';
|
||||
const APP_VERSION = 'v0.2.4';
|
||||
const DEFAULT_MAX_HP = 10;
|
||||
const DEFAULT_INIT_MOD = 0;
|
||||
const MONSTER_DEFAULT_INIT_MOD = 2;
|
||||
@@ -225,10 +225,10 @@ function Modal({ onClose, title, children }) {
|
||||
|
||||
return (
|
||||
<div className="fixed inset-0 bg-black bg-opacity-75 flex items-center justify-center p-4 z-50">
|
||||
<div className="bg-slate-800 p-6 rounded-lg shadow-xl w-full max-w-md">
|
||||
<div className="bg-stone-900 p-6 rounded-lg shadow-xl w-full max-w-md">
|
||||
<div className="flex justify-between items-center mb-4">
|
||||
<h2 className="text-xl font-semibold text-amber-300 font-cinzel tracking-wide">{title}</h2>
|
||||
<button onClick={onClose} className="text-slate-400 hover:text-slate-200">
|
||||
<button onClick={onClose} className="text-stone-400 hover:text-stone-200">
|
||||
<XCircle size={24} />
|
||||
</button>
|
||||
</div>
|
||||
@@ -243,16 +243,16 @@ function ConfirmationModal({ isOpen, onClose, onConfirm, title, message }) {
|
||||
|
||||
return (
|
||||
<div className="fixed inset-0 bg-black bg-opacity-75 flex items-center justify-center p-4 z-50">
|
||||
<div className="bg-slate-800 p-6 rounded-lg shadow-xl w-full max-w-md">
|
||||
<div className="bg-stone-900 p-6 rounded-lg shadow-xl w-full max-w-md">
|
||||
<div className="flex items-center mb-4">
|
||||
<AlertTriangle size={24} className="text-yellow-400 mr-3 flex-shrink-0" />
|
||||
<h2 className="text-xl font-semibold text-yellow-300">{title || "Confirm Action"}</h2>
|
||||
</div>
|
||||
<p className="text-slate-300 mb-6">{message || "Are you sure you want to proceed?"}</p>
|
||||
<p className="text-stone-300 mb-6">{message || "Are you sure you want to proceed?"}</p>
|
||||
<div className="flex justify-end space-x-3">
|
||||
<button
|
||||
onClick={onClose}
|
||||
className="px-4 py-2 text-sm font-medium text-slate-300 bg-slate-600 hover:bg-slate-500 rounded-md transition-colors"
|
||||
className="px-4 py-2 text-sm font-medium text-stone-300 bg-stone-700 hover:bg-stone-600 rounded-md transition-colors"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
@@ -270,7 +270,7 @@ function ConfirmationModal({ isOpen, onClose, onConfirm, title, message }) {
|
||||
|
||||
function LoadingSpinner({ message = "Loading..." }) {
|
||||
return (
|
||||
<div className="min-h-screen bg-slate-900 text-white flex flex-col items-center justify-center p-4">
|
||||
<div className="min-h-screen bg-stone-950 text-white flex flex-col items-center justify-center p-4">
|
||||
<div className="animate-spin rounded-full h-16 w-16 border-t-4 border-amber-500 border-solid"></div>
|
||||
<p className="mt-4 text-xl">{message}</p>
|
||||
</div>
|
||||
@@ -279,7 +279,7 @@ function LoadingSpinner({ message = "Loading..." }) {
|
||||
|
||||
function ErrorDisplay({ message, critical = false }) {
|
||||
return (
|
||||
<div className={`min-h-screen ${critical ? 'bg-red-900' : 'bg-slate-900'} text-white flex flex-col items-center justify-center p-4`}>
|
||||
<div className={`min-h-screen ${critical ? 'bg-red-900' : 'bg-stone-950'} text-white flex flex-col items-center justify-center p-4`}>
|
||||
<h1 className="text-3xl font-bold mb-4">
|
||||
{critical ? 'Configuration Error' : 'Error'}
|
||||
</h1>
|
||||
@@ -306,7 +306,7 @@ function CreateCampaignForm({ onCreate, onCancel }) {
|
||||
return (
|
||||
<form onSubmit={handleSubmit} className="space-y-4">
|
||||
<div>
|
||||
<label htmlFor="campaignName" className="block text-sm font-medium text-slate-300">
|
||||
<label htmlFor="campaignName" className="block text-sm font-medium text-stone-300">
|
||||
Campaign Name
|
||||
</label>
|
||||
<input
|
||||
@@ -314,12 +314,12 @@ function CreateCampaignForm({ onCreate, onCancel }) {
|
||||
id="campaignName"
|
||||
value={name}
|
||||
onChange={(e) => setName(e.target.value)}
|
||||
className="mt-1 block w-full px-3 py-2 bg-slate-700 border border-slate-600 rounded-md shadow-sm focus:outline-none focus:ring-amber-600 focus:border-amber-600 sm:text-sm text-white"
|
||||
className="mt-1 block w-full px-3 py-2 bg-stone-800 border border-stone-700 rounded-md shadow-sm focus:outline-none focus:ring-amber-600 focus:border-amber-600 sm:text-sm text-white"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label htmlFor="backgroundUrl" className="block text-sm font-medium text-slate-300">
|
||||
<label htmlFor="backgroundUrl" className="block text-sm font-medium text-stone-300">
|
||||
Player Display Background URL (Optional)
|
||||
</label>
|
||||
<input
|
||||
@@ -328,20 +328,20 @@ function CreateCampaignForm({ onCreate, onCancel }) {
|
||||
value={backgroundUrl}
|
||||
onChange={(e) => setBackgroundUrl(e.target.value)}
|
||||
placeholder="https://example.com/image.jpg"
|
||||
className="mt-1 block w-full px-3 py-2 bg-slate-700 border border-slate-600 rounded-md shadow-sm focus:outline-none focus:ring-amber-600 focus:border-amber-600 sm:text-sm text-white"
|
||||
className="mt-1 block w-full px-3 py-2 bg-stone-800 border border-stone-700 rounded-md shadow-sm focus:outline-none focus:ring-amber-600 focus:border-amber-600 sm:text-sm text-white"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex justify-end space-x-3">
|
||||
<button
|
||||
type="button"
|
||||
onClick={onCancel}
|
||||
className="px-4 py-2 text-sm font-medium text-slate-300 bg-slate-600 hover:bg-slate-500 rounded-md transition-colors"
|
||||
className="px-4 py-2 text-sm font-medium text-stone-300 bg-stone-700 hover:bg-stone-600 rounded-md transition-colors"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
type="submit"
|
||||
className="px-4 py-2 text-sm font-medium text-white bg-green-500 hover:bg-green-600 rounded-md transition-colors"
|
||||
className="px-4 py-2 text-sm font-medium text-white bg-red-700 hover:bg-red-800 rounded-md transition-colors"
|
||||
>
|
||||
Create
|
||||
</button>
|
||||
@@ -363,7 +363,7 @@ function CreateEncounterForm({ onCreate, onCancel }) {
|
||||
return (
|
||||
<form onSubmit={handleSubmit} className="space-y-4">
|
||||
<div>
|
||||
<label htmlFor="encounterName" className="block text-sm font-medium text-slate-300">
|
||||
<label htmlFor="encounterName" className="block text-sm font-medium text-stone-300">
|
||||
Encounter Name
|
||||
</label>
|
||||
<input
|
||||
@@ -371,7 +371,7 @@ function CreateEncounterForm({ onCreate, onCancel }) {
|
||||
id="encounterName"
|
||||
value={name}
|
||||
onChange={(e) => setName(e.target.value)}
|
||||
className="mt-1 block w-full px-3 py-2 bg-slate-700 border border-slate-600 rounded-md shadow-sm focus:outline-none focus:ring-amber-600 focus:border-amber-600 sm:text-sm text-white"
|
||||
className="mt-1 block w-full px-3 py-2 bg-stone-800 border border-stone-700 rounded-md shadow-sm focus:outline-none focus:ring-amber-600 focus:border-amber-600 sm:text-sm text-white"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
@@ -379,13 +379,13 @@ function CreateEncounterForm({ onCreate, onCancel }) {
|
||||
<button
|
||||
type="button"
|
||||
onClick={onCancel}
|
||||
className="px-4 py-2 text-sm font-medium text-slate-300 bg-slate-600 hover:bg-slate-500 rounded-md transition-colors"
|
||||
className="px-4 py-2 text-sm font-medium text-stone-300 bg-stone-700 hover:bg-stone-600 rounded-md transition-colors"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
type="submit"
|
||||
className="px-4 py-2 text-sm font-medium text-white bg-orange-500 hover:bg-orange-600 rounded-md transition-colors"
|
||||
className="px-4 py-2 text-sm font-medium text-white bg-amber-700 hover:bg-amber-800 rounded-md transition-colors"
|
||||
>
|
||||
Create
|
||||
</button>
|
||||
@@ -416,40 +416,40 @@ function EditParticipantModal({ participant, onClose, onSave }) {
|
||||
<Modal onClose={onClose} title={`Edit ${participant.name}`}>
|
||||
<form onSubmit={handleSubmit} className="space-y-4">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-slate-300">Name</label>
|
||||
<label className="block text-sm font-medium text-stone-300">Name</label>
|
||||
<input
|
||||
type="text"
|
||||
value={name}
|
||||
onChange={(e) => setName(e.target.value)}
|
||||
className="mt-1 block w-full px-3 py-2 bg-slate-700 border border-slate-600 rounded-md shadow-sm focus:outline-none focus:ring-amber-600 focus:border-amber-600 sm:text-sm text-white"
|
||||
className="mt-1 block w-full px-3 py-2 bg-stone-800 border border-stone-700 rounded-md shadow-sm focus:outline-none focus:ring-amber-600 focus:border-amber-600 sm:text-sm text-white"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-slate-300">Initiative</label>
|
||||
<label className="block text-sm font-medium text-stone-300">Initiative</label>
|
||||
<input
|
||||
type="number"
|
||||
value={initiative}
|
||||
onChange={(e) => setInitiative(e.target.value)}
|
||||
className="mt-1 block w-full px-3 py-2 bg-slate-700 border border-slate-600 rounded-md shadow-sm focus:outline-none focus:ring-amber-600 focus:border-amber-600 sm:text-sm text-white"
|
||||
className="mt-1 block w-full px-3 py-2 bg-stone-800 border border-stone-700 rounded-md shadow-sm focus:outline-none focus:ring-amber-600 focus:border-amber-600 sm:text-sm text-white"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex gap-4">
|
||||
<div className="flex-1">
|
||||
<label className="block text-sm font-medium text-slate-300">Current HP</label>
|
||||
<label className="block text-sm font-medium text-stone-300">Current HP</label>
|
||||
<input
|
||||
type="number"
|
||||
value={currentHp}
|
||||
onChange={(e) => setCurrentHp(e.target.value)}
|
||||
className="mt-1 block w-full px-3 py-2 bg-slate-700 border border-slate-600 rounded-md shadow-sm focus:outline-none focus:ring-amber-600 focus:border-amber-600 sm:text-sm text-white"
|
||||
className="mt-1 block w-full px-3 py-2 bg-stone-800 border border-stone-700 rounded-md shadow-sm focus:outline-none focus:ring-amber-600 focus:border-amber-600 sm:text-sm text-white"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<label className="block text-sm font-medium text-slate-300">Max HP</label>
|
||||
<label className="block text-sm font-medium text-stone-300">Max HP</label>
|
||||
<input
|
||||
type="number"
|
||||
value={maxHp}
|
||||
onChange={(e) => setMaxHp(e.target.value)}
|
||||
className="mt-1 block w-full px-3 py-2 bg-slate-700 border border-slate-600 rounded-md shadow-sm focus:outline-none focus:ring-amber-600 focus:border-amber-600 sm:text-sm text-white"
|
||||
className="mt-1 block w-full px-3 py-2 bg-stone-800 border border-stone-700 rounded-md shadow-sm focus:outline-none focus:ring-amber-600 focus:border-amber-600 sm:text-sm text-white"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@@ -460,9 +460,9 @@ function EditParticipantModal({ participant, onClose, onSave }) {
|
||||
id="editIsNpc"
|
||||
checked={isNpc}
|
||||
onChange={(e) => setIsNpc(e.target.checked)}
|
||||
className="h-4 w-4 text-indigo-600 border-gray-300 rounded focus:ring-indigo-500"
|
||||
className="h-4 w-4 text-violet-600 border-stone-400 rounded focus:ring-violet-500"
|
||||
/>
|
||||
<label htmlFor="editIsNpc" className="ml-2 block text-sm text-slate-300">
|
||||
<label htmlFor="editIsNpc" className="ml-2 block text-sm text-stone-300">
|
||||
Is NPC?
|
||||
</label>
|
||||
</div>
|
||||
@@ -471,7 +471,7 @@ function EditParticipantModal({ participant, onClose, onSave }) {
|
||||
<button
|
||||
type="button"
|
||||
onClick={onClose}
|
||||
className="px-4 py-2 text-sm font-medium text-slate-300 bg-slate-600 hover:bg-slate-500 rounded-md transition-colors"
|
||||
className="px-4 py-2 text-sm font-medium text-stone-300 bg-stone-700 hover:bg-stone-600 rounded-md transition-colors"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
@@ -590,14 +590,14 @@ function CharacterManager({ campaignId, campaignCharacters }) {
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="p-4 bg-slate-800 rounded-lg shadow">
|
||||
<div className="p-4 bg-stone-900 rounded-lg shadow">
|
||||
<div className="flex justify-between items-center mb-3">
|
||||
<h3 className="text-xl font-semibold text-amber-300 font-cinzel tracking-wide flex items-center">
|
||||
<Users size={24} className="mr-2" /> Campaign Characters
|
||||
</h3>
|
||||
<button
|
||||
onClick={() => setIsOpen(!isOpen)}
|
||||
className="p-1 text-slate-400 hover:text-slate-200"
|
||||
className="p-1 text-stone-400 hover:text-stone-200"
|
||||
aria-label={isOpen ? "Collapse" : "Expand"}
|
||||
>
|
||||
{isOpen ? <ChevronUp size={20} /> : <ChevronDown size={20} />}
|
||||
@@ -608,7 +608,7 @@ function CharacterManager({ campaignId, campaignCharacters }) {
|
||||
<>
|
||||
<form onSubmit={(e) => { e.preventDefault(); handleAddCharacter(); }} className="grid grid-cols-1 sm:grid-cols-3 gap-2 mb-4 items-end">
|
||||
<div className="sm:col-span-1">
|
||||
<label htmlFor="characterName" className="block text-xs font-medium text-slate-400">
|
||||
<label htmlFor="characterName" className="block text-xs font-medium text-stone-400">
|
||||
Name
|
||||
</label>
|
||||
<input
|
||||
@@ -617,11 +617,11 @@ function CharacterManager({ campaignId, campaignCharacters }) {
|
||||
value={characterName}
|
||||
onChange={(e) => setCharacterName(e.target.value)}
|
||||
placeholder="Character name"
|
||||
className="w-full px-3 py-2 bg-slate-700 border border-slate-600 rounded-md shadow-sm focus:outline-none focus:ring-amber-600 focus:border-amber-600 sm:text-sm text-white"
|
||||
className="w-full px-3 py-2 bg-stone-800 border border-stone-700 rounded-md shadow-sm focus:outline-none focus:ring-amber-600 focus:border-amber-600 sm:text-sm text-white"
|
||||
/>
|
||||
</div>
|
||||
<div className="w-full sm:w-auto">
|
||||
<label htmlFor="defaultMaxHp" className="block text-xs font-medium text-slate-400">
|
||||
<label htmlFor="defaultMaxHp" className="block text-xs font-medium text-stone-400">
|
||||
Default HP
|
||||
</label>
|
||||
<input
|
||||
@@ -629,11 +629,11 @@ function CharacterManager({ campaignId, campaignCharacters }) {
|
||||
id="defaultMaxHp"
|
||||
value={defaultMaxHp}
|
||||
onChange={(e) => setDefaultMaxHp(e.target.value)}
|
||||
className="w-full px-3 py-2 bg-slate-700 border border-slate-600 rounded-md shadow-sm focus:outline-none focus:ring-amber-600 focus:border-amber-600 sm:text-sm text-white"
|
||||
className="w-full px-3 py-2 bg-stone-800 border border-stone-700 rounded-md shadow-sm focus:outline-none focus:ring-amber-600 focus:border-amber-600 sm:text-sm text-white"
|
||||
/>
|
||||
</div>
|
||||
<div className="w-full sm:w-auto">
|
||||
<label htmlFor="defaultInitMod" className="block text-xs font-medium text-slate-400">
|
||||
<label htmlFor="defaultInitMod" className="block text-xs font-medium text-stone-400">
|
||||
Init Mod
|
||||
</label>
|
||||
<input
|
||||
@@ -641,7 +641,7 @@ function CharacterManager({ campaignId, campaignCharacters }) {
|
||||
id="defaultInitMod"
|
||||
value={defaultInitMod}
|
||||
onChange={(e) => setDefaultInitMod(e.target.value)}
|
||||
className="w-full px-3 py-2 bg-slate-700 border border-slate-600 rounded-md shadow-sm focus:outline-none focus:ring-amber-600 focus:border-amber-600 sm:text-sm text-white"
|
||||
className="w-full px-3 py-2 bg-stone-800 border border-stone-700 rounded-md shadow-sm focus:outline-none focus:ring-amber-600 focus:border-amber-600 sm:text-sm text-white"
|
||||
/>
|
||||
</div>
|
||||
<button
|
||||
@@ -653,12 +653,12 @@ function CharacterManager({ campaignId, campaignCharacters }) {
|
||||
</form>
|
||||
|
||||
{campaignCharacters.length === 0 && (
|
||||
<p className="text-sm text-slate-400">No characters added yet.</p>
|
||||
<p className="text-sm text-stone-400">No characters added yet.</p>
|
||||
)}
|
||||
|
||||
<ul className="space-y-2">
|
||||
{campaignCharacters.map(character => (
|
||||
<li key={character.id} className="flex justify-between items-center p-3 bg-slate-700 rounded-md">
|
||||
<li key={character.id} className="flex justify-between items-center p-3 bg-stone-800 rounded-md">
|
||||
{editingCharacter && editingCharacter.id === character.id ? (
|
||||
<form
|
||||
onSubmit={(e) => {
|
||||
@@ -676,20 +676,20 @@ function CharacterManager({ campaignId, campaignCharacters }) {
|
||||
type="text"
|
||||
value={editingCharacter.name}
|
||||
onChange={(e) => setEditingCharacter({ ...editingCharacter, name: e.target.value })}
|
||||
className="flex-grow min-w-[100px] px-2 py-1 bg-slate-600 border border-slate-500 rounded-md text-white"
|
||||
className="flex-grow min-w-[100px] px-2 py-1 bg-stone-700 border border-stone-600 rounded-md text-white"
|
||||
/>
|
||||
<input
|
||||
type="number"
|
||||
value={editingCharacter.defaultMaxHp}
|
||||
onChange={(e) => setEditingCharacter({ ...editingCharacter, defaultMaxHp: e.target.value })}
|
||||
className="w-20 px-2 py-1 bg-slate-600 border border-slate-500 rounded-md text-white"
|
||||
className="w-20 px-2 py-1 bg-stone-700 border border-stone-600 rounded-md text-white"
|
||||
title="Default Max HP"
|
||||
/>
|
||||
<input
|
||||
type="number"
|
||||
value={editingCharacter.defaultInitMod}
|
||||
onChange={(e) => setEditingCharacter({ ...editingCharacter, defaultInitMod: e.target.value })}
|
||||
className="w-20 px-2 py-1 bg-slate-600 border border-slate-500 rounded-md text-white"
|
||||
className="w-20 px-2 py-1 bg-stone-700 border border-stone-600 rounded-md text-white"
|
||||
title="Default Init Mod"
|
||||
/>
|
||||
<button type="submit" className="p-1 text-green-400 hover:text-green-300">
|
||||
@@ -698,16 +698,16 @@ function CharacterManager({ campaignId, campaignCharacters }) {
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setEditingCharacter(null)}
|
||||
className="p-1 text-slate-400 hover:text-slate-200"
|
||||
className="p-1 text-stone-400 hover:text-stone-200"
|
||||
>
|
||||
<XCircle size={18} />
|
||||
</button>
|
||||
</form>
|
||||
) : (
|
||||
<>
|
||||
<span className="text-slate-100">
|
||||
<span className="text-stone-100">
|
||||
{character.name}{' '}
|
||||
<span className="text-xs text-slate-400">
|
||||
<span className="text-xs text-stone-400">
|
||||
(HP: {character.defaultMaxHp || 'N/A'}, Init Mod: {formatInitMod(character.defaultInitMod)})
|
||||
</span>
|
||||
</span>
|
||||
@@ -719,14 +719,14 @@ function CharacterManager({ campaignId, campaignCharacters }) {
|
||||
defaultMaxHp: character.defaultMaxHp || DEFAULT_MAX_HP,
|
||||
defaultInitMod: character.defaultInitMod || DEFAULT_INIT_MOD
|
||||
})}
|
||||
className="p-1 rounded transition-colors text-yellow-400 hover:text-yellow-300 bg-slate-600 hover:bg-slate-500"
|
||||
className="p-1 rounded transition-colors text-yellow-400 hover:text-yellow-300 bg-stone-700 hover:bg-stone-600"
|
||||
aria-label="Edit character"
|
||||
>
|
||||
<Edit3 size={18} />
|
||||
</button>
|
||||
<button
|
||||
onClick={() => requestDeleteCharacter(character.id, character.name)}
|
||||
className="p-1 rounded transition-colors text-red-400 hover:text-red-300 bg-slate-600 hover:bg-slate-500"
|
||||
className="p-1 rounded transition-colors text-red-400 hover:text-red-300 bg-stone-700 hover:bg-stone-600"
|
||||
aria-label="Delete character"
|
||||
>
|
||||
<Trash2 size={18} />
|
||||
@@ -1118,12 +1118,12 @@ function ParticipantManager({ encounter, encounterPath, campaignCharacters }) {
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="p-3 bg-slate-800 rounded-md mt-4">
|
||||
<div className="p-3 bg-stone-900 rounded-md mt-4">
|
||||
<div className="flex justify-between items-center mb-3">
|
||||
<h4 className="text-lg font-medium text-amber-200 font-cinzel tracking-wide">Add Participants</h4>
|
||||
<button
|
||||
onClick={handleAddAllCampaignCharacters}
|
||||
className="px-3 py-1.5 text-xs font-medium text-white bg-indigo-600 hover:bg-indigo-700 rounded-md transition-colors flex items-center"
|
||||
className="px-3 py-1.5 text-xs font-medium text-white bg-violet-700 hover:bg-violet-800 rounded-md transition-colors flex items-center"
|
||||
disabled={!campaignCharacters || campaignCharacters.length === 0 || (encounter.isStarted && !encounter.isPaused)}
|
||||
>
|
||||
<Users2 size={16} className="mr-1.5" />
|
||||
@@ -1138,7 +1138,7 @@ function ParticipantManager({ encounter, encounterPath, campaignCharacters }) {
|
||||
<div>
|
||||
<p className="text-yellow-300 font-semibold text-sm">Combat is Running</p>
|
||||
<p className="text-yellow-200 text-xs mt-1">
|
||||
Pause combat first to add participants. Participants added during active combat won't be included in the turn order until you pause and resume.
|
||||
Pause combat to add or remove participants. The turn order will be recalculated when combat is resumed.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1149,10 +1149,10 @@ function ParticipantManager({ encounter, encounterPath, campaignCharacters }) {
|
||||
e.preventDefault();
|
||||
handleAddParticipant();
|
||||
}}
|
||||
className="grid grid-cols-1 md:grid-cols-6 gap-x-4 gap-y-2 mb-4 p-3 bg-slate-700 rounded items-end"
|
||||
className="grid grid-cols-1 md:grid-cols-6 gap-x-4 gap-y-2 mb-4 p-3 bg-stone-800 rounded items-end"
|
||||
>
|
||||
<div className="md:col-span-2">
|
||||
<label className="block text-sm font-medium text-slate-300">Type</label>
|
||||
<label className="block text-sm font-medium text-stone-300">Type</label>
|
||||
<select
|
||||
value={participantType}
|
||||
onChange={(e) => {
|
||||
@@ -1160,7 +1160,7 @@ function ParticipantManager({ encounter, encounterPath, campaignCharacters }) {
|
||||
setSelectedCharacterId('');
|
||||
setIsNpc(false);
|
||||
}}
|
||||
className="mt-1 w-full px-3 py-2 bg-slate-600 border-slate-500 rounded text-white"
|
||||
className="mt-1 w-full px-3 py-2 bg-stone-700 border-stone-600 rounded text-white"
|
||||
>
|
||||
<option value="monster">Monster</option>
|
||||
<option value="character">Character</option>
|
||||
@@ -1170,7 +1170,7 @@ function ParticipantManager({ encounter, encounterPath, campaignCharacters }) {
|
||||
{participantType === 'monster' ? (
|
||||
<>
|
||||
<div className="md:col-span-4">
|
||||
<label htmlFor="monsterName" className="block text-sm font-medium text-slate-300">
|
||||
<label htmlFor="monsterName" className="block text-sm font-medium text-stone-300">
|
||||
Monster Name
|
||||
</label>
|
||||
<input
|
||||
@@ -1179,11 +1179,11 @@ function ParticipantManager({ encounter, encounterPath, campaignCharacters }) {
|
||||
value={participantName}
|
||||
onChange={(e) => setParticipantName(e.target.value)}
|
||||
placeholder="e.g., Dire Wolf"
|
||||
className="mt-1 w-full px-3 py-2 bg-slate-700 border border-slate-600 rounded-md shadow-sm focus:outline-none focus:ring-amber-600 focus:border-amber-600 sm:text-sm text-white"
|
||||
className="mt-1 w-full px-3 py-2 bg-stone-800 border border-stone-700 rounded-md shadow-sm focus:outline-none focus:ring-amber-600 focus:border-amber-600 sm:text-sm text-white"
|
||||
/>
|
||||
</div>
|
||||
<div className="md:col-span-2">
|
||||
<label htmlFor="monsterInitMod" className="block text-sm font-medium text-slate-300">
|
||||
<label htmlFor="monsterInitMod" className="block text-sm font-medium text-stone-300">
|
||||
Init Mod
|
||||
</label>
|
||||
<input
|
||||
@@ -1191,11 +1191,11 @@ function ParticipantManager({ encounter, encounterPath, campaignCharacters }) {
|
||||
id="monsterInitMod"
|
||||
value={monsterInitMod}
|
||||
onChange={(e) => setMonsterInitMod(e.target.value)}
|
||||
className="mt-1 w-full px-3 py-2 bg-slate-700 border border-slate-600 rounded-md shadow-sm focus:outline-none focus:ring-amber-600 focus:border-amber-600 sm:text-sm text-white"
|
||||
className="mt-1 w-full px-3 py-2 bg-stone-800 border border-stone-700 rounded-md shadow-sm focus:outline-none focus:ring-amber-600 focus:border-amber-600 sm:text-sm text-white"
|
||||
/>
|
||||
</div>
|
||||
<div className="md:col-span-2">
|
||||
<label htmlFor="monsterMaxHp" className="block text-sm font-medium text-slate-300">
|
||||
<label htmlFor="monsterMaxHp" className="block text-sm font-medium text-stone-300">
|
||||
Max HP
|
||||
</label>
|
||||
<input
|
||||
@@ -1203,7 +1203,7 @@ function ParticipantManager({ encounter, encounterPath, campaignCharacters }) {
|
||||
id="monsterMaxHp"
|
||||
value={maxHp}
|
||||
onChange={(e) => setMaxHp(e.target.value)}
|
||||
className="mt-1 w-full px-3 py-2 bg-slate-700 border border-slate-600 rounded-md shadow-sm focus:outline-none focus:ring-amber-600 focus:border-amber-600 sm:text-sm text-white"
|
||||
className="mt-1 w-full px-3 py-2 bg-stone-800 border border-stone-700 rounded-md shadow-sm focus:outline-none focus:ring-amber-600 focus:border-amber-600 sm:text-sm text-white"
|
||||
/>
|
||||
</div>
|
||||
<div className="md:col-span-2 flex items-center pt-5">
|
||||
@@ -1212,9 +1212,9 @@ function ParticipantManager({ encounter, encounterPath, campaignCharacters }) {
|
||||
id="isNpc"
|
||||
checked={isNpc}
|
||||
onChange={(e) => setIsNpc(e.target.checked)}
|
||||
className="h-4 w-4 text-indigo-600 border-gray-300 rounded focus:ring-indigo-500"
|
||||
className="h-4 w-4 text-violet-600 border-stone-400 rounded focus:ring-violet-500"
|
||||
/>
|
||||
<label htmlFor="isNpc" className="ml-2 block text-sm text-slate-300">
|
||||
<label htmlFor="isNpc" className="ml-2 block text-sm text-stone-300">
|
||||
Is NPC?
|
||||
</label>
|
||||
</div>
|
||||
@@ -1222,11 +1222,11 @@ function ParticipantManager({ encounter, encounterPath, campaignCharacters }) {
|
||||
) : (
|
||||
<>
|
||||
<div className="md:col-span-4">
|
||||
<label className="block text-sm font-medium text-slate-300">Select Character</label>
|
||||
<label className="block text-sm font-medium text-stone-300">Select Character</label>
|
||||
<select
|
||||
value={selectedCharacterId}
|
||||
onChange={(e) => setSelectedCharacterId(e.target.value)}
|
||||
className="mt-1 w-full px-3 py-2 bg-slate-600 border-slate-500 rounded text-white"
|
||||
className="mt-1 w-full px-3 py-2 bg-stone-700 border-stone-600 rounded text-white"
|
||||
>
|
||||
<option value="">-- Select from Campaign --</option>
|
||||
{campaignCharacters.map(c => (
|
||||
@@ -1237,12 +1237,12 @@ function ParticipantManager({ encounter, encounterPath, campaignCharacters }) {
|
||||
</select>
|
||||
</div>
|
||||
<div className="md:col-span-2">
|
||||
<label className="block text-sm font-medium text-slate-300">Max HP (Encounter)</label>
|
||||
<label className="block text-sm font-medium text-stone-300">Max HP (Encounter)</label>
|
||||
<input
|
||||
type="number"
|
||||
value={maxHp}
|
||||
onChange={(e) => setMaxHp(e.target.value)}
|
||||
className="mt-1 w-full px-3 py-2 bg-slate-700 border border-slate-600 rounded-md shadow-sm focus:outline-none focus:ring-amber-600 focus:border-amber-600 sm:text-sm text-white"
|
||||
className="mt-1 w-full px-3 py-2 bg-stone-800 border border-stone-700 rounded-md shadow-sm focus:outline-none focus:ring-amber-600 focus:border-amber-600 sm:text-sm text-white"
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
@@ -1252,7 +1252,7 @@ function ParticipantManager({ encounter, encounterPath, campaignCharacters }) {
|
||||
<button
|
||||
type="submit"
|
||||
disabled={encounter.isStarted && !encounter.isPaused}
|
||||
className={`px-4 py-2 text-sm font-medium text-white rounded-md transition-colors flex items-center ${encounter.isStarted && !encounter.isPaused ? 'bg-slate-500 cursor-not-allowed opacity-50' : 'bg-green-500 hover:bg-green-600'}`}
|
||||
className={`px-4 py-2 text-sm font-medium text-white rounded-md transition-colors flex items-center ${encounter.isStarted && !encounter.isPaused ? 'bg-stone-600 cursor-not-allowed opacity-50' : 'bg-red-700 hover:bg-red-800'}`}
|
||||
title={encounter.isStarted && !encounter.isPaused ? 'Pause combat to add participants' : 'Add participant and roll initiative'}
|
||||
>
|
||||
<Dices size={18} className="mr-1.5" /> Add to Encounter (Roll Init)
|
||||
@@ -1262,12 +1262,12 @@ function ParticipantManager({ encounter, encounterPath, campaignCharacters }) {
|
||||
|
||||
{lastRollDetails && (
|
||||
<p className="text-sm text-green-400 mt-2 mb-2 text-center">
|
||||
{lastRollDetails.name} ({lastRollDetails.type === 'character' ? 'Character' : lastRollDetails.type})
|
||||
: Rolled D20 ({lastRollDetails.roll}) {formatInitMod(lastRollDetails.mod)} = {lastRollDetails.total} Initiative
|
||||
{lastRollDetails.name} ({lastRollDetails.type === 'character' ? 'Character' : lastRollDetails.type === 'monster' ? 'Monster' : lastRollDetails.type})
|
||||
: Rolled d20 ({lastRollDetails.roll}) {formatInitMod(lastRollDetails.mod)} = {lastRollDetails.total} Initiative
|
||||
</p>
|
||||
)}
|
||||
|
||||
{participants.length === 0 && <p className="text-sm text-slate-400">No participants added yet.</p>}
|
||||
{participants.length === 0 && <p className="text-sm text-stone-400">No participants added yet.</p>}
|
||||
|
||||
<ul className="space-y-2">
|
||||
{sortedParticipants.map((p) => {
|
||||
@@ -1275,7 +1275,7 @@ function ParticipantManager({ encounter, encounterPath, campaignCharacters }) {
|
||||
const isDraggable = (!encounter.isStarted || encounter.isPaused) && tiedInitiatives.includes(Number(p.initiative));
|
||||
const participantDisplayType = p.type === 'monster' ? (p.isNpc ? 'NPC' : 'Monster') : 'Character';
|
||||
|
||||
let bgColor = p.type === 'character' ? 'bg-blue-950' : (p.isNpc ? 'bg-slate-600' : 'bg-[#8e351c]');
|
||||
let bgColor = p.type === 'character' ? 'bg-indigo-950' : (p.isNpc ? 'bg-stone-700' : 'bg-[#8e351c]');
|
||||
if (isCurrentTurn && !encounter.isPaused) bgColor = 'bg-green-600';
|
||||
|
||||
const isDead = p.currentHp === 0;
|
||||
@@ -1294,7 +1294,7 @@ function ParticipantManager({ encounter, encounterPath, campaignCharacters }) {
|
||||
{isDraggable && (
|
||||
<ChevronsUpDown
|
||||
size={18}
|
||||
className="mr-2 text-slate-400 flex-shrink-0"
|
||||
className="mr-2 text-stone-400 flex-shrink-0"
|
||||
title="Drag to reorder in tie"
|
||||
/>
|
||||
)}
|
||||
@@ -1307,21 +1307,21 @@ function ParticipantManager({ encounter, encounterPath, campaignCharacters }) {
|
||||
<Zap size={12} className="mr-1" /> CURRENT
|
||||
</span>
|
||||
)}
|
||||
{isDead && <span className="ml-2 text-xs text-red-300 font-semibold">(Unconscious)</span>}
|
||||
{isDead && <span className="ml-2 text-xs text-red-300 font-semibold">{p.type === 'character' ? '(Unconscious)' : '(Dead)'}</span>}
|
||||
</p>
|
||||
<p className={`text-sm ${isCurrentTurn && !encounter.isPaused ? 'text-green-100' : 'text-slate-200'}`}>
|
||||
<p className={`text-sm ${isCurrentTurn && !encounter.isPaused ? 'text-green-100' : 'text-stone-200'}`}>
|
||||
Init: {p.initiative} | HP: {p.currentHp}/{p.maxHp}
|
||||
</p>
|
||||
|
||||
{/* Death Saves */}
|
||||
{isDead && encounter.isStarted && (
|
||||
{/* Death Saves - only player characters make death saving throws */}
|
||||
{isDead && encounter.isStarted && p.type === 'character' && (
|
||||
<div className="mt-2 flex items-center space-x-2">
|
||||
<span className="text-xs text-red-300 font-medium">Death Saves:</span>
|
||||
{[1, 2, 3].map(saveNum => (
|
||||
<button
|
||||
key={saveNum}
|
||||
onClick={() => handleDeathSaveChange(p.id, saveNum)}
|
||||
className={`w-6 h-6 rounded border-2 transition-all ${(p.deathSaves || 0) >= saveNum ? 'bg-red-600 border-red-500' : 'bg-slate-700 border-slate-500 hover:border-red-400'} ${saveNum === 3 && (p.deathSaves || 0) === 3 ? 'animate-pulse' : ''}`}
|
||||
className={`w-6 h-6 rounded border-2 transition-all ${(p.deathSaves || 0) >= saveNum ? 'bg-red-600 border-red-500' : 'bg-stone-800 border-stone-600 hover:border-red-400'} ${saveNum === 3 && (p.deathSaves || 0) === 3 ? 'animate-pulse' : ''}`}
|
||||
title={`Death save ${saveNum}`}
|
||||
>
|
||||
{(p.deathSaves || 0) >= saveNum && <span className="text-white text-sm">✕</span>}
|
||||
@@ -1334,13 +1334,13 @@ function ParticipantManager({ encounter, encounterPath, campaignCharacters }) {
|
||||
|
||||
<div className="flex flex-wrap items-center space-x-2 mt-2 sm:mt-0">
|
||||
{encounter.isStarted && (
|
||||
<div className="flex items-center space-x-1 bg-slate-700 p-1 rounded-md">
|
||||
<div className="flex items-center space-x-1 bg-stone-800 p-1 rounded-md">
|
||||
<input
|
||||
type="number"
|
||||
placeholder="HP"
|
||||
value={hpChangeValues[p.id] || ''}
|
||||
onChange={(e) => setHpChangeValues(prev => ({ ...prev, [p.id]: e.target.value }))}
|
||||
className="w-16 p-1 text-sm bg-slate-600 border border-slate-500 rounded-md text-white focus:ring-amber-600 focus:border-amber-600"
|
||||
className="w-16 p-1 text-sm bg-stone-700 border border-stone-600 rounded-md text-white focus:ring-amber-600 focus:border-amber-600"
|
||||
aria-label={`HP change for ${p.name}`}
|
||||
/>
|
||||
{!isDead && (
|
||||
@@ -1354,7 +1354,7 @@ function ParticipantManager({ encounter, encounterPath, campaignCharacters }) {
|
||||
)}
|
||||
<button
|
||||
onClick={() => applyHpChange(p.id, 'heal')}
|
||||
className="p-1 bg-green-500 hover:bg-green-600 text-white rounded-md text-xs"
|
||||
className="p-1 bg-emerald-600 hover:bg-emerald-700 text-white rounded-md text-xs"
|
||||
title={isDead ? "Heal / Revive" : "Heal"}
|
||||
>
|
||||
<HeartPulse size={16} />
|
||||
@@ -1364,7 +1364,7 @@ function ParticipantManager({ encounter, encounterPath, campaignCharacters }) {
|
||||
{!isDead && (
|
||||
<button
|
||||
onClick={() => toggleParticipantActive(p.id)}
|
||||
className={`p-1 rounded transition-colors ${p.isActive ? 'text-yellow-400 hover:text-yellow-300' : 'text-slate-400 hover:text-slate-300'} bg-slate-600 hover:bg-slate-500`}
|
||||
className={`p-1 rounded transition-colors ${p.isActive ? 'text-yellow-400 hover:text-yellow-300' : 'text-stone-400 hover:text-stone-300'} bg-stone-700 hover:bg-stone-600`}
|
||||
title={p.isActive ? "Mark Inactive" : "Mark Active"}
|
||||
>
|
||||
{p.isActive ? <UserCheck size={18} /> : <UserX size={18} />}
|
||||
@@ -1372,14 +1372,14 @@ function ParticipantManager({ encounter, encounterPath, campaignCharacters }) {
|
||||
)}
|
||||
<button
|
||||
onClick={() => setEditingParticipant(p)}
|
||||
className="p-1 rounded transition-colors text-yellow-400 hover:text-yellow-300 bg-slate-600 hover:bg-slate-500"
|
||||
className="p-1 rounded transition-colors text-yellow-400 hover:text-yellow-300 bg-stone-700 hover:bg-stone-600"
|
||||
title="Edit"
|
||||
>
|
||||
<Edit3 size={18} />
|
||||
</button>
|
||||
<button
|
||||
onClick={() => requestDeleteParticipant(p.id, p.name)}
|
||||
className="p-1 rounded transition-colors text-red-400 hover:text-red-300 bg-slate-600 hover:bg-slate-500"
|
||||
className="p-1 rounded transition-colors text-red-400 hover:text-red-300 bg-stone-700 hover:bg-stone-600"
|
||||
title="Remove"
|
||||
>
|
||||
<Trash2 size={18} />
|
||||
@@ -1540,13 +1540,13 @@ function InitiativeControls({ campaignId, encounter, encounterPath }) {
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="lg:sticky lg:top-4 p-4 bg-slate-800 rounded-md shadow-lg">
|
||||
<div className="lg:sticky lg:top-4 p-4 bg-stone-900 rounded-md shadow-lg">
|
||||
<h4 className="text-lg font-medium text-amber-200 mb-4 text-center font-cinzel tracking-wide">Combat Controls</h4>
|
||||
<div className="flex flex-col gap-3">
|
||||
{!encounter.isStarted ? (
|
||||
<button
|
||||
onClick={handleStartEncounter}
|
||||
className="w-full px-4 py-3 text-sm font-medium text-white bg-green-600 hover:bg-green-700 rounded-md transition-colors flex items-center justify-center"
|
||||
className="w-full px-4 py-3 text-sm font-medium text-white bg-red-700 hover:bg-red-800 rounded-md transition-colors flex items-center justify-center"
|
||||
disabled={!encounter.participants || encounter.participants.filter(p => p.isActive).length === 0}
|
||||
>
|
||||
<PlayIcon size={18} className="mr-2" /> Start Combat
|
||||
@@ -1555,14 +1555,17 @@ function InitiativeControls({ campaignId, encounter, encounterPath }) {
|
||||
<>
|
||||
<button
|
||||
onClick={handleTogglePause}
|
||||
className={`w-full px-4 py-3 text-sm font-medium text-white rounded-md transition-colors flex items-center justify-center ${encounter.isPaused ? 'bg-green-500 hover:bg-green-600' : 'bg-yellow-500 hover:bg-yellow-600'}`}
|
||||
className={`w-full px-4 py-3 text-sm font-medium text-white rounded-md transition-colors flex items-center justify-center ${encounter.isPaused ? 'bg-red-700 hover:bg-red-800' : 'bg-amber-600 hover:bg-amber-700'}`}
|
||||
title={encounter.isPaused
|
||||
? 'Resume combat from the current turn. The initiative order will be recalculated to include any participants added while paused.'
|
||||
: 'Pause combat to freeze the turn order. While paused, you can add or remove participants, adjust HP, and edit initiative values. The turn order will be recalculated when you resume.'}
|
||||
>
|
||||
{encounter.isPaused ? <PlayIcon size={18} className="mr-2" /> : <PauseIcon size={18} className="mr-2" />}
|
||||
{encounter.isPaused ? 'Resume' : 'Pause'}
|
||||
{encounter.isPaused ? 'Resume Combat' : 'Pause Combat'}
|
||||
</button>
|
||||
<button
|
||||
onClick={handleNextTurn}
|
||||
className="w-full px-4 py-3 text-sm font-medium text-white bg-blue-600 hover:bg-blue-700 rounded-md transition-colors flex items-center justify-center"
|
||||
className="w-full px-4 py-3 text-sm font-medium text-white bg-purple-700 hover:bg-purple-800 rounded-md transition-colors flex items-center justify-center"
|
||||
disabled={!encounter.currentTurnParticipantId || encounter.isPaused}
|
||||
>
|
||||
<SkipForwardIcon size={18} className="mr-2" /> Next Turn
|
||||
@@ -1575,7 +1578,7 @@ function InitiativeControls({ campaignId, encounter, encounterPath }) {
|
||||
</button>
|
||||
|
||||
{/* Round Counter */}
|
||||
<div className="mt-2 pt-3 border-t border-slate-600">
|
||||
<div className="mt-2 pt-3 border-t border-stone-700">
|
||||
<p className="text-center text-lg font-semibold text-amber-300 font-cinzel">Round: {encounter.round}</p>
|
||||
{encounter.isPaused && (
|
||||
<p className="text-center text-sm text-yellow-400 font-semibold mt-1">(Paused)</p>
|
||||
@@ -1730,26 +1733,26 @@ function EncounterManager({ campaignId, initialActiveEncounterId, campaignCharac
|
||||
const selectedEncounter = encounters?.find(e => e.id === selectedEncounterId);
|
||||
|
||||
if (isLoadingEncounters && campaignId) {
|
||||
return <p className="text-center text-slate-300 mt-4">Loading encounters...</p>;
|
||||
return <p className="text-center text-stone-300 mt-4">Loading encounters...</p>;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="mt-6 p-4 bg-slate-800 rounded-lg shadow">
|
||||
<div className="mt-6 p-4 bg-stone-900 rounded-lg shadow">
|
||||
<div className="flex justify-between items-center mb-3">
|
||||
<h3 className="text-xl font-semibold text-amber-300 font-cinzel tracking-wide flex items-center">
|
||||
<Swords size={24} className="mr-2" /> Encounters
|
||||
</h3>
|
||||
<button
|
||||
onClick={() => setShowCreateModal(true)}
|
||||
className="px-4 py-2 text-sm font-medium text-white bg-orange-500 hover:bg-orange-600 rounded-md transition-colors flex items-center"
|
||||
className="px-4 py-2 text-sm font-medium text-white bg-amber-700 hover:bg-amber-800 rounded-md transition-colors flex items-center"
|
||||
>
|
||||
<PlusCircle size={18} className="mr-1" /> Create Encounter
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{(!encounters || encounters.length === 0) && (
|
||||
<p className="text-sm text-slate-400">No encounters yet.</p>
|
||||
<p className="text-sm text-stone-400">No encounters yet.</p>
|
||||
)}
|
||||
|
||||
<div className="space-y-3">
|
||||
@@ -1761,12 +1764,12 @@ function EncounterManager({ campaignId, initialActiveEncounterId, campaignCharac
|
||||
return (
|
||||
<div
|
||||
key={encounter.id}
|
||||
className={`p-3 rounded-md shadow transition-all ${selectedEncounterId === encounter.id ? 'bg-amber-900 ring-2 ring-amber-500' : 'bg-slate-700 hover:bg-slate-600'} ${isLive ? 'ring-2 ring-green-500 shadow-md shadow-green-500/30' : ''}`}
|
||||
className={`p-3 rounded-md shadow transition-all ${selectedEncounterId === encounter.id ? 'bg-amber-900 ring-2 ring-amber-500' : 'bg-stone-800 hover:bg-stone-700'} ${isLive ? 'ring-2 ring-green-500 shadow-md shadow-green-500/30' : ''}`}
|
||||
>
|
||||
<div className="flex justify-between items-center">
|
||||
<div onClick={() => setSelectedEncounterId(encounter.id)} className="cursor-pointer flex-grow">
|
||||
<h4 className="font-medium text-white">{encounter.name}</h4>
|
||||
<p className="text-xs text-slate-300">
|
||||
<p className="text-xs text-stone-300">
|
||||
Participants: {encounter.participants?.length || 0}
|
||||
</p>
|
||||
{isLive && (
|
||||
@@ -1778,7 +1781,7 @@ function EncounterManager({ campaignId, initialActiveEncounterId, campaignCharac
|
||||
<div className="flex items-center space-x-2">
|
||||
<button
|
||||
onClick={() => handleTogglePlayerDisplay(encounter.id)}
|
||||
className={`p-1 rounded transition-colors ${isLive ? 'bg-red-500 hover:bg-red-600 text-white' : 'text-amber-400 hover:text-amber-300 bg-slate-600 hover:bg-slate-500'}`}
|
||||
className={`p-1 rounded transition-colors ${isLive ? 'bg-red-500 hover:bg-red-600 text-white' : 'text-amber-400 hover:text-amber-300 bg-stone-700 hover:bg-stone-600'}`}
|
||||
title={isLive ? "Deactivate for Player Display" : "Activate for Player Display"}
|
||||
>
|
||||
{isLive ? <EyeOff size={18} /> : <Eye size={18} />}
|
||||
@@ -1788,7 +1791,7 @@ function EncounterManager({ campaignId, initialActiveEncounterId, campaignCharac
|
||||
e.stopPropagation();
|
||||
requestDeleteEncounter(encounter.id, encounter.name);
|
||||
}}
|
||||
className="p-1 rounded transition-colors text-red-400 hover:text-red-300 bg-slate-600 hover:bg-slate-500"
|
||||
className="p-1 rounded transition-colors text-red-400 hover:text-red-300 bg-stone-700 hover:bg-stone-600"
|
||||
title="Delete Encounter"
|
||||
>
|
||||
<Trash2 size={18} />
|
||||
@@ -1810,7 +1813,7 @@ function EncounterManager({ campaignId, initialActiveEncounterId, campaignCharac
|
||||
)}
|
||||
|
||||
{selectedEncounter && (
|
||||
<div className="mt-6 p-4 bg-slate-800 rounded-lg shadow-inner">
|
||||
<div className="mt-6 p-4 bg-stone-900 rounded-lg shadow-inner">
|
||||
<h3 className="text-xl font-semibold text-amber-300 mb-3 font-cinzel tracking-wide">
|
||||
Managing Encounter: {selectedEncounter.name}
|
||||
</h3>
|
||||
@@ -1975,7 +1978,7 @@ function AdminView({ userId }) {
|
||||
const selectedCampaign = campaignsWithDetails.find(c => c.id === selectedCampaignId);
|
||||
|
||||
if (isLoadingCampaigns) {
|
||||
return <p className="text-center text-slate-300">Loading campaigns...</p>;
|
||||
return <p className="text-center text-stone-300">Loading campaigns...</p>;
|
||||
}
|
||||
|
||||
if (campaignsError) {
|
||||
@@ -1994,14 +1997,14 @@ function AdminView({ userId }) {
|
||||
<h2 className="text-2xl font-semibold text-amber-300 font-cinzel tracking-wide">Campaigns</h2>
|
||||
<button
|
||||
onClick={() => setShowCreateModal(true)}
|
||||
className="bg-green-500 hover:bg-green-600 text-white font-bold py-2 px-4 rounded-lg flex items-center transition-colors"
|
||||
className="bg-red-700 hover:bg-red-800 text-white font-bold py-2 px-4 rounded-lg flex items-center transition-colors"
|
||||
>
|
||||
<PlusCircle size={20} className="mr-2" /> Create Campaign
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{campaignsWithDetails.length === 0 && !isLoadingCampaigns && (
|
||||
<p className="text-slate-400">No campaigns yet. Create one to get started!</p>
|
||||
<p className="text-stone-400">No campaigns yet. Create one to get started!</p>
|
||||
)}
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
||||
@@ -2010,7 +2013,7 @@ function AdminView({ userId }) {
|
||||
? { backgroundImage: `url(${campaign.playerDisplayBackgroundUrl})` }
|
||||
: {};
|
||||
|
||||
const cardClasses = `h-40 flex flex-col justify-between rounded-lg shadow-md cursor-pointer transition-all relative overflow-hidden bg-cover bg-center ${selectedCampaignId === campaign.id ? 'ring-4 ring-amber-500' : ''} ${!campaign.playerDisplayBackgroundUrl ? 'bg-slate-700 hover:bg-slate-600' : 'hover:shadow-xl'}`;
|
||||
const cardClasses = `h-40 flex flex-col justify-between rounded-lg shadow-md cursor-pointer transition-all relative overflow-hidden bg-cover bg-center ${selectedCampaignId === campaign.id ? 'ring-4 ring-amber-500' : ''} ${!campaign.playerDisplayBackgroundUrl ? 'bg-stone-800 hover:bg-stone-700' : 'hover:shadow-xl'}`;
|
||||
|
||||
return (
|
||||
<div
|
||||
@@ -2024,7 +2027,7 @@ function AdminView({ userId }) {
|
||||
>
|
||||
<div>
|
||||
<h3 className="text-xl font-semibold text-white">{campaign.name}</h3>
|
||||
<div className="text-xs text-slate-100 mt-1 space-x-3">
|
||||
<div className="text-xs text-stone-100 mt-1 space-x-3">
|
||||
<span className="inline-flex items-center">
|
||||
<Users size={12} className="mr-1" /> {campaign.characters?.length || 0} Characters
|
||||
</span>
|
||||
@@ -2059,7 +2062,7 @@ function AdminView({ userId }) {
|
||||
)}
|
||||
|
||||
{selectedCampaign && (
|
||||
<div className="mt-6 p-6 bg-slate-800 rounded-lg shadow-xl">
|
||||
<div className="mt-6 p-6 bg-stone-900 rounded-lg shadow-xl">
|
||||
<h2 className="text-2xl font-semibold text-amber-300 mb-4 font-cinzel tracking-wide">
|
||||
Managing: {selectedCampaign.name}
|
||||
</h2>
|
||||
@@ -2067,7 +2070,7 @@ function AdminView({ userId }) {
|
||||
campaignId={selectedCampaignId}
|
||||
campaignCharacters={selectedCampaign.characters || []}
|
||||
/>
|
||||
<hr className="my-6 border-slate-600" />
|
||||
<hr className="my-6 border-stone-700" />
|
||||
<EncounterManager
|
||||
campaignId={selectedCampaignId}
|
||||
initialActiveEncounterId={
|
||||
@@ -2189,7 +2192,7 @@ function DisplayView() {
|
||||
}, [activeEncounterData?.currentTurnParticipantId, activeEncounterData?.isStarted, activeEncounterData?.isPaused]);
|
||||
|
||||
if (isLoadingActiveDisplay || (isPlayerDisplayActive && isLoadingEncounter)) {
|
||||
return <div className="text-center py-10 text-2xl text-slate-300">Loading Player Display...</div>;
|
||||
return <div className="text-center py-10 text-2xl text-stone-300">Loading Player Display...</div>;
|
||||
}
|
||||
|
||||
if (activeDisplayError || (isPlayerDisplayActive && encounterError)) {
|
||||
@@ -2198,8 +2201,8 @@ function DisplayView() {
|
||||
|
||||
if (!isPlayerDisplayActive || !activeEncounterData) {
|
||||
return (
|
||||
<div className="min-h-screen bg-black text-slate-400 flex flex-col items-center justify-center p-4 text-center">
|
||||
<EyeOff size={64} className="mb-4 text-slate-500" />
|
||||
<div className="min-h-screen bg-black text-stone-400 flex flex-col items-center justify-center p-4 text-center">
|
||||
<EyeOff size={64} className="mb-4 text-stone-500" />
|
||||
<h2 className="text-3xl font-semibold font-cinzel tracking-wide">Game Session Paused</h2>
|
||||
<p className="text-xl mt-2">The Dungeon Master has not activated an encounter for display.</p>
|
||||
</div>
|
||||
@@ -2226,10 +2229,10 @@ function DisplayView() {
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`p-4 md:p-8 rounded-xl shadow-2xl ${!campaignBackgroundUrl ? 'bg-slate-900' : ''}`}
|
||||
className={`p-4 md:p-8 rounded-xl shadow-2xl ${!campaignBackgroundUrl ? 'bg-stone-950' : ''}`}
|
||||
style={displayStyles}
|
||||
>
|
||||
<div className={campaignBackgroundUrl ? 'bg-slate-900 bg-opacity-75 p-4 md:p-6 rounded-lg' : ''}>
|
||||
<div className={campaignBackgroundUrl ? 'bg-stone-950 bg-opacity-75 p-4 md:p-6 rounded-lg' : ''}>
|
||||
<h2 className="text-4xl md:text-5xl font-bold text-center text-amber-400 mb-2 font-cinzel tracking-wide">{name}</h2>
|
||||
|
||||
{isStarted && <p className="text-2xl text-center text-amber-300 mb-1 font-cinzel">Round: {round}</p>}
|
||||
@@ -2239,15 +2242,15 @@ function DisplayView() {
|
||||
)}
|
||||
|
||||
{!isStarted && participants?.length > 0 && (
|
||||
<p className="text-2xl text-center text-slate-400 mb-6">Awaiting Start</p>
|
||||
<p className="text-2xl text-center text-stone-400 mb-6">Awaiting Start</p>
|
||||
)}
|
||||
|
||||
{!isStarted && (!participants || participants.length === 0) && (
|
||||
<p className="text-2xl text-slate-500 mb-6">No participants.</p>
|
||||
<p className="text-2xl text-stone-500 mb-6">No participants.</p>
|
||||
)}
|
||||
|
||||
{participantsToRender.length === 0 && isStarted && (
|
||||
<p className="text-xl text-slate-400">No active participants.</p>
|
||||
<p className="text-xl text-stone-400">No active participants.</p>
|
||||
)}
|
||||
|
||||
<div className="space-y-4 max-w-3xl mx-auto">
|
||||
@@ -2255,8 +2258,8 @@ function DisplayView() {
|
||||
const isDead = p.currentHp === 0;
|
||||
const isDying = p.isDying || false;
|
||||
let participantBgColor = p.type === 'monster'
|
||||
? (p.isNpc ? 'bg-slate-700' : 'bg-[#8e351c]')
|
||||
: 'bg-blue-950';
|
||||
? (p.isNpc ? 'bg-stone-800' : 'bg-[#8e351c]')
|
||||
: 'bg-indigo-950';
|
||||
|
||||
const isCurrentTurn = p.id === currentTurnParticipantId && isStarted && !isPaused;
|
||||
|
||||
@@ -2274,24 +2277,24 @@ function DisplayView() {
|
||||
>
|
||||
<div className="flex justify-between items-center mb-2">
|
||||
<h3
|
||||
className={`text-2xl md:text-3xl font-bold font-cinzel ${isCurrentTurn ? 'text-white' : (p.type === 'character' ? 'text-amber-100' : (p.isNpc ? 'text-slate-100' : 'text-white'))}`}
|
||||
className={`text-2xl md:text-3xl font-bold font-cinzel ${isCurrentTurn ? 'text-white' : (p.type === 'character' ? 'text-amber-100' : (p.isNpc ? 'text-stone-100' : 'text-white'))}`}
|
||||
>
|
||||
{isDead && <span className="mr-2">☠️</span>}
|
||||
{p.name}
|
||||
{isCurrentTurn && (
|
||||
<span className="text-yellow-300 animate-pulse ml-2">(Current)</span>
|
||||
)}
|
||||
{isDead && <span className="text-red-300 text-lg ml-2">(Unconscious)</span>}
|
||||
{isDead && <span className="text-red-300 text-lg ml-2">{p.type === 'character' ? '(Unconscious)' : '(Dead)'}</span>}
|
||||
</h3>
|
||||
<span
|
||||
className={`text-xl md:text-2xl font-semibold ${isCurrentTurn ? 'text-green-200' : 'text-slate-200'}`}
|
||||
className={`text-xl md:text-2xl font-semibold ${isCurrentTurn ? 'text-green-200' : 'text-stone-200'}`}
|
||||
>
|
||||
Init: {p.initiative}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="flex justify-between items-center">
|
||||
<div className="w-full bg-slate-600 rounded-full h-6 md:h-8 relative overflow-hidden border-2 border-slate-500">
|
||||
<div className="w-full bg-stone-700 rounded-full h-6 md:h-8 relative overflow-hidden border-2 border-stone-600">
|
||||
<div
|
||||
className={`h-full rounded-full transition-all ${isDead ? 'bg-red-900' : (p.currentHp <= p.maxHp / 4 ? 'bg-red-500' : (p.currentHp <= p.maxHp / 2 ? 'bg-yellow-500' : 'bg-green-500'))}`}
|
||||
style={{ width: `${Math.max(0, (p.currentHp / p.maxHp) * 100)}%` }}
|
||||
@@ -2309,7 +2312,7 @@ function DisplayView() {
|
||||
)}
|
||||
|
||||
{!p.isActive && !isDead && (
|
||||
<p className="text-center text-lg font-semibold text-slate-300 mt-2">(Inactive)</p>
|
||||
<p className="text-center text-lg font-semibold text-stone-300 mt-2">(Inactive)</p>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
@@ -2393,7 +2396,7 @@ function App() {
|
||||
|
||||
if (isPlayerViewOnlyMode) {
|
||||
return (
|
||||
<div className="min-h-screen bg-slate-900 text-slate-100 font-garamond">
|
||||
<div className="min-h-screen bg-stone-950 text-stone-100 font-garamond">
|
||||
{isAuthReady && <DisplayView />}
|
||||
{!isAuthReady && !error && <p>Authenticating for Player Display...</p>}
|
||||
</div>
|
||||
@@ -2401,8 +2404,8 @@ function App() {
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-slate-900 text-slate-100 font-garamond">
|
||||
<header className="bg-slate-900 p-4 shadow-lg border-b border-amber-900">
|
||||
<div className="min-h-screen bg-stone-950 text-stone-100 font-garamond">
|
||||
<header className="bg-stone-950 p-4 shadow-lg border-b border-amber-900">
|
||||
<div className="container mx-auto flex justify-between items-center">
|
||||
<h1 className="text-3xl font-bold text-amber-400 font-cinzel tracking-wide">TTRPG Initiative Tracker</h1>
|
||||
<button
|
||||
@@ -2419,7 +2422,7 @@ function App() {
|
||||
{!isAuthReady && !error && <p>Authenticating...</p>}
|
||||
</main>
|
||||
|
||||
<footer className="bg-slate-900 p-4 text-center text-sm text-slate-400 mt-8">
|
||||
<footer className="bg-stone-950 p-4 text-center text-sm text-stone-400 mt-8">
|
||||
TTRPG Initiative Tracker {APP_VERSION}
|
||||
</footer>
|
||||
</div>
|
||||
|
||||
+1
-1
@@ -7,7 +7,7 @@
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
font-family: 'EB Garamond', Georgia, 'Times New Roman', serif;
|
||||
font-family: 'Alegreya Sans', system-ui, sans-serif;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
|
||||
+1
-1
@@ -9,7 +9,7 @@ module.exports = {
|
||||
extend: {
|
||||
fontFamily: {
|
||||
cinzel: ['Cinzel', 'serif'],
|
||||
garamond: ['"EB Garamond"', 'Georgia', 'serif'],
|
||||
garamond: ['"Alegreya Sans"', 'system-ui', 'sans-serif'],
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user