we're about to port the chrome-extension. everything else mostly works
This commit is contained in:
101
frontend/src/components/GamePoolModal.jsx
Normal file
101
frontend/src/components/GamePoolModal.jsx
Normal file
@@ -0,0 +1,101 @@
|
||||
import React from 'react';
|
||||
|
||||
function GamePoolModal({ games, onClose }) {
|
||||
return (
|
||||
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50 p-4">
|
||||
<div className="bg-white dark:bg-gray-800 rounded-lg max-w-4xl w-full max-h-[90vh] overflow-hidden flex flex-col">
|
||||
<div className="flex justify-between items-center p-4 sm:p-6 border-b dark:border-gray-700">
|
||||
<div>
|
||||
<h2 className="text-xl sm:text-2xl font-bold dark:text-gray-100">Available Game Pool</h2>
|
||||
<p className="text-sm text-gray-600 dark:text-gray-400 mt-1">
|
||||
{games.length} {games.length === 1 ? 'game' : 'games'} match your filters
|
||||
</p>
|
||||
</div>
|
||||
<button
|
||||
onClick={onClose}
|
||||
className="text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-200 text-2xl"
|
||||
>
|
||||
×
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="overflow-y-auto p-4 sm:p-6">
|
||||
{games.length === 0 ? (
|
||||
<p className="text-gray-500 dark:text-gray-400 text-center py-8">
|
||||
No games match your current filters. Try adjusting the filters or enabling more games.
|
||||
</p>
|
||||
) : (
|
||||
<div className="grid gap-3 sm:grid-cols-2 lg:grid-cols-3">
|
||||
{games.map((game) => {
|
||||
// Determine border color based on favor bias
|
||||
const getBorderStyle = () => {
|
||||
if (game.favor_bias === 1) {
|
||||
return 'border-green-300 dark:border-green-700 bg-green-50 dark:bg-green-900/10';
|
||||
}
|
||||
if (game.favor_bias === -1) {
|
||||
return 'border-red-300 dark:border-red-700 bg-red-50 dark:bg-red-900/10';
|
||||
}
|
||||
return 'border-gray-200 dark:border-gray-700 bg-gray-50 dark:bg-gray-700';
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
key={game.id}
|
||||
className={`border rounded-lg p-3 sm:p-4 ${getBorderStyle()}`}
|
||||
>
|
||||
<div className="flex items-start justify-between gap-2 mb-1">
|
||||
<h3 className="font-semibold text-sm sm:text-base dark:text-gray-100 flex-1">
|
||||
{game.title}
|
||||
</h3>
|
||||
{/* Show favor indicator */}
|
||||
{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 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 whitespace-nowrap">
|
||||
Disfavored
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<p className="text-xs text-gray-600 dark:text-gray-400 mb-2">
|
||||
{game.pack_name}
|
||||
</p>
|
||||
<div className="flex flex-wrap gap-2 text-xs">
|
||||
<span className="bg-indigo-100 dark:bg-indigo-900 text-indigo-800 dark:text-indigo-200 px-2 py-1 rounded">
|
||||
{game.min_players}-{game.max_players} players
|
||||
</span>
|
||||
{game.game_type && (
|
||||
<span className="bg-gray-200 dark:bg-gray-600 text-gray-700 dark:text-gray-300 px-2 py-1 rounded">
|
||||
{game.game_type}
|
||||
</span>
|
||||
)}
|
||||
{game.play_count > 0 && (
|
||||
<span className="bg-green-100 dark:bg-green-900 text-green-800 dark:text-green-200 px-2 py-1 rounded">
|
||||
{game.play_count} plays
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="border-t dark:border-gray-700 p-4 sm:p-6">
|
||||
<button
|
||||
onClick={onClose}
|
||||
className="w-full bg-indigo-600 dark:bg-indigo-700 text-white py-2 sm:py-3 rounded-lg hover:bg-indigo-700 dark:hover:bg-indigo-800 transition"
|
||||
>
|
||||
Close
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default GamePoolModal;
|
||||
|
||||
48
frontend/src/components/Logo.jsx
Normal file
48
frontend/src/components/Logo.jsx
Normal file
@@ -0,0 +1,48 @@
|
||||
import React from 'react';
|
||||
import { useTheme } from '../context/ThemeContext';
|
||||
|
||||
function Logo({ size = 'md', className = '' }) {
|
||||
const { isDark } = useTheme();
|
||||
|
||||
const sizes = {
|
||||
sm: 'w-8 h-8',
|
||||
md: 'w-10 h-10',
|
||||
lg: 'w-16 h-16',
|
||||
xl: 'w-24 h-24',
|
||||
};
|
||||
|
||||
return (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 100 100"
|
||||
className={`${sizes[size]} ${className}`}
|
||||
aria-label="Jackbox Game Picker Logo"
|
||||
>
|
||||
<defs>
|
||||
<linearGradient id="logo-grad" x1="0%" y1="0%" x2="100%" y2="100%">
|
||||
<stop offset="0%" style={{ stopColor: '#6366f1', stopOpacity: 1 }} />
|
||||
<stop offset="100%" style={{ stopColor: '#4f46e5', stopOpacity: 1 }} />
|
||||
</linearGradient>
|
||||
</defs>
|
||||
|
||||
{/* Dice/Box shape */}
|
||||
<rect x="10" y="10" width="80" height="80" rx="12" fill="url(#logo-grad)"/>
|
||||
|
||||
{/* Dots representing game selection */}
|
||||
<circle cx="30" cy="30" r="6" fill="white" opacity="0.9"/>
|
||||
<circle cx="50" cy="30" r="6" fill="white" opacity="0.9"/>
|
||||
<circle cx="70" cy="30" r="6" fill="white" opacity="0.9"/>
|
||||
|
||||
<circle cx="30" cy="50" r="6" fill="white" opacity="0.9"/>
|
||||
<circle cx="50" cy="50" r="6" fill="white" opacity="1"/>
|
||||
<circle cx="70" cy="50" r="6" fill="white" opacity="0.9"/>
|
||||
|
||||
<circle cx="30" cy="70" r="6" fill="white" opacity="0.9"/>
|
||||
<circle cx="50" cy="70" r="6" fill="white" opacity="0.9"/>
|
||||
<circle cx="70" cy="70" r="6" fill="white" opacity="0.9"/>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
export default Logo;
|
||||
|
||||
41
frontend/src/components/ThemeToggle.jsx
Normal file
41
frontend/src/components/ThemeToggle.jsx
Normal file
@@ -0,0 +1,41 @@
|
||||
import React from 'react';
|
||||
import { useTheme } from '../context/ThemeContext';
|
||||
|
||||
function ThemeToggle() {
|
||||
const { theme, toggleTheme } = useTheme();
|
||||
|
||||
const getTitle = () => {
|
||||
if (theme === 'light') return 'Switch to dark mode';
|
||||
if (theme === 'dark') return 'Switch to system mode';
|
||||
return 'Switch to light mode';
|
||||
};
|
||||
|
||||
return (
|
||||
<button
|
||||
onClick={toggleTheme}
|
||||
className="p-2 rounded-lg hover:bg-indigo-700 dark:hover:bg-indigo-900 transition-colors relative"
|
||||
aria-label="Toggle theme"
|
||||
title={getTitle()}
|
||||
>
|
||||
{theme === 'light' ? (
|
||||
// Sun icon - currently light mode
|
||||
<svg xmlns="http://www.w3.org/2000/svg" className="h-5 w-5" viewBox="0 0 20 20" fill="currentColor">
|
||||
<path fillRule="evenodd" d="M10 2a1 1 0 011 1v1a1 1 0 11-2 0V3a1 1 0 011-1zm4 8a4 4 0 11-8 0 4 4 0 018 0zm-.464 4.95l.707.707a1 1 0 001.414-1.414l-.707-.707a1 1 0 00-1.414 1.414zm2.12-10.607a1 1 0 010 1.414l-.706.707a1 1 0 11-1.414-1.414l.707-.707a1 1 0 011.414 0zM17 11a1 1 0 100-2h-1a1 1 0 100 2h1zm-7 4a1 1 0 011 1v1a1 1 0 11-2 0v-1a1 1 0 011-1zM5.05 6.464A1 1 0 106.465 5.05l-.708-.707a1 1 0 00-1.414 1.414l.707.707zm1.414 8.486l-.707.707a1 1 0 01-1.414-1.414l.707-.707a1 1 0 011.414 1.414zM4 11a1 1 0 100-2H3a1 1 0 000 2h1z" clipRule="evenodd" />
|
||||
</svg>
|
||||
) : theme === 'dark' ? (
|
||||
// Moon icon - currently dark mode
|
||||
<svg xmlns="http://www.w3.org/2000/svg" className="h-5 w-5" viewBox="0 0 20 20" fill="currentColor">
|
||||
<path d="M17.293 13.293A8 8 0 016.707 2.707a8.001 8.001 0 1010.586 10.586z" />
|
||||
</svg>
|
||||
) : (
|
||||
// Computer/System icon - currently system mode
|
||||
<svg xmlns="http://www.w3.org/2000/svg" className="h-5 w-5" viewBox="0 0 20 20" fill="currentColor">
|
||||
<path fillRule="evenodd" d="M3 5a2 2 0 012-2h10a2 2 0 012 2v8a2 2 0 01-2 2h-2.22l.123.489.804.804A1 1 0 0113 18H7a1 1 0 01-.707-1.707l.804-.804L7.22 15H5a2 2 0 01-2-2V5zm5.771 7H5V5h10v7H8.771z" clipRule="evenodd" />
|
||||
</svg>
|
||||
)}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
export default ThemeToggle;
|
||||
|
||||
76
frontend/src/components/Toast.jsx
Normal file
76
frontend/src/components/Toast.jsx
Normal file
@@ -0,0 +1,76 @@
|
||||
import { createContext, useContext, useState, useCallback } from 'react';
|
||||
|
||||
const ToastContext = createContext();
|
||||
|
||||
export const useToast = () => {
|
||||
const context = useContext(ToastContext);
|
||||
if (!context) {
|
||||
throw new Error('useToast must be used within ToastProvider');
|
||||
}
|
||||
return context;
|
||||
};
|
||||
|
||||
export const ToastProvider = ({ children }) => {
|
||||
const [toasts, setToasts] = useState([]);
|
||||
|
||||
const showToast = useCallback((message, type = 'info', duration = 4000) => {
|
||||
const id = Date.now();
|
||||
setToasts(prev => [...prev, { id, message, type }]);
|
||||
|
||||
if (duration > 0) {
|
||||
setTimeout(() => {
|
||||
setToasts(prev => prev.filter(t => t.id !== id));
|
||||
}, duration);
|
||||
}
|
||||
}, []);
|
||||
|
||||
const removeToast = useCallback((id) => {
|
||||
setToasts(prev => prev.filter(t => t.id !== id));
|
||||
}, []);
|
||||
|
||||
const success = useCallback((message, duration) => showToast(message, 'success', duration), [showToast]);
|
||||
const error = useCallback((message, duration) => showToast(message, 'error', duration), [showToast]);
|
||||
const info = useCallback((message, duration) => showToast(message, 'info', duration), [showToast]);
|
||||
const warning = useCallback((message, duration) => showToast(message, 'warning', duration), [showToast]);
|
||||
|
||||
return (
|
||||
<ToastContext.Provider value={{ showToast, success, error, info, warning, removeToast }}>
|
||||
{children}
|
||||
<div className="fixed top-4 right-4 z-50 space-y-2 pointer-events-none">
|
||||
{toasts.map(toast => (
|
||||
<Toast key={toast.id} toast={toast} onClose={() => removeToast(toast.id)} />
|
||||
))}
|
||||
</div>
|
||||
</ToastContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
const Toast = ({ toast, onClose }) => {
|
||||
const bgColors = {
|
||||
success: 'bg-green-600 dark:bg-green-700',
|
||||
error: 'bg-red-600 dark:bg-red-700',
|
||||
warning: 'bg-orange-600 dark:bg-orange-700',
|
||||
info: 'bg-indigo-600 dark:bg-indigo-700'
|
||||
};
|
||||
|
||||
const icons = {
|
||||
success: '✓',
|
||||
error: '✕',
|
||||
warning: '⚠',
|
||||
info: 'ℹ'
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={`${bgColors[toast.type]} text-white px-4 py-3 rounded-lg shadow-lg flex items-center gap-3 min-w-[300px] max-w-md pointer-events-auto animate-slide-in`}>
|
||||
<span className="text-xl font-bold flex-shrink-0">{icons[toast.type]}</span>
|
||||
<span className="flex-1 text-sm">{toast.message}</span>
|
||||
<button
|
||||
onClick={onClose}
|
||||
className="flex-shrink-0 hover:opacity-80 transition text-xl leading-none"
|
||||
>
|
||||
×
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user