Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 9 additions & 4 deletions docs/features/homepage-panel.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,15 @@ otherwise never discover — the community printing-identification game

The whole panel returns `null` without a remote backend configured
(`useRemoteBackendConfigured`) — the exact same condition `Navbar.tsx`
already uses to decide whether to show its own `/whatsthat` and `My Decks` nav links. Promoting a CTA to a route that would 404/no-op for a
Local-Folder-only visitor is worse than not showing it; this keeps the
panel's own gating identical to the nav's, rather than inventing a
second, possibly-drifting rule for the same underlying condition.
uses to gate its own `/whatsthat` link. Promoting a CTA to a route that
would 404/no-op for a Local-Folder-only visitor is worse than not showing
it; this keeps the panel's own gating identical to the nav's, rather than
inventing a second, possibly-drifting rule for the same underlying
condition. (Nav+footer redesign, 2026-07-22: `My Decks` no longer has a
nav-bar link of its own at all - cut from the nav regardless of backend
state, see `docs/user-guide.md` - so this panel's own `/myDecks` CTA is
now one of `My Decks`'s only two discovery paths, alongside a direct
`/myDecks` visit; it no longer merely _duplicates_ a nav link.)

## The two CTA cards

Expand Down
24 changes: 16 additions & 8 deletions docs/user-guide.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,10 @@ it. See [`documentation-process.md`](documentation-process.md).
How to search the catalog, read search results, and pick a printing for
a slot in your decklist.

The **Display** page's search bar is dual-mode: an **Add / Browse**
The **Editor** page (the redesigned `/display` route - nav+footer redesign,
2026-07-22, renamed the nav link from "Display (beta)" to plain "Editor";
the classic `/editor` page is still reachable directly by URL but no longer
has a nav link of its own) search bar is dual-mode: an **Add / Browse**
toggle next to the input switches between adding cards to your project
(the usual decklist-line paste/search box) and browsing the whole
catalog without touching your project — browsing renders a grid of
Expand Down Expand Up @@ -44,7 +47,7 @@ questions scroll underneath it.
How to arrange your chosen cards into pages, set bleed/DPI/paper size,
and export a PDF ready to print and cut.

