diff --git a/public/i18n/de.json b/public/i18n/de.json index 73a67a2..4de28fc 100644 --- a/public/i18n/de.json +++ b/public/i18n/de.json @@ -109,7 +109,10 @@ "SYNCING": "Synchronisiere mit Steam …", "SYNCING_FIRST_TIME": "Erste Synchronisation mit Steam läuft. Deine Spiele erscheinen gleich.", "SYNC_FAILED": "Synchronisation fehlgeschlagen. Bitte später erneut versuchen.", - "LAST_SYNCED": "Zuletzt synchronisiert {{when}}" + "LAST_SYNCED": "Zuletzt synchronisiert {{when}}", + "FILTER_ALL": "Alle", + "SEARCH_PLACEHOLDER": "Spiel suchen …", + "NO_MATCH": "Keine Spiele für diesen Filter." }, "GAME_DETAIL": { "BACK": "Zurück", @@ -157,5 +160,32 @@ "LANGUAGE": "Sprache", "DARK_MODE": "Dunkles Design", "LIGHT_MODE": "Helles Design" + }, + "BACKLOG": { + "TITLE": "Mein Backlog", + "STATUS_LABEL": "Status", + "PRIORITY_LABEL": "Priorität", + "RATING_LABEL": "Bewertung", + "RATING_STARS_one": "{{count}} Stern", + "RATING_STARS_other": "{{count}} Sterne", + "NOTES_LABEL": "Notizen", + "NOTES_PLACEHOLDER": "Was willst du dir zu diesem Spiel merken?", + "SAVE": "Speichern", + "RESET": "Verwerfen", + "SAVED": "Gespeichert", + "SAVE_FAILED": "Speichern fehlgeschlagen. Bitte erneut versuchen.", + "STATUS": { + "BACKLOG": "Backlog", + "PLAYING": "Spiele ich", + "FINISHED": "Durchgespielt", + "COMPLETED": "100 %", + "DROPPED": "Abgebrochen" + }, + "PRIORITY": { + "NONE": "Keine", + "HIGH": "Hoch", + "MEDIUM": "Mittel", + "LOW": "Niedrig" + } } } diff --git a/public/i18n/en.json b/public/i18n/en.json index 334a73d..0e5f4ee 100644 --- a/public/i18n/en.json +++ b/public/i18n/en.json @@ -109,7 +109,10 @@ "SYNCING": "Syncing with Steam …", "SYNCING_FIRST_TIME": "First sync with Steam in progress. Your games will appear here shortly.", "SYNC_FAILED": "Sync failed. Please try again later.", - "LAST_SYNCED": "Last synced {{when}}" + "LAST_SYNCED": "Last synced {{when}}", + "FILTER_ALL": "All", + "SEARCH_PLACEHOLDER": "Search games …", + "NO_MATCH": "No games match this filter." }, "GAME_DETAIL": { "BACK": "Back", @@ -157,5 +160,32 @@ "LANGUAGE": "Language", "DARK_MODE": "Dark mode", "LIGHT_MODE": "Light mode" + }, + "BACKLOG": { + "TITLE": "My backlog", + "STATUS_LABEL": "Status", + "PRIORITY_LABEL": "Priority", + "RATING_LABEL": "Rating", + "RATING_STARS_one": "{{count}} star", + "RATING_STARS_other": "{{count}} stars", + "NOTES_LABEL": "Notes", + "NOTES_PLACEHOLDER": "What do you want to remember about this game?", + "SAVE": "Save", + "RESET": "Discard", + "SAVED": "Saved", + "SAVE_FAILED": "Saving failed. Please try again.", + "STATUS": { + "BACKLOG": "Backlog", + "PLAYING": "Playing", + "FINISHED": "Finished", + "COMPLETED": "100%", + "DROPPED": "Dropped" + }, + "PRIORITY": { + "NONE": "None", + "HIGH": "High", + "MEDIUM": "Medium", + "LOW": "Low" + } } } diff --git a/src/components/BacklogCard.jsx b/src/components/BacklogCard.jsx new file mode 100644 index 0000000..896896a --- /dev/null +++ b/src/components/BacklogCard.jsx @@ -0,0 +1,156 @@ +import {useState} from 'react'; +import {useNavigate} from 'react-router-dom'; +import {useTranslation} from 'react-i18next'; +import {toast} from 'sonner'; +import {ListTodo, Loader2, Star} from 'lucide-react'; + +import {Button} from '@/components/ui/button'; +import {Card} from '@/components/ui/card'; +import {Label} from '@/components/ui/label'; +import {Select, SelectContent, SelectItem, SelectTrigger, SelectValue} from '@/components/ui/select'; +import {Textarea} from '@/components/ui/textarea'; +import {cn} from '@/lib/utils'; +import {STATUS_META} from './GameStatusBadge.jsx'; +import {GAME_PRIORITIES, GAME_STATUSES, NO_PRIORITY, toBacklogRequest} from '../utils/backlog.js'; + +const NOTES_MAX = 2000; + +const toForm = (entry) => ({ + status: entry.status ?? 'BACKLOG', + rating: entry.rating ?? 0, + notes: entry.notes ?? '', + priority: entry.priority ?? NO_PRIORITY, +}); + +export default function BacklogCard({game, onSaved}) { + const {t} = useTranslation(); + const navigate = useNavigate(); + const [saved, setSaved] = useState(() => toForm(game)); + const [form, setForm] = useState(saved); + const [busy, setBusy] = useState(false); + + const dirty = JSON.stringify(form) !== JSON.stringify(saved); + const set = (field) => (value) => setForm((f) => ({...f, [field]: value})); + + const save = async () => { + const token = localStorage.getItem('tbp_jwt'); + if (!token) { + navigate('/login'); + return; + } + setBusy(true); + try { + const response = await fetch(`${import.meta.env.VITE_CORE_API_URL}/api/library/games/${game.appid}/backlog`, { + method: 'PUT', + headers: {'Authorization': `Bearer ${token}`, 'Content-Type': 'application/json'}, + body: JSON.stringify(toBacklogRequest(form)), + }); + if (response.status === 401) { + localStorage.removeItem('tbp_jwt'); + navigate('/login'); + return; + } + if (!response.ok) { + toast.error(t('BACKLOG.SAVE_FAILED')); + return; + } + const entry = await response.json(); + const next = toForm(entry); + setSaved(next); + onSaved?.(entry); + setForm(next); + toast.success(t('BACKLOG.SAVED')); + } catch (err) { + console.error('backlog save error:', err); + toast.error(t('INFO.ERROR.NETWORK_FAILURE')); + } finally { + setBusy(false); + } + }; + + return ( + +

+ + {t('BACKLOG.TITLE')} +

+ +
+
+ + +
+ +
+ + +
+ +
+ +
+ {[1, 2, 3, 4, 5].map((n) => ( + + ))} +
+
+
+ +
+
+ + {form.notes.length} / {NOTES_MAX} +
+