pretty much ready to 'ship'
This commit is contained in:
@@ -3,6 +3,7 @@ import { useAuth } from '../context/AuthContext';
|
||||
import { useToast } from '../components/Toast';
|
||||
import api from '../api/axios';
|
||||
import { formatLocalDateTime, formatLocalDate, formatLocalTime } from '../utils/dateUtils';
|
||||
import PopularityBadge from '../components/PopularityBadge';
|
||||
|
||||
function History() {
|
||||
const { isAuthenticated } = useAuth();
|
||||
@@ -297,12 +298,12 @@ function History() {
|
||||
Games Played ({sessionGames.length})
|
||||
</h3>
|
||||
<div className="space-y-3">
|
||||
{sessionGames.map((game, index) => (
|
||||
{[...sessionGames].reverse().map((game, index) => (
|
||||
<div key={game.id} className="border border-gray-200 dark:border-gray-700 rounded-lg p-4 bg-gray-50 dark:bg-gray-700/50">
|
||||
<div className="flex justify-between items-start mb-2">
|
||||
<div>
|
||||
<div className="font-semibold text-lg text-gray-800 dark:text-gray-100">
|
||||
{index + 1}. {game.title}
|
||||
{sessionGames.length - index}. {game.title}
|
||||
</div>
|
||||
<div className="text-gray-600 dark:text-gray-400">{game.pack_name}</div>
|
||||
</div>
|
||||
@@ -325,11 +326,22 @@ function History() {
|
||||
<div>
|
||||
<span className="font-semibold">Type:</span> {game.game_type || 'N/A'}
|
||||
</div>
|
||||
<div>
|
||||
<span className="font-semibold">Popularity:</span>{' '}
|
||||
<span className={game.popularity_score >= 0 ? 'text-green-600 dark:text-green-400' : 'text-red-600 dark:text-red-400'}>
|
||||
{game.popularity_score > 0 ? '+' : ''}{game.popularity_score}
|
||||
<div className="flex items-center gap-2">
|
||||
<span
|
||||
className="font-semibold"
|
||||
title="Popularity is cumulative across all sessions where this game was played"
|
||||
>
|
||||
Popularity:
|
||||
</span>
|
||||
<PopularityBadge
|
||||
upvotes={game.upvotes || 0}
|
||||
downvotes={game.downvotes || 0}
|
||||
popularityScore={game.popularity_score || 0}
|
||||
size="sm"
|
||||
showCounts={true}
|
||||
showNet={true}
|
||||
showRatio={true}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -467,9 +479,22 @@ function ChatImportPanel({ sessionId, onClose, onImportComplete }) {
|
||||
const [result, setResult] = useState(null);
|
||||
const { error, success } = useToast();
|
||||
|
||||
const handleFileUpload = async (event) => {
|
||||
const file = event.target.files[0];
|
||||
if (!file) return;
|
||||
|
||||
try {
|
||||
const text = await file.text();
|
||||
setChatData(text);
|
||||
success('File loaded successfully');
|
||||
} catch (err) {
|
||||
error('Failed to read file: ' + err.message);
|
||||
}
|
||||
};
|
||||
|
||||
const handleImport = async () => {
|
||||
if (!chatData.trim()) {
|
||||
error('Please enter chat data');
|
||||
error('Please enter chat data or upload a file');
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -498,13 +523,41 @@ function ChatImportPanel({ sessionId, onClose, onImportComplete }) {
|
||||
<h3 className="text-xl font-semibold mb-4 dark:text-gray-100">Import Chat Log</h3>
|
||||
|
||||
<p className="text-sm text-gray-600 dark:text-gray-400 mb-4">
|
||||
Paste JSON array with format: [{"{"}"username": "...", "message": "...", "timestamp": "..."{"}"}]
|
||||
Upload a JSON file or paste JSON array with format: [{"{"}"username": "...", "message": "...", "timestamp": "..."{"}"}]
|
||||
<br />
|
||||
The system will detect "thisgame++" and "thisgame--" patterns and update game popularity.
|
||||
<br />
|
||||
<span className="text-xs italic">
|
||||
Note: Popularity is cumulative - votes are added to each game's all-time totals.
|
||||
</span>
|
||||
</p>
|
||||
|
||||
{/* File Upload */}
|
||||
<div className="mb-4">
|
||||
<label className="block text-gray-700 dark:text-gray-300 font-semibold mb-2">Chat JSON Data</label>
|
||||
<label className="block text-gray-700 dark:text-gray-300 font-semibold mb-2">Upload JSON File</label>
|
||||
<input
|
||||
type="file"
|
||||
accept=".json"
|
||||
onChange={handleFileUpload}
|
||||
disabled={importing}
|
||||
className="block w-full text-sm text-gray-900 dark:text-gray-100
|
||||
file:mr-4 file:py-2 file:px-4
|
||||
file:rounded-lg file:border-0
|
||||
file:text-sm file:font-semibold
|
||||
file:bg-indigo-50 file:text-indigo-700
|
||||
dark:file:bg-indigo-900/30 dark:file:text-indigo-300
|
||||
hover:file:bg-indigo-100 dark:hover:file:bg-indigo-900/50
|
||||
file:cursor-pointer cursor-pointer
|
||||
disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="mb-4 text-center text-gray-500 dark:text-gray-400 text-sm">
|
||||
— or —
|
||||
</div>
|
||||
|
||||
<div className="mb-4">
|
||||
<label className="block text-gray-700 dark:text-gray-300 font-semibold mb-2">Paste Chat JSON Data</label>
|
||||
<textarea
|
||||
value={chatData}
|
||||
onChange={(e) => setChatData(e.target.value)}
|
||||
@@ -519,6 +572,11 @@ function ChatImportPanel({ sessionId, onClose, onImportComplete }) {
|
||||
<p className="font-semibold text-green-800 dark:text-green-200">Import Successful!</p>
|
||||
<p className="text-sm text-green-700 dark:text-green-300">
|
||||
Imported {result.messagesImported} messages, processed {result.votesProcessed} votes
|
||||
{result.duplicatesSkipped > 0 && (
|
||||
<span className="block mt-1 text-xs italic opacity-75">
|
||||
({result.duplicatesSkipped} duplicate{result.duplicatesSkipped !== 1 ? 's' : ''} skipped)
|
||||
</span>
|
||||
)}
|
||||
</p>
|
||||
{result.votesByGame && Object.keys(result.votesByGame).length > 0 && (
|
||||
<div className="mt-2 text-sm text-green-700 dark:text-green-300">
|
||||
@@ -530,8 +588,47 @@ function ChatImportPanel({ sessionId, onClose, onImportComplete }) {
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
<p className="text-xs mt-2 italic opacity-80">
|
||||
Note: Popularity is cumulative across all sessions. If a game is played multiple times, votes apply to the game itself.
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Debug Info */}
|
||||
{result.debug && (
|
||||
<details className="mt-4">
|
||||
<summary className="cursor-pointer font-semibold text-green-800 dark:text-green-200">
|
||||
Debug Info (click to expand)
|
||||
</summary>
|
||||
<div className="mt-2 text-xs text-green-700 dark:text-green-300 space-y-2">
|
||||
{/* Session Timeline */}
|
||||
<div>
|
||||
<p className="font-semibold">Session Timeline:</p>
|
||||
<ul className="list-disc list-inside ml-2">
|
||||
{result.debug.sessionGamesTimeline?.map((game, i) => (
|
||||
<li key={i}>
|
||||
{game.title} - {new Date(game.played_at).toLocaleString()}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
{/* Vote Matches */}
|
||||
{result.debug.voteMatches && result.debug.voteMatches.length > 0 && (
|
||||
<div>
|
||||
<p className="font-semibold">Vote Matches ({result.debug.voteMatches.length}):</p>
|
||||
<ul className="list-disc list-inside ml-2 max-h-48 overflow-y-auto">
|
||||
{result.debug.voteMatches.map((match, i) => (
|
||||
<li key={i}>
|
||||
{match.username}: {match.vote} at {new Date(match.timestamp).toLocaleString()} → matched to "{match.matched_game}" (played at {new Date(match.game_played_at).toLocaleString()})
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</details>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@ import { Link } from 'react-router-dom';
|
||||
import { useAuth } from '../context/AuthContext';
|
||||
import api from '../api/axios';
|
||||
import { formatLocalDateTime, formatLocalTime } from '../utils/dateUtils';
|
||||
import PopularityBadge from '../components/PopularityBadge';
|
||||
|
||||
function Home() {
|
||||
const { isAuthenticated } = useAuth();
|
||||
@@ -130,6 +131,15 @@ function Home() {
|
||||
⏭️ Skipped
|
||||
</span>
|
||||
)}
|
||||
<PopularityBadge
|
||||
upvotes={game.upvotes || 0}
|
||||
downvotes={game.downvotes || 0}
|
||||
popularityScore={game.popularity_score || 0}
|
||||
size="sm"
|
||||
showCounts={true}
|
||||
showNet={true}
|
||||
showRatio={true}
|
||||
/>
|
||||
</div>
|
||||
<div className="text-xs sm:text-sm text-gray-500 dark:text-gray-400 mt-1">
|
||||
({game.pack_name})
|
||||
|
||||
@@ -3,6 +3,7 @@ import { useNavigate } from 'react-router-dom';
|
||||
import { useAuth } from '../context/AuthContext';
|
||||
import { useToast } from '../components/Toast';
|
||||
import api from '../api/axios';
|
||||
import PopularityBadge from '../components/PopularityBadge';
|
||||
|
||||
function Manager() {
|
||||
const { isAuthenticated, loading: authLoading } = useAuth();
|
||||
@@ -427,94 +428,102 @@ function PackTreeNode({ pack, isExpanded, onToggle, onTogglePack, onToggleGame,
|
||||
<div className="border border-gray-200 dark:border-gray-700 rounded-lg overflow-hidden">
|
||||
{/* Pack Header */}
|
||||
<div
|
||||
className={`flex items-center gap-2 p-3 transition ${getPackBackground()}`}
|
||||
className={`p-3 transition ${getPackBackground()}`}
|
||||
>
|
||||
{/* Expand/Collapse Button */}
|
||||
<button
|
||||
onClick={onToggle}
|
||||
className="text-gray-600 dark:text-gray-400 hover:text-gray-800 dark:hover:text-gray-200 w-6 h-6 flex items-center justify-center"
|
||||
>
|
||||
{isExpanded ? '▼' : '▶'}
|
||||
</button>
|
||||
|
||||
{/* Pack Checkbox */}
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={isFullyEnabled}
|
||||
onChange={(e) => {
|
||||
e.stopPropagation();
|
||||
onTogglePack(pack.name, e.target.checked);
|
||||
}}
|
||||
className="w-4 h-4 rounded border-gray-300 dark:border-gray-600 text-indigo-600 focus:ring-indigo-500 dark:bg-gray-700 cursor-pointer"
|
||||
title={`${isFullyEnabled ? 'Disable' : 'Enable'} all games in ${pack.name}`}
|
||||
/>
|
||||
|
||||
{/* Pack Name and Info */}
|
||||
<div
|
||||
className="flex-1 flex items-center justify-between cursor-pointer"
|
||||
onClick={onToggle}
|
||||
>
|
||||
<span className={`font-semibold ${
|
||||
isFullyDisabled
|
||||
? 'text-gray-500 dark:text-gray-500'
|
||||
: 'text-gray-800 dark:text-gray-100'
|
||||
}`}>
|
||||
📦 {pack.name}
|
||||
</span>
|
||||
<span className="text-sm text-gray-500 dark:text-gray-400">
|
||||
{pack.enabled_count} / {pack.total_count} enabled
|
||||
</span>
|
||||
{/* Row 1: Expand/Collapse + Checkbox + Pack Name */}
|
||||
<div className="flex items-center gap-2 mb-2">
|
||||
{/* Expand/Collapse Button */}
|
||||
<button
|
||||
onClick={onToggle}
|
||||
className="text-gray-600 dark:text-gray-400 hover:text-gray-800 dark:hover:text-gray-200 w-6 h-6 flex items-center justify-center flex-shrink-0"
|
||||
>
|
||||
{isExpanded ? '▼' : '▶'}
|
||||
</button>
|
||||
|
||||
{/* Pack Checkbox */}
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={isFullyEnabled}
|
||||
onChange={(e) => {
|
||||
e.stopPropagation();
|
||||
onTogglePack(pack.name, e.target.checked);
|
||||
}}
|
||||
className="w-4 h-4 rounded border-gray-300 dark:border-gray-600 text-indigo-600 focus:ring-indigo-500 dark:bg-gray-700 cursor-pointer flex-shrink-0 mr-2"
|
||||
title={`${isFullyEnabled ? 'Disable' : 'Enable'} all games in ${pack.name}`}
|
||||
/>
|
||||
|
||||
{/* Pack Name */}
|
||||
<div
|
||||
className="flex-1 min-w-0 cursor-pointer"
|
||||
onClick={onToggle}
|
||||
>
|
||||
<div className={`font-semibold text-base md:text-lg truncate ${
|
||||
isFullyDisabled
|
||||
? 'text-gray-500 dark:text-gray-500'
|
||||
: 'text-gray-800 dark:text-gray-100'
|
||||
}`}>
|
||||
📦 {pack.name}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Pack Favor Controls - Only show if pack has enabled games */}
|
||||
{!isFullyDisabled && (
|
||||
<div className="flex items-center gap-1 ml-2" onClick={(e) => e.stopPropagation()}>
|
||||
{pack.favor_bias === 1 && (
|
||||
<span className="text-xs font-semibold bg-green-100 dark:bg-green-900 text-green-800 dark:text-green-200 px-2 py-1 rounded">
|
||||
Favored
|
||||
</span>
|
||||
)}
|
||||
{pack.favor_bias === -1 && (
|
||||
<span className="text-xs font-semibold bg-red-100 dark:bg-red-900 text-red-800 dark:text-red-200 px-2 py-1 rounded">
|
||||
Disfavored
|
||||
</span>
|
||||
)}
|
||||
<button
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
if (pack.favor_bias === 1) {
|
||||
onPackFavorChange(pack.name, 0);
|
||||
} else {
|
||||
onPackFavorChange(pack.name, 1);
|
||||
}
|
||||
}}
|
||||
className={`text-xs px-2 py-1 rounded transition font-medium ${
|
||||
pack.favor_bias === 1
|
||||
? 'bg-gray-200 dark:bg-gray-700 text-gray-700 dark:text-gray-300 hover:bg-gray-300 dark:hover:bg-gray-600'
|
||||
: 'bg-green-100 dark:bg-green-900 text-green-800 dark:text-green-200 hover:bg-green-200 dark:hover:bg-green-800'
|
||||
}`}
|
||||
>
|
||||
{pack.favor_bias === 1 ? 'Unfavor' : 'Favor'}
|
||||
</button>
|
||||
<button
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
if (pack.favor_bias === -1) {
|
||||
onPackFavorChange(pack.name, 0);
|
||||
} else {
|
||||
onPackFavorChange(pack.name, -1);
|
||||
}
|
||||
}}
|
||||
className={`text-xs px-2 py-1 rounded transition font-medium ${
|
||||
pack.favor_bias === -1
|
||||
? 'bg-gray-200 dark:bg-gray-700 text-gray-700 dark:text-gray-300 hover:bg-gray-300 dark:hover:bg-gray-600'
|
||||
: 'bg-red-100 dark:bg-red-900 text-red-800 dark:text-red-200 hover:bg-red-200 dark:hover:bg-red-800'
|
||||
}`}
|
||||
>
|
||||
{pack.favor_bias === -1 ? 'Undisfavor' : 'Disfavor'}
|
||||
</button>
|
||||
{/* Row 2: Metadata + Favor Controls */}
|
||||
<div className="flex items-center justify-between gap-2 ml-12">
|
||||
{/* Pack Info */}
|
||||
<div className="text-xs md:text-sm text-gray-500 dark:text-gray-400">
|
||||
{pack.enabled_count} / {pack.total_count} enabled
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Pack Favor Controls */}
|
||||
{!isFullyDisabled && (
|
||||
<div className="flex items-center gap-1.5 md:gap-2" onClick={(e) => e.stopPropagation()}>
|
||||
{pack.favor_bias === 1 && (
|
||||
<span className="text-xs md:text-sm font-semibold bg-green-100 dark:bg-green-900 text-green-800 dark:text-green-200 px-2 py-0.5 md:py-1 rounded whitespace-nowrap">
|
||||
Favored
|
||||
</span>
|
||||
)}
|
||||
{pack.favor_bias === -1 && (
|
||||
<span className="text-xs md:text-sm font-semibold bg-red-100 dark:bg-red-900 text-red-800 dark:text-red-200 px-2 py-0.5 md:py-1 rounded whitespace-nowrap">
|
||||
Disfavored
|
||||
</span>
|
||||
)}
|
||||
<button
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
if (pack.favor_bias === 1) {
|
||||
onPackFavorChange(pack.name, 0);
|
||||
} else {
|
||||
onPackFavorChange(pack.name, 1);
|
||||
}
|
||||
}}
|
||||
className={`text-xs md:text-sm px-2 md:px-3 py-1 md:py-1.5 rounded transition font-medium whitespace-nowrap ${
|
||||
pack.favor_bias === 1
|
||||
? 'bg-gray-200 dark:bg-gray-700 text-gray-700 dark:text-gray-300 hover:bg-gray-300 dark:hover:bg-gray-600'
|
||||
: 'bg-green-100 dark:bg-green-900 text-green-800 dark:text-green-200 hover:bg-green-200 dark:hover:bg-green-800'
|
||||
}`}
|
||||
>
|
||||
{pack.favor_bias === 1 ? 'Unfavor' : 'Favor'}
|
||||
</button>
|
||||
<button
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
if (pack.favor_bias === -1) {
|
||||
onPackFavorChange(pack.name, 0);
|
||||
} else {
|
||||
onPackFavorChange(pack.name, -1);
|
||||
}
|
||||
}}
|
||||
className={`text-xs md:text-sm px-2 md:px-3 py-1 md:py-1.5 rounded transition font-medium whitespace-nowrap ${
|
||||
pack.favor_bias === -1
|
||||
? 'bg-gray-200 dark:bg-gray-700 text-gray-700 dark:text-gray-300 hover:bg-gray-300 dark:hover:bg-gray-600'
|
||||
: 'bg-red-100 dark:bg-red-900 text-red-800 dark:text-red-200 hover:bg-red-200 dark:hover:bg-red-800'
|
||||
}`}
|
||||
>
|
||||
{pack.favor_bias === -1 ? 'Undisfavor' : 'Disfavor'}
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Games List */}
|
||||
@@ -535,23 +544,24 @@ function PackTreeNode({ pack, isExpanded, onToggle, onTogglePack, onToggleGame,
|
||||
return (
|
||||
<div
|
||||
key={game.id}
|
||||
className={`flex items-center gap-2 p-3 pl-12 border-t border-gray-100 dark:border-gray-700 transition ${getGameBackground()}`}
|
||||
className={`p-3 pl-8 sm:pl-12 border-t border-gray-100 dark:border-gray-700 transition ${getGameBackground()}`}
|
||||
>
|
||||
{/* Game Checkbox */}
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={game.enabled}
|
||||
onChange={(e) => {
|
||||
e.stopPropagation();
|
||||
onToggleGame(game.id);
|
||||
}}
|
||||
className="w-4 h-4 rounded border-gray-300 dark:border-gray-600 text-indigo-600 focus:ring-indigo-500 dark:bg-gray-700 cursor-pointer"
|
||||
/>
|
||||
|
||||
{/* Game Icon and Title */}
|
||||
<div className="flex-1 flex items-center gap-2 min-w-0">
|
||||
<span className="text-lg">🎮</span>
|
||||
<span className={`font-medium truncate ${
|
||||
{/* Row 1: Checkbox + Game Icon + Title */}
|
||||
<div className="flex items-center gap-2 mb-2">
|
||||
{/* Game Checkbox */}
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={game.enabled}
|
||||
onChange={(e) => {
|
||||
e.stopPropagation();
|
||||
onToggleGame(game.id);
|
||||
}}
|
||||
className="w-4 h-4 rounded border-gray-300 dark:border-gray-600 text-indigo-600 focus:ring-indigo-500 dark:bg-gray-700 cursor-pointer flex-shrink-0 mr-3"
|
||||
/>
|
||||
|
||||
{/* Game Icon and Title */}
|
||||
<span className="text-lg md:text-xl flex-shrink-0">🎮</span>
|
||||
<span className={`font-medium text-base md:text-lg truncate flex-1 ${
|
||||
game.enabled
|
||||
? 'text-gray-800 dark:text-gray-200'
|
||||
: 'text-gray-500 dark:text-gray-500 line-through'
|
||||
@@ -560,35 +570,37 @@ function PackTreeNode({ pack, isExpanded, onToggle, onTogglePack, onToggleGame,
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* Game Stats */}
|
||||
<div className="hidden sm:flex items-center gap-4 text-xs text-gray-500 dark:text-gray-400">
|
||||
<span title="Players">{game.min_players}-{game.max_players} 👥</span>
|
||||
<span title="Play count">{game.play_count} ▶️</span>
|
||||
<span
|
||||
title="Popularity"
|
||||
className={game.popularity_score >= 0 ? 'text-green-600 dark:text-green-400' : 'text-red-600 dark:text-red-400'}
|
||||
>
|
||||
{game.popularity_score > 0 ? '+' : ''}{game.popularity_score} ⭐
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* Actions */}
|
||||
<div className="flex items-center gap-2 flex-wrap">
|
||||
{/* Game Favor Status Badge and Controls - Only show if game is enabled */}
|
||||
{/* Row 2: Game Stats + Favor Controls */}
|
||||
<div className="flex items-center justify-between gap-2 ml-7">
|
||||
{/* Game Stats */}
|
||||
<div className="flex items-center gap-3 text-xs md:text-sm text-gray-500 dark:text-gray-400">
|
||||
<span title="Players">{game.min_players}-{game.max_players} 👥</span>
|
||||
<span title="Play count">{game.play_count} ▶️</span>
|
||||
<PopularityBadge
|
||||
upvotes={game.upvotes || 0}
|
||||
downvotes={game.downvotes || 0}
|
||||
popularityScore={game.popularity_score || 0}
|
||||
size="sm"
|
||||
showCounts={false}
|
||||
showNet={true}
|
||||
showRatio={false}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Favor Controls */}
|
||||
{game.enabled && (
|
||||
<>
|
||||
<div className="flex items-center gap-1.5 md:gap-2">
|
||||
{game.favor_bias === 1 && (
|
||||
<span className="text-xs font-semibold bg-green-100 dark:bg-green-900 text-green-800 dark:text-green-200 px-2 py-0.5 rounded">
|
||||
<span className="text-xs md:text-sm font-semibold bg-green-100 dark:bg-green-900 text-green-800 dark:text-green-200 px-2 py-0.5 md:py-1 rounded whitespace-nowrap">
|
||||
Favored
|
||||
</span>
|
||||
)}
|
||||
{game.favor_bias === -1 && (
|
||||
<span className="text-xs font-semibold bg-red-100 dark:bg-red-900 text-red-800 dark:text-red-200 px-2 py-0.5 rounded">
|
||||
<span className="text-xs md:text-sm font-semibold bg-red-100 dark:bg-red-900 text-red-800 dark:text-red-200 px-2 py-0.5 md:py-1 rounded whitespace-nowrap">
|
||||
Disfavored
|
||||
</span>
|
||||
)}
|
||||
|
||||
{/* Game Favor Controls */}
|
||||
<button
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
@@ -598,7 +610,7 @@ function PackTreeNode({ pack, isExpanded, onToggle, onTogglePack, onToggleGame,
|
||||
onGameFavorChange(game.id, 1);
|
||||
}
|
||||
}}
|
||||
className={`text-xs px-2 py-1 rounded transition font-medium ${
|
||||
className={`text-xs md:text-sm px-2 md:px-3 py-1 md:py-1.5 rounded transition font-medium whitespace-nowrap ${
|
||||
game.favor_bias === 1
|
||||
? 'bg-gray-200 dark:bg-gray-700 text-gray-700 dark:text-gray-300 hover:bg-gray-300 dark:hover:bg-gray-600'
|
||||
: 'bg-green-100 dark:bg-green-900 text-green-800 dark:text-green-200 hover:bg-green-200 dark:hover:bg-green-800'
|
||||
@@ -615,42 +627,23 @@ function PackTreeNode({ pack, isExpanded, onToggle, onTogglePack, onToggleGame,
|
||||
onGameFavorChange(game.id, -1);
|
||||
}
|
||||
}}
|
||||
className={`text-xs px-2 py-1 rounded transition font-medium ${
|
||||
game.favor_bias === -1
|
||||
? 'bg-gray-200 dark:bg-gray-700 text-gray-700 dark:text-gray-300 hover:bg-gray-300 dark:hover:bg-gray-600'
|
||||
: 'bg-red-100 dark:bg-red-900 text-red-800 dark:text-red-200 hover:bg-red-200 dark:hover:bg-red-800'
|
||||
}`}
|
||||
>
|
||||
{game.favor_bias === -1 ? 'Undisfavor' : 'Disfavor'}
|
||||
</button>
|
||||
</>
|
||||
className={`text-xs md:text-sm px-2 md:px-3 py-1 md:py-1.5 rounded transition font-medium whitespace-nowrap ${
|
||||
game.favor_bias === -1
|
||||
? 'bg-gray-200 dark:bg-gray-700 text-gray-700 dark:text-gray-300 hover:bg-gray-300 dark:hover:bg-gray-600'
|
||||
: 'bg-red-100 dark:bg-red-900 text-red-800 dark:text-red-200 hover:bg-red-200 dark:hover:bg-red-800'
|
||||
}`}
|
||||
>
|
||||
{game.favor_bias === -1 ? 'Undisfavor' : 'Disfavor'}
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<button
|
||||
onClick={() => onEditGame(game)}
|
||||
className="text-blue-600 dark:text-blue-400 hover:text-blue-800 dark:hover:text-blue-300 text-sm px-2 py-1"
|
||||
title="Edit game"
|
||||
>
|
||||
✏️
|
||||
</button>
|
||||
<button
|
||||
onClick={() => onDeleteClick(game.id)}
|
||||
className={`text-sm px-2 py-1 transition ${
|
||||
confirmingDelete === game.id
|
||||
? 'text-red-800 dark:text-red-200 animate-pulse'
|
||||
: 'text-red-600 dark:text-red-400 hover:text-red-800 dark:hover:text-red-300'
|
||||
}`}
|
||||
title={confirmingDelete === game.id ? 'Click again to confirm' : 'Delete game'}
|
||||
>
|
||||
🗑️{confirmingDelete === game.id ? '?' : ''}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@ import { useAuth } from '../context/AuthContext';
|
||||
import api from '../api/axios';
|
||||
import GamePoolModal from '../components/GamePoolModal';
|
||||
import { formatLocalTime } from '../utils/dateUtils';
|
||||
import PopularityBadge from '../components/PopularityBadge';
|
||||
|
||||
function Picker() {
|
||||
const { isAuthenticated, loading: authLoading } = useAuth();
|
||||
@@ -15,6 +16,7 @@ function Picker() {
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [picking, setPicking] = useState(false);
|
||||
const [error, setError] = useState('');
|
||||
const [showPopularity, setShowPopularity] = useState(true);
|
||||
|
||||
// Filters
|
||||
const [playerCount, setPlayerCount] = useState('');
|
||||
@@ -542,12 +544,22 @@ function Picker() {
|
||||
<span className="font-semibold text-gray-700 dark:text-gray-300">Play Count:</span>
|
||||
<span className="ml-2 text-gray-600 dark:text-gray-400">{selectedGame.play_count}</span>
|
||||
</div>
|
||||
<div>
|
||||
<span className="font-semibold text-gray-700 dark:text-gray-300">Popularity:</span>
|
||||
<span className="ml-2 text-gray-600 dark:text-gray-400">
|
||||
{selectedGame.popularity_score > 0 ? '+' : ''}
|
||||
{selectedGame.popularity_score}
|
||||
<div className="flex items-center gap-2">
|
||||
<span
|
||||
className="font-semibold text-gray-700 dark:text-gray-300"
|
||||
title="Cumulative popularity across all sessions"
|
||||
>
|
||||
Popularity:
|
||||
</span>
|
||||
<PopularityBadge
|
||||
upvotes={selectedGame.upvotes || 0}
|
||||
downvotes={selectedGame.downvotes || 0}
|
||||
popularityScore={selectedGame.popularity_score || 0}
|
||||
size="md"
|
||||
showCounts={true}
|
||||
showNet={true}
|
||||
showRatio={true}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -645,6 +657,7 @@ function SessionInfo({ sessionId, onGamesUpdate }) {
|
||||
const [games, setGames] = useState([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [confirmingRemove, setConfirmingRemove] = useState(null);
|
||||
const [showPopularity, setShowPopularity] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
loadGames();
|
||||
@@ -714,9 +727,20 @@ function SessionInfo({ sessionId, onGamesUpdate }) {
|
||||
|
||||
return (
|
||||
<div className="bg-white dark:bg-gray-800 rounded-lg shadow-lg p-4 sm:p-6">
|
||||
<h3 className="text-lg sm:text-xl font-semibold mb-4 text-gray-800 dark:text-gray-100">
|
||||
Games Played This Session ({games.length})
|
||||
</h3>
|
||||
<div className="flex justify-between items-center mb-4">
|
||||
<h3 className="text-lg sm:text-xl font-semibold text-gray-800 dark:text-gray-100">
|
||||
Games Played This Session ({games.length})
|
||||
</h3>
|
||||
<label className="flex items-center gap-2 text-sm text-gray-600 dark:text-gray-400 cursor-pointer">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={showPopularity}
|
||||
onChange={(e) => setShowPopularity(e.target.checked)}
|
||||
className="w-4 h-4 rounded border-gray-300 dark:border-gray-600 text-indigo-600 focus:ring-indigo-500 dark:bg-gray-700 cursor-pointer"
|
||||
/>
|
||||
<span className="whitespace-nowrap">Show Popularity</span>
|
||||
</label>
|
||||
</div>
|
||||
{loading ? (
|
||||
<p className="text-gray-500 dark:text-gray-400">Loading...</p>
|
||||
) : games.length === 0 ? (
|
||||
@@ -752,6 +776,17 @@ function SessionInfo({ sessionId, onGamesUpdate }) {
|
||||
Manual
|
||||
</span>
|
||||
)}
|
||||
{showPopularity && (
|
||||
<PopularityBadge
|
||||
upvotes={game.upvotes || 0}
|
||||
downvotes={game.downvotes || 0}
|
||||
popularityScore={game.popularity_score || 0}
|
||||
size="sm"
|
||||
showCounts={true}
|
||||
showNet={true}
|
||||
showRatio={false}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<div className="text-xs sm:text-sm text-gray-500 dark:text-gray-400 mt-1">
|
||||
{game.pack_name} • {formatLocalTime(game.played_at)}
|
||||
|
||||
Reference in New Issue
Block a user