The **Display** page's Page Setup section defaults to Letter landscape,
The **Editor** page's Page Setup section defaults to Letter landscape,
3.175mm bleed, and a **Margin profile** picker (Borderless / Bordered /
Rear-feed) calibrated against an Epson ET-8500/8550 printer — Borderless
is the default and the only profile that fits full bleed on a 4-across
Expand All @@ -59,10 +62,13 @@ strip-cutting while rows keep a gap that suits a die cutter. A
**Link**/**Linked** toggle next to it locks the two axes to move
together when you'd rather set one value for both.

Once your sheet is ready, the **Display** page's Finish footer has two
Once your sheet is ready, the **Editor** page's Finish footer has two
equal-weight buttons: **Save Deck** (or **Sign in to Save**, if you're
not signed in) and **Print / Export →**, plus the existing **Export**
dropdown for lightweight XML/card-image/decklist exports. Your project is
dropdown for lightweight XML/card-image/decklist exports and a small
cloud-download counter beside it (nav+footer redesign, 2026-07-22 - this
used to sit in the top navbar; it now lives next to the exports it counts,
here and again on the Print page). Your project is
also quietly backed up to this browser as you work — a small "Draft
backed up locally" note under the buttons confirms it — and pressing
**Print / Export →** while signed in with unsaved changes offers to save
Expand All @@ -72,14 +78,16 @@ browser's memory and you don't want to lose your work if it struggles.
## Saving and re-using a project

Local-folder and Google Drive options for coming back to a project
later. If you're signed in and have saved decks already, the **Display**
later. If you're signed in and have saved decks already, the **Editor**
page's empty-project landing screen lists them directly so you can jump
back into one without a trip to **My Decks** first.
back into one without a trip to **My Decks** first (**My Decks** itself
has no nav-bar link since the 2026-07-22 nav redesign - reach it from this
landing screen, the homepage's own CTA, or `/myDecks` directly).

## Saved decks, export, and the standalone decrypt tool

Signed in with Discord? The editor and display page's Save button
persists your deck to your account, and the **My Decks** page lists
Signed in with Discord? The classic editor and the Editor page's Save
button persists your deck to your account, and the **My Decks** page lists
everything you've saved, decrypted right there in your browser — the
server only ever stores encrypted, opaque bytes it can't read.

Expand Down
25 changes: 25 additions & 0 deletions frontend/__mocks__/@googleworkspace/drive-picker-react.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
// Jest manual mock (node_modules mock, auto-applied per
// https://jestjs.io/docs/manual-mocks#mocking-node-modules) for
// @googleworkspace/drive-picker-react - an ESM-only package ("type": "module", no "require"
// export condition) that jest/jsdom can't resolve as-installed on this machine. It's only ever
// referenced as JSX components (DrivePicker/DrivePickerDocsView are custom-element wrappers
// with no interactive behavior worth exercising here) - Footer.tsx became the first jest test
// to render BackendConfig's component tree (via its own "Sources" button, see Footer.tsx's own
// module comment) since GoogleDriveBackendConfig.tsx statically imports GoogleDrivePicker.tsx,
// which statically imports this package - a plain no-op stub is enough for that tree to mount
// without error; nothing here asserts on Google Drive picker behavior itself.
const React = require("react");

function DrivePicker({ children }) {
return React.createElement(
"div",
{ "data-testid": "mock-drive-picker" },
children
);
}

function DrivePickerDocsView() {
return null;
}

module.exports = { DrivePicker, DrivePickerDocsView };
32 changes: 28 additions & 4 deletions frontend/src/features/display/FinishFooter.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,10 @@ import React, { useEffect, useState } from "react";
import Button from "react-bootstrap/Button";

import { useAppSelector } from "@/common/types";
import {
DownloadManager,
OpenDownloadManagerButton,
} from "@/features/download/DownloadManager";
import { DisplayExportMenu } from "@/features/export/DisplayExportMenu";
import { useSaveDeckFlow } from "@/features/savedDecks/useSaveDeckFlow";
import { useGetWhoamiQuery } from "@/store/api";
Expand All @@ -49,6 +53,17 @@ export function FinishFooter({
const backendURL = useAppSelector(selectRemoteBackendURL);
const whoami = useGetWhoamiQuery();

// Nav+footer redesign (2026-07-22, N10) - the cloud download-queue counter/manager used to
// live in the global navbar (OpenDownloadManagerButton + DownloadManager), cut from there
// per the redesign since it only ever counted in-browser export downloads (XML/card
// images/decklist/PDF), never the abandoned desktop tool. This is one of its two new mounts
// (the other is pages/print.tsx, which owns the memory-heavy PDF/desktop-tool downloads this
// footer's own DisplayExportMenu deliberately excludes) - both read the same global
// fileDownloadsSlice, so either one shows every download regardless of where it started;
// mounting in both closes the gap where a download enqueued on the other page wouldn't be
// visible without navigating back.
const [showDownloadManager, setShowDownloadManager] = useState(false);

// window isn't available during the static export build - resolved client-only, mirroring
// AuthWidget.tsx's own identical pattern for the exact same `?next=` round-trip.
const [currentHref, setCurrentHref] = useState<string | null>(null);
Expand Down Expand Up @@ -97,10 +112,19 @@ export function FinishFooter({
Print / Export &rarr;
</Button>
</div>
{/* Issue #241 (design doc §5's export-beyond-PDF row) - XML/Card Images/Decklist, unchanged
and unforked; the ONLY export surface this footer still owns directly, per D9's own
"memory-heavy operations move OUT" line. */}
<DisplayExportMenu />
<div className="d-flex gap-2 align-items-center">
{/* Issue #241 (design doc §5's export-beyond-PDF row) - XML/Card Images/Decklist,
unchanged and unforked; the ONLY export surface this footer still owns directly, per
D9's own "memory-heavy operations move OUT" line. */}
<DisplayExportMenu />
<OpenDownloadManagerButton
handleClick={() => setShowDownloadManager(true)}
/>
</div>
<DownloadManager
show={showDownloadManager}
handleClose={() => setShowDownloadManager(false)}
/>
{hasBackedUpThisSession && (
<div
className="text-muted small text-center"
Expand Down
1 change: 1 addition & 0 deletions frontend/src/features/download/DownloadManager.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,7 @@ export function OpenDownloadManagerButton({
onClick={handleClick}
id="dropdown-basic"
className="m-0 p-0"
data-testid="download-manager-toggle"
>
<DownloadIcon />
<span
Expand Down
101 changes: 87 additions & 14 deletions frontend/src/features/moderation/AuthWidget.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,21 @@
* The links round-trip: they point at the backend's allauth routes with
* `?next=<current frontend URL>`, which the backend's account adapter validates against the
* same origin allowlist CORS uses (see accounts/adapter.py).
*
* Nav+footer redesign (2026-07-22, N9) - the signed-in branch used to render a plain
* "Signed in as X (moderator)" / "Sign out" text pair (StatusRow), which wrapped across two
* lines once the navbar was crowded. It's now a compact react-bootstrap `Dropdown` user menu:
* a monogram avatar (whoami carries no avatar image) + username toggle that opens "Sign out"
* (and, for moderators, a "Moderator" entry pointing at the existing moderation surface -
* /whatsthat's own Moderation tab, ModerationTab.tsx - since that's presentation-only tab
* state with no deep-linkable query param yet, not a dedicated route). Additive to this file;
* the signed-out branch and every `?next=` round-trip below are unchanged.
*/

import styled from "@emotion/styled";
import Link from "next/link";
import React, { useEffect, useState } from "react";
import Dropdown from "react-bootstrap/Dropdown";

import { useAppSelector } from "@/common/types";
import { useGetWhoamiQuery } from "@/store/api";
Expand All @@ -44,6 +55,7 @@ const DiscordButton = styled.a`
padding: 0.5rem 0.9rem;
border-radius: 0.3rem;
text-decoration: none;
white-space: nowrap;
&:hover,
&:focus {
background-color: ${DISCORD_BLURPLE_HOVER};
Expand All @@ -69,10 +81,50 @@ function DiscordIcon() {
);
}

const StatusRow = styled.p`
display: flex;
// variant="link" (rather than the Button default of variant="primary") keeps the underlying
// react-bootstrap Button this renders from picking up the theme's solid orange btn-primary
// chrome - this toggle is meant to read as a plain nav-bar control (avatar + username + the
// standard .dropdown-toggle caret), not a button. The &&& bumps this rule's specificity above
// .btn-link's own defaults (underline, link-color) without needing !important.
const UserMenuToggle = styled(Dropdown.Toggle)`
&&& {
display: inline-flex;
align-items: center;
gap: 0.5rem;
background: transparent;
border: 0;
padding: 0.25rem 0.5rem;
color: rgba(255, 255, 255, 0.75);
font-weight: 600;
text-decoration: none;
white-space: nowrap;
&:hover,
&:focus,
&:active {
background: rgba(255, 255, 255, 0.08);
color: #fff;
text-decoration: none;
box-shadow: none;
}
}
`;

const Avatar = styled.span`
display: inline-flex;
align-items: center;
gap: 0.6rem;
justify-content: center;
width: 30px;
height: 30px;
border-radius: 50%;
background-color: var(--bs-primary);
color: #fff;
font-weight: 700;
font-size: 0.9rem;
flex: 0 0 auto;
`;

const UserMenuMenu = styled(Dropdown.Menu)`
background-color: #4e5d6c;
`;

export function AuthWidget() {
Expand All @@ -97,18 +149,39 @@ export function AuthWidget() {

const next = `?next=${encodeURIComponent(currentHref)}`;
return whoami.data.authenticated ? (
<StatusRow className="small m-0" data-testid="auth-widget">
<span>
Signed in as <b>{whoami.data.username}</b>
{whoami.data.moderator && " (moderator)"}
</span>
<a
href={`${backendURL}${whoami.data.logoutUrl}${next}`}
data-testid="auth-widget-logout"
<Dropdown data-testid="auth-widget">
<UserMenuToggle
variant="link"
id="auth-widget-toggle"
data-testid="auth-widget-toggle"
>
Sign out
</a>
</StatusRow>
<Avatar aria-hidden="true">
{whoami.data.username?.[0]?.toUpperCase() ?? "?"}
</Avatar>
<span>{whoami.data.username}</span>
</UserMenuToggle>
<UserMenuMenu align="end">
{whoami.data.moderator && (
<>
<Dropdown.Item
as={Link}
href="/whatsthat"
data-testid="auth-widget-moderator"
title="Open the Moderation tab on What's That Card?"
>
Moderator
</Dropdown.Item>
<Dropdown.Divider />
</>
)}
<Dropdown.Item
href={`${backendURL}${whoami.data.logoutUrl}${next}`}
data-testid="auth-widget-logout"
>
Sign out
</Dropdown.Item>
</UserMenuMenu>
</Dropdown>
) : (
<DiscordButton
href={`${backendURL}${whoami.data.loginUrl}${next}`}
Expand Down
Loading
Loading