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>
|
||||
)}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user