diff --git a/README.md b/README.md index 9dfe24b..a4d8bdc 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ # To Be Played – Frontend Web app of **To Be Played**, a Steam backlog tracker: import your Steam library, decide what to play, and track what -you finished. The app is shown as "Game Log" in the UI, in German and English, light and dark. +you finished. The UI is in German and English, with light and dark mode. ![Library](docs/screenshots/library-desktop-dark.png) diff --git a/public/apple-touch-icon.png b/public/apple-touch-icon.png index af52f7c..01c1027 100644 Binary files a/public/apple-touch-icon.png and b/public/apple-touch-icon.png differ diff --git a/public/favicon.svg b/public/favicon.svg index 2ffe218..a8fab96 100644 --- a/public/favicon.svg +++ b/public/favicon.svg @@ -1,10 +1,23 @@ - - - - - - - - + + + + + + + + + + + + + + + + + + + + + diff --git a/public/icon-192.png b/public/icon-192.png index ac4040b..d633a9e 100644 Binary files a/public/icon-192.png and b/public/icon-192.png differ diff --git a/public/icon-512.png b/public/icon-512.png index 97b0dff..7b4aff4 100644 Binary files a/public/icon-512.png and b/public/icon-512.png differ diff --git a/public/icon-maskable-512.png b/public/icon-maskable-512.png index 2e3aaae..4d3c2a5 100644 Binary files a/public/icon-maskable-512.png and b/public/icon-maskable-512.png differ diff --git a/src/components/AppHeader.jsx b/src/components/AppHeader.jsx index ec73ed3..1d51893 100644 --- a/src/components/AppHeader.jsx +++ b/src/components/AppHeader.jsx @@ -1,6 +1,6 @@ import {Link, NavLink, useLocation} from 'react-router-dom'; import {useTranslation} from 'react-i18next'; -import {Dices, Gamepad2, Languages, LayoutDashboard, Library, LogOut, Moon, Settings, Sun} from 'lucide-react'; +import {Dices, Languages, LayoutDashboard, Library, LogOut, Moon, Settings, Sun} from 'lucide-react'; import {Button} from '@/components/ui/button'; import { @@ -12,6 +12,7 @@ import { } from '@/components/ui/dropdown-menu'; import {usePreferences} from '@/context/PreferencesContext.jsx'; import {cn} from '@/lib/utils'; +import Logo from './Logo.jsx'; import PageContainer from './PageContainer.jsx'; import SteamAvatar from './SteamAvatar.jsx'; import useSteamProfile from '../hooks/useSteamProfile.js'; @@ -43,11 +44,10 @@ export default function AppHeader({isAuthenticated, onLogout}) { return (
- -
- - - Game Log + + + {/* On a narrow phone the menu and buttons need the room; the logo alone stays. */} + ToBePlayed {isAuthenticated && ( diff --git a/src/components/AppHeader.test.jsx b/src/components/AppHeader.test.jsx new file mode 100644 index 0000000..de2baa8 --- /dev/null +++ b/src/components/AppHeader.test.jsx @@ -0,0 +1,24 @@ +// @vitest-environment jsdom +import {afterEach, describe, expect, it, vi} from 'vitest'; +import {cleanup, render, screen} from '@testing-library/react'; +import {MemoryRouter} from 'react-router-dom'; +import AppHeader from './AppHeader.jsx'; +import {PreferencesProvider} from '../context/PreferencesContext.jsx'; + +vi.mock('react-i18next', () => import('../test/i18n-mock.js')); + +afterEach(cleanup); + +describe('AppHeader', () => { + // jsdom applies no media queries, so the test hides the name the way the 380px breakpoint does. + it('keeps a name on the home link when a narrow phone hides the app name', () => { + render( + + {}}/> + , + ); + screen.getByText('ToBePlayed').style.display = 'none'; + + expect(screen.getByRole('link', {name: 'ToBePlayed'})).toBeTruthy(); + }); +}); diff --git a/src/components/Logo.jsx b/src/components/Logo.jsx new file mode 100644 index 0000000..6a2f9a7 --- /dev/null +++ b/src/components/Logo.jsx @@ -0,0 +1,64 @@ +import {useId} from 'react'; + +import {BODY, CHECKLIST, DPAD, OUTLINE, PROGRESS_BAR} from './logoPaths.js'; + +// Each color is [themed CSS, fixed hex]. The hex is the logo as designed; it also stands in where a browser +// cannot read color-mix, which would otherwise make that stop black. +const violet = ['var(--logo-violet, #7467f9)', '#7467f9']; +const blue = ['var(--logo-blue, #0192ef)', '#0192ef']; +const teal = ['var(--logo-teal, #17c7a4)', '#17c7a4']; +const mix = ([a], [b], percentOfB, hex) => [`color-mix(in oklch, ${a}, ${b} ${percentOfB}%)`, hex]; + +function Gradient({id, from, to, stops}) { + return ( + + {stops.map(([offset, [color, hex]]) => )} + + ); +} + +/** + * The ToBePlayed logo. Its three colors come from --logo-violet, --logo-blue and --logo-teal, which style.css sets + * per accent color. + */ +export default function Logo({className, title}) { + // Every copy on the page needs its own ids, or a hidden copy can take the gradients of a visible one with it. + const id = useId().replace(/[^a-zA-Z0-9_-]/g, ''); + const ref = (name) => `${name}-${id}`; + const url = (name) => `url(#${ref(name)})`; + + return ( + + + + + + + + + + + + + + + + + + + + + + + + + + + ); +} diff --git a/src/components/Logo.test.jsx b/src/components/Logo.test.jsx new file mode 100644 index 0000000..434bb40 --- /dev/null +++ b/src/components/Logo.test.jsx @@ -0,0 +1,39 @@ +// @vitest-environment jsdom +import {afterEach, describe, expect, it} from 'vitest'; +import {cleanup, render} from '@testing-library/react'; +import Logo from './Logo.jsx'; + +afterEach(cleanup); + +describe('Logo', () => { + it('gives each copy its own gradients, and each copy points only at its own', () => { + const {container} = render(<>); + const [first, second] = container.querySelectorAll('svg'); + const ids = (svg) => [...svg.querySelectorAll('[id]')].map((element) => element.id); + const references = (svg) => [...svg.querySelectorAll('*')] + .flatMap((element) => [element.getAttribute('fill'), element.getAttribute('clip-path')]) + .filter((value) => value?.startsWith('url(#')) + .map((value) => value.slice(5, -1)); + + expect(ids(first).filter((id) => ids(second).includes(id))).toEqual([]); + for (const svg of [first, second]) { + expect(references(svg).length).toBeGreaterThan(5); + expect(references(svg).every((reference) => ids(svg).includes(reference))).toBe(true); + } + }); + + it('is hidden from screen readers unless it has a title', () => { + const {container, getByRole} = render(<>); + + expect(container.querySelector('svg').getAttribute('aria-hidden')).toBe('true'); + expect(getByRole('img', {name: 'ToBePlayed'})).toBeTruthy(); + }); + + it('gives every gradient stop a fixed color for browsers without color-mix', () => { + const {container} = render(); + const stops = [...container.querySelectorAll('stop')]; + + expect(stops.length).toBeGreaterThan(10); + expect(stops.every((stop) => /^#[0-9a-f]{6}$/.test(stop.getAttribute('stop-color')))).toBe(true); + }); +}); diff --git a/src/components/logoPaths.js b/src/components/logoPaths.js new file mode 100644 index 0000000..857289b --- /dev/null +++ b/src/components/logoPaths.js @@ -0,0 +1,6 @@ +// Traced from the ToBePlayed logo (a JPEG); used by Logo.jsx and, with fixed colors, by public/favicon.svg. +export const OUTLINE = 'M 429 235.6 C 392.6 239.4, 357.1 259.4, 337.1 287.5 C 308.1 328.3, 278.1 428.0, 267.0 520.5 C 260.7 572.6, 267.7 612.7, 287.0 635.5 C 311.9 665.0, 350.6 669.1, 386 646.1 C 393.4 641.2, 416.6 618.4, 430.8 602 C 465.6 561.5, 470.2 557.3, 483.9 553.8 C 493.4 551.3, 706.0 551.0, 715.4 553.5 C 729.4 557.1, 735.2 562.2, 765.0 597 C 805.6 644.4, 826.8 659.2, 856.5 660.6 C 896.8 662.7, 926.3 633.4, 933.3 584.3 C 943.2 515.9, 904.8 351.0, 865.1 291 C 843.3 258.0, 805.4 237.4, 762.4 235.2 C 744.0 234.3, 734.0 237.0, 714 248.5 C 690.1 262.0, 691.4 261.9, 600.3 261.9 C 508.8 262.0, 511.5 262.3, 485.1 247.6 C 464.7 236.2, 451.2 233.3, 429 235.6'; +export const BODY = 'M 585.8 263.2 C 584.9 263.9, 580.7 269.6, 576.5 276 C 572.3 282.3, 567.8 288.0, 566.6 288.7 C 562.6 291.0, 519.9 290.4, 509.2 287.9 C 501.0 286.0, 498.2 285.1, 485.2 280.1 C 482.2 279.0, 475.7 275.6, 470.5 272.5 C 457.3 264.6, 454.9 264, 440.1 264 C 396.7 264, 358.2 292.8, 341.6 337.7 C 340.2 341.2, 338.8 344.9, 333.8 357.2 C 324.3 380.8, 312.9 420.7, 306.4 452.4 C 305.0 459.5, 303.6 465.7, 303.4 466.0 C 303.2 466.4, 302.5 469.8, 301.9 473.6 C 301.4 477.4, 300.5 482.9, 300 486 C 296.7 505.4, 295.9 510.6, 293.7 527 C 292.3 538.2, 292.2 570.5, 293.7 580 C 296.2 596.5, 302.2 611.4, 310.0 620.2 C 322.7 634.7, 347.7 636.7, 367.5 624.8 C 373.4 621.3, 398.0 597.7, 405.9 588 C 408.4 584.9, 411.6 581.3, 413 580 C 414.3 578.6, 417.5 575.0, 420 572 C 422.4 568.9, 425.6 565.3, 427.0 564 C 428.4 562.6, 432.0 558.5, 435.0 555.0 C 449.9 537.3, 457.6 532.3, 478.9 526.3 C 487.4 524.0, 487.9 524, 536.1 524 C 562.9 524, 585.5 524.2, 586.4 524.6 C 589.3 525.7, 588.0 529.8, 581.9 538.1 C 573.3 550.1, 573.6 551.4, 585.7 551.7 C 596.4 552.1, 598.0 551.4, 601.8 545.6 C 609.4 533.8, 616.2 525.1, 618.2 524.6 C 619.4 524.2, 641.6 524.0, 667.5 524.0 C 718.4 523.9, 717.4 523.9, 732.8 529.8 C 744.3 534.2, 755.3 542.9, 766.0 556 C 768.4 559.0, 772.0 563.0, 774 565 C 775.9 566.9, 779.5 570.9, 781.9 574 C 784.4 577.0, 787.1 580.1, 788 581 C 788.8 581.8, 791.5 584.9, 794.0 587.9 C 800.4 595.8, 822.3 617.4, 827.4 620.9 C 839.6 629.3, 847.4 632, 860.0 632 C 871.1 632, 871.9 631.8, 878.0 628.7 C 890.8 622.4, 898.9 610.6, 903.3 591.9 C 907.6 573.9, 908.2 568.5, 907.6 551 C 907.0 533.0, 905.7 520.1, 902.0 498 C 897.3 470.2, 897.4 470.7, 894.0 454.5 C 885.8 415.6, 871.5 367.1, 862.0 345.9 C 850.1 319.4, 847.3 313.9, 842.8 307.4 C 823.2 279.0, 794.4 263.9, 759.5 264.0 C 744.5 264.0, 744.4 264.0, 738.3 267.2 C 696.2 289.5, 694.3 290, 638.0 290 C 589.6 290, 592.2 290.9, 602 276.0 C 608.2 266.5, 608.7 265.1, 606.8 263.2 C 605.1 261.5, 587.9 261.5, 585.8 263.2'; +export const DPAD = 'M 407.1 337.3 C 402.7 339.8, 402.0 342.5, 402.0 357.5 C 401.9 375.8, 402.7 375, 385.1 375 C 365.0 375, 362.9 376.7, 363.0 394.2 C 363.0 412.1, 364.9 413.9, 384.7 413.9 C 402.4 414.0, 402 413.5, 402 430.9 C 402 450.7, 403.1 452, 421 452 C 438.9 452, 440.4 450.2, 440.4 429.8 C 440.4 420.7, 440.8 416.7, 441.7 415.8 C 442.6 414.8, 447.6 414.3, 458.1 414 L 473.1 413.5 476.1 410.1 L 479.1 406.7 478.8 393.9 C 478.3 375.9, 478.0 375.6, 459.7 375.2 C 439.3 374.8, 440 375.4, 440 356.4 C 440 342.6, 439.0 339.2, 434.5 337.1 C 430.8 335.5, 410.2 335.6, 407.1 337.3'; +export const CHECKLIST = 'M 563.5 333.5 C 559.7 337.6, 555.9 342.0, 555.1 343.2 C 550.8 349.4, 549.2 349.9, 545 346 C 537.8 339.3, 532.7 344.9, 539.2 352.2 C 541.0 354.2, 543.4 357.0, 544.6 358.4 C 547.8 362.1, 551.3 361.7, 554.7 357.4 C 556.2 355.4, 559.6 351.5, 562.3 348.6 C 565.0 345.8, 568.3 341.9, 569.8 340.0 C 571.3 338.0, 573.5 335.4, 574.7 334.2 C 577.2 331.6, 577.6 329.0, 575.8 327.2 C 573.2 324.6, 570.4 326.0, 563.5 333.5 M 525.4 329.2 C 525.1 329.9, 525.0 340.2, 525.2 352.0 L 525.5 373.5 547.2 373.7 L 569.0 374.0 570.0 371.2 C 571.2 368.3, 570.3 350.3, 569.0 349.5 C 566.9 348.2, 564.1 353.7, 563.8 359.5 L 563.5 365.5 556.3 366 C 552.4 366.2, 545.4 366.2, 540.8 366 L 532.5 365.5 532.2 351.1 C 531.8 333.1, 531.1 334, 545.4 334 C 555.2 334, 556.9 333.7, 558.4 332.0 C 559.4 330.9, 559.9 329.6, 559.5 329.0 C 558.5 327.4, 526.0 327.6, 525.4 329.2 M 591.6 335.8 C 589.4 338.2, 589.5 340.1, 592.0 342.8 C 594.0 344.9, 594.2 345, 631.2 344.9 C 651.7 344.9, 669.5 344.6, 670.8 344.3 C 673.7 343.5, 675.2 339.8, 674 336.5 L 673.0 334 633.1 334 C 595.8 334, 593.2 334.1, 591.6 335.8 M 591.6 357.8 C 589.4 360.2, 589.5 362.2, 591.8 364.3 C 593.4 365.8, 596.4 366.0, 617.5 365.7 L 641.5 365.5 641.8 362.6 C 642.5 356.3, 641.2 356, 616 356 C 595.2 356, 593.1 356.1, 591.6 357.8 M 567.4 387.2 C 547.5 409.5, 548.5 408.6, 545.4 404.5 C 543.0 401.3, 541.5 400.8, 538.4 402.0 C 534.2 403.6, 535.9 407.7, 544.3 416.7 C 548.5 421.2, 550.1 420.8, 555.6 414.2 C 558.3 411.1, 561.4 407.5, 562.5 406.3 C 563.7 405.2, 566.4 402.0, 568.5 399.3 C 570.7 396.7, 573.5 393.4, 574.7 392.2 C 577.2 389.6, 577.6 387.0, 575.8 385.2 C 573.6 383.0, 570.4 383.9, 567.4 387.2 M 525.4 387.2 C 525.1 387.9, 525.0 398.2, 525.2 410.0 L 525.5 431.5 546.8 431.7 C 561.7 431.9, 568.6 431.6, 569.6 430.8 C 571.6 429.2, 571.6 407, 569.5 407 C 566.8 407, 564.1 412.3, 563.8 418.5 L 563.5 424.5 548 424.5 L 532.5 424.5 532.5 409 L 532.5 393.5 544.1 393.2 C 554.8 392.9, 555.8 392.7, 557.9 390.5 C 559.1 389.2, 559.9 387.6, 559.5 387.0 C 558.5 385.4, 526.0 385.6, 525.4 387.2 M 591.6 393.8 C 589.3 396.3, 589.5 398.2, 592.3 400.8 C 594.6 402.9, 595.0 403, 631.5 402.9 C 651.8 402.9, 669.5 402.6, 670.8 402.3 C 674.1 401.4, 675.4 397.3, 673.5 394.3 L 671.9 392 632.6 392 C 595.8 392, 593.2 392.1, 591.6 393.8 M 592 416 C 589.5 418.4, 589.4 421.1, 591.7 423.3 C 594.1 425.4, 638.0 426.0, 640.5 423.9 C 642.7 422.1, 642.3 417.3, 639.7 415.5 C 635.9 412.8, 594.7 413.2, 592 416 M 525.6 444.6 C 525.3 445.0, 525 454.9, 525 466.6 L 525 488.0 527.5 489.0 C 531.4 490.4, 568.3 490.2, 569.8 488.8 C 571.4 487.1, 571.4 446.8, 569.8 445.2 C 568.5 443.9, 526.9 443.4, 525.6 444.6 M 592 452 C 589.4 454.5, 589.4 457.2, 592.1 459.5 C 595.5 462.7, 668.3 463.0, 672.3 459.9 C 675.6 457.3, 675.7 455.0, 672.6 452.1 C 669.2 448.8, 595.2 448.7, 592 452 M 532.4 452.2 C 532.1 453, 532.0 459.8, 532.2 467.5 L 532.5 481.5 548 481.5 L 563.5 481.5 563.5 466.5 L 563.5 451.5 548.2 451.2 C 536.2 451.0, 532.8 451.2, 532.4 452.2 M 596.8 472.9 C 590.0 474.2, 588.0 478.0, 592 482 C 595.0 485.0, 637.6 485.0, 640.4 482.0 C 642.8 479.3, 642.1 474.8, 639 473.2 C 636.2 471.9, 603.5 471.6, 596.8 472.9'; +export const PROGRESS_BAR = 'M 532.4 584 C 478.7 584.5, 481.1 584.1, 477.2 595 C 475.5 599.7, 475.6 618.4, 477.4 623.5 C 480.2 631.7, 483.8 633.9, 494.3 634.1 C 499.3 634.2, 504.8 634.4, 506.5 634.5 C 508.1 634.7, 554.3 635.2, 609.0 635.6 L 708.6 636.3 713.9 633.9 C 722.2 630.1, 724.0 625.7, 723.9 608.7 C 723.9 594.3, 723.0 591.4, 717.1 586.2 L 714.5 584.0 657.5 583.8 C 626.1 583.8, 595.5 583.6, 589.5 583.6 C 583.4 583.5, 557.7 583.7, 532.4 584 M 491.5 591.1 C 484.5 593.6, 484 595.0, 484 609.0 L 484 621.8 487.0 624.8 L 490.0 627.8 598.6 627.8 C 716.1 627.8, 710.7 628.1, 714.3 622.5 C 716.3 619.5, 716.7 600.6, 714.8 596.6 C 712.7 592.0, 714.4 592.1, 604.5 591.6 C 546.7 591.3, 498.3 590.9, 497 590.6 C 495.6 590.3, 493.1 590.5, 491.5 591.1 M 491.6 597.7 C 490.3 599.2, 490 601.6, 490 609.1 C 490 621.9, 490.1 622.1, 504.6 621.7 L 515.5 621.5 515.5 609 L 515.5 596.5 504.4 596.2 C 494.6 595.9, 493.1 596.1, 491.6 597.7 M 524.4 597.2 C 524.1 598.0, 524.0 603.7, 524.2 610.0 L 524.5 621.5 537 621.5 L 549.5 621.5 549.5 609 L 549.5 596.5 537.2 596.2 C 527.6 596.0, 524.8 596.2, 524.4 597.2 M 557.9 598.5 C 557.4 599.9, 557 604.6, 557 609 C 557 613.3, 557.4 618.0, 557.9 619.4 L 558.9 622.0 571.2 621.7 L 583.5 621.5 583.5 609 L 583.5 596.5 571.2 596.2 L 558.9 595.9 557.9 598.5 M 592.1 597.3 C 591.3 598.3, 591.0 602.5, 591.2 610.1 L 591.5 621.5 621.5 621.5 L 651.5 621.5 651.5 609 L 651.5 596.5 622.3 596.2 C 598.3 596.0, 593.0 596.2, 592.1 597.3'; diff --git a/src/pages/LandingPage.jsx b/src/pages/LandingPage.jsx index 6a73148..b12982d 100644 --- a/src/pages/LandingPage.jsx +++ b/src/pages/LandingPage.jsx @@ -9,6 +9,7 @@ import {Button} from '@/components/ui/button'; import {Card} from '@/components/ui/card'; import {Skeleton} from '@/components/ui/skeleton'; import GameCover from '../components/GameCover.jsx'; +import Logo from '../components/Logo.jsx'; import StatCard from '../components/StatCard.jsx'; import {hasSession} from '../utils/auth.js'; @@ -59,7 +60,7 @@ export default function LandingPage() {
- Game Log + ToBePlayed

{t('MAIN.WELCOME')}

{t('MAIN.DESCRIPTION')}

diff --git a/src/style.css b/src/style.css index ebc46d4..c4d47c2 100644 --- a/src/style.css +++ b/src/style.css @@ -182,6 +182,38 @@ --color-ring: var(--ring); } +/* The three logo colors per accent (components/Logo.jsx). Violet is the logo as designed; the others use + neighbouring hues, because a mix of a warm accent with the logo's teal turns olive. */ +[data-accent="blue"] { + --logo-violet: #3b82f6; + --logo-blue: #06b6d4; + --logo-teal: #14b8a6; +} + +[data-accent="green"] { + --logo-violet: #22c55e; + --logo-blue: #14b8a6; + --logo-teal: #0ea5e9; +} + +[data-accent="orange"] { + --logo-violet: #f97316; + --logo-blue: #f59e0b; + --logo-teal: #eab308; +} + +[data-accent="pink"] { + --logo-violet: #ec4899; + --logo-blue: #a855f7; + --logo-teal: #6366f1; +} + +[data-accent="red"] { + --logo-violet: #ef4444; + --logo-blue: #f97316; + --logo-teal: #f59e0b; +} + @layer base { * { @apply border-border outline-ring/50;