Skip to content

Commit

Permalink
feat: add toast
Browse files Browse the repository at this point in the history
  • Loading branch information
fspoettel committed Jun 20, 2024
1 parent b0995f8 commit d2be008
Show file tree
Hide file tree
Showing 13 changed files with 155 additions and 72 deletions.
2 changes: 2 additions & 0 deletions src/components/ui/button.module.css
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@
user-select: none;
cursor: pointer;
align-items: center;
justify-content: center;
min-width: 5ch;

&[disabled] {
opacity: 0.3;
Expand Down
34 changes: 30 additions & 4 deletions src/components/ui/toast.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,20 @@
import clsx from "clsx";
import { CheckCircle, CircleAlert } from "lucide-react";
import { createContext, useCallback, useContext, useState } from "react";
import {
createContext,
useCallback,
useContext,
useEffect,
useState,
} from "react";

import css from "./toast.module.css";

type Toast = {
children: React.ReactNode;
children:
| React.ReactNode
| ((props: { handleClose: () => void }) => React.ReactNode);
displayTime?: number;
variant?: "success" | "error";
};

Expand All @@ -18,7 +27,20 @@ export function ToastProvider({ children }: { children: React.ReactNode }) {

const showToast = useCallback((value: Toast) => {
setToast(value);
setTimeout(() => setToast(undefined), 3000);
}, []);

useEffect(() => {
if (!toast?.displayTime) return;

const timer = setTimeout(() => {
setToast(undefined);
}, toast.displayTime);

return () => clearTimeout(timer);
}, [toast]);

const handleClose = useCallback(() => {
setToast(undefined);
}, []);

return (
Expand All @@ -33,7 +55,11 @@ export function ToastProvider({ children }: { children: React.ReactNode }) {
<CheckCircle className={css["icon"]} />
)}
{toast.variant === "error" && <CircleAlert className={css["icon"]} />}
<div>{toast.children}</div>
<div>
{typeof toast.children === "function"
? toast.children({ handleClose })
: toast.children}
</div>
</div>
)}
</ToastContext.Provider>
Expand Down
2 changes: 2 additions & 0 deletions src/pages/browse/deck-collection/deck-collection-import.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -35,13 +35,15 @@ export function DeckCollectionImport() {

showToast({
children: "Successfully imported deck.",
displayTime: 3000,
variant: "success",
});

setOpen(false);
} catch (err) {
showToast({
children: `Error: ${err instanceof Error ? err.message : "Unknown error."}`,
displayTime: 3000,
variant: "error",
});
} finally {
Expand Down
103 changes: 53 additions & 50 deletions src/pages/choose-investigator/choose-investigator.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { useCallback, useEffect, useState } from "react";
import { Link } from "wouter";

import { CardList } from "@/components/card-list/card-list";
import { CardModalProvider } from "@/components/card-modal/card-modal-context";
import { Filters } from "@/components/filters/filters";
import { Masthead } from "@/components/masthead";
import { Button } from "@/components/ui/button";
Expand Down Expand Up @@ -45,57 +46,59 @@ function DeckCreateChooseInvestigator() {
if (activeListId !== "create_deck") return null;

return (
<div
className={clsx(css["layout"], filtersOpen && css["filters-open"])}
onPointerDown={onContentClick}
>
<Masthead className={css["masthead"]}>
<Button onClick={goBack} variant="bare">
<ChevronLeft /> Back
</Button>
</Masthead>
<main className={css["content"]}>
<header className={css["header"]}>
<h1 className={css["title"]}>Choose investigator</h1>
</header>
<CardList
renderListCardAction={(card) => (
<Link asChild to={`/deck/create/${card.code}`}>
<Button as="a" size="lg" variant="bare">
<CirclePlusIcon />
</Button>
</Link>
)}
renderListCardExtra={({ code }) => {
const resolved = cardResolver(code);
const signatures = resolved?.relations?.requiredCards;
if (!signatures?.length) return null;

return (
<ul className={css["signatures"]}>
{signatures.map(({ card }) => (
<SignatureLink card={card} key={card.code} />
))}
</ul>
);
}}
slotRight={
<Button
className={css["toggle-filters"]}
onClick={() => onToggleFilters(true)}
>
<i className="icon-filter" />
</Button>
}
/>
</main>
<nav
className={css["filters"]}
data-state={filtersOpen ? "open" : "closed"}
<CardModalProvider>
<div
className={clsx(css["layout"], filtersOpen && css["filters-open"])}
onPointerDown={onContentClick}
>
<Filters />
</nav>
</div>
<Masthead className={css["masthead"]}>
<Button onClick={goBack} variant="bare">
<ChevronLeft /> Back
</Button>
</Masthead>
<main className={css["content"]}>
<header className={css["header"]}>
<h1 className={css["title"]}>Choose investigator</h1>
</header>
<CardList
renderListCardAction={(card) => (
<Link asChild to={`/deck/create/${card.code}`}>
<Button as="a" size="lg" variant="bare">
<CirclePlusIcon />
</Button>
</Link>
)}
renderListCardExtra={({ code }) => {
const resolved = cardResolver(code);
const signatures = resolved?.relations?.requiredCards;
if (!signatures?.length) return null;

return (
<ul className={css["signatures"]}>
{signatures.map(({ card }) => (
<SignatureLink card={card} key={card.code} />
))}
</ul>
);
}}
slotRight={
<Button
className={css["toggle-filters"]}
onClick={() => onToggleFilters(true)}
>
<i className="icon-filter" />
</Button>
}
/>
</main>
<nav
className={css["filters"]}
data-state={filtersOpen ? "open" : "closed"}
>
<Filters />
</nav>
</div>
</CardModalProvider>
);
}

Expand Down
6 changes: 5 additions & 1 deletion src/pages/deck-create/deck-create-editor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,11 @@ export function DeckCreateEditor() {
const handleDeckCreate = () => {
const id = createDeck();
navigate(`/deck/edit/${id}`, { replace: true });
toast({ children: "Deck created successfully.", variant: "success" });
toast({
children: "Deck created successfully.",
displayTime: 3000,
variant: "success",
});
};

const tabooSets = useStore(selectTabooSetSelectOptions);
Expand Down
38 changes: 38 additions & 0 deletions src/pages/deck-edit/deck-edit.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { Undo } from "lucide-react";
import { useEffect, useMemo, useState } from "react";
import { Redirect, useParams } from "wouter";

Expand All @@ -6,6 +7,8 @@ import { CardList } from "@/components/card-list/card-list";
import { CardModalProvider } from "@/components/card-modal/card-modal-context";
import { DecklistValidation } from "@/components/decklist/decklist-validation";
import { Filters } from "@/components/filters/filters";
import { Button } from "@/components/ui/button";
import { useToast } from "@/components/ui/toast";
import { useStore } from "@/store";
import type { DisplayDeck } from "@/store/lib/deck-grouping";
import {
Expand All @@ -22,13 +25,48 @@ import { ShowUnusableCardsToggle } from "./show-unusable-cards-toggle";
function DeckEdit() {
const { id } = useParams<{ id: string }>();

const showToast = useToast();
const activeListId = useStore((state) => state.activeList);
const resetFilters = useStore((state) => state.resetFilters);
const setActiveList = useStore((state) => state.setActiveList);
const discardEdits = useStore((state) => state.discardEdits);
const deck = useStore((state) => selectActiveDeckById(state, id, true));
const changes = useStore((state) => state.deckEdits[id]);

useEffect(() => {
if (changes) {
showToast({
children({ handleClose }) {
return (
<>
Unsaved changes were restored.
<div>
<Button onClick={handleClose} size="sm">
OK
</Button>
<Button
onClick={() => {
discardEdits(id);
handleClose();
}}
size="sm"
>
<Undo />
Revert
</Button>
</div>
</>
);
},
variant: "success",
});
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [discardEdits, id, showToast]);

useEffect(() => {
setActiveList("editor_player");

return () => {
resetFilters();
};
Expand Down
17 changes: 8 additions & 9 deletions src/pages/deck-edit/editor/editor-actions.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,31 +21,30 @@ export function EditorActions({ deck }: Props) {
deck.cards.investigator.card.faction_code,
);

const hasEdits = useStore((state) => !!state.deckEdits[deck.id]);
const discardEdits = useStore((state) => state.discardEdits);
const saveDeck = useStore((state) => state.saveDeck);

const handleSave = useCallback(() => {
const id = saveDeck(deck.id);

navigate(`/deck/view/${id}`, {
state: { confirm: false },
});
navigate(`/deck/view/${id}`);

showToast({
children: "Deck saved successfully.",
displayTime: 3000,
variant: "success",
});
}, [saveDeck, navigate, showToast, deck.id]);

const handleDiscard = useCallback(() => {
const confirmed = window.confirm(
"Are you sure you want to discard your changes?",
);
const confirmed =
!hasEdits ||
window.confirm("Are you sure you want to discard your changes?");
if (confirmed) {
discardEdits(deck.id);
navigate(`/deck/view/${deck.id}`);
}
}, [discardEdits, navigate, deck.id]);
}, [discardEdits, navigate, deck.id, hasEdits]);

return (
<div className={css["actions"]} style={cssVariables}>
Expand All @@ -54,7 +53,7 @@ export function EditorActions({ deck }: Props) {
Save
</Button>
<Button onClick={handleDiscard} variant="bare">
Cancel edits
Discard edits
</Button>
</div>
);
Expand Down
4 changes: 4 additions & 0 deletions src/pages/settings/card-data-sync.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ export function CardDataSync() {
if (synced)
toast({
children: "Card data was synced successfully.",
displayTime: 3000,
variant: "success",
});
}, [synced, toast]);
Expand All @@ -49,11 +50,13 @@ export function CardDataSync() {
if (res) {
toast({
children: "Persistence enabled successfully.",
displayTime: 3000,
variant: "success",
});
} else {
toast({
children: "Persistence could not be enabled.",
displayTime: 3000,
variant: "error",
});
}
Expand All @@ -62,6 +65,7 @@ export function CardDataSync() {
console.error(err);
toast({
children: "Persistence could not be enabled (see browser console).",
displayTime: 3000,
variant: "error",
});
});
Expand Down
8 changes: 6 additions & 2 deletions src/pages/settings/settings.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,11 @@ function Settings() {
evt.preventDefault();
if (evt.target instanceof HTMLFormElement) {
updateSettings(new FormData(evt.target));
toast({ children: "Settings saved successfully.", variant: "success" });
toast({
children: "Settings saved successfully.",
displayTime: 3000,
variant: "success",
});
}
},
[updateSettings, toast],
Expand All @@ -52,7 +56,7 @@ function Settings() {
</div>
</header>
<div className={css["settings-container"]}>
<Link to="/about">
<Link asChild to="/about">
<Button as="a">
<Info />
About this site
Expand Down
8 changes: 2 additions & 6 deletions src/store/slices/shared.ts
Original file line number Diff line number Diff line change
Expand Up @@ -149,14 +149,10 @@ export const createSharedSlice: StateCreator<
const state = get();

const edits = state.deckEdits[deckId];

if (!edits) {
console.warn("Tried to save deck but not in edit mode.");
return;
}
if (!edits) return deckId;

const deck = state.data.decks[deckId];
if (!deck) return;
if (!deck) return deckId;

const nextDeck = applyDeckEdits(deck, edits, state.metadata, true);
nextDeck.date_update = new Date().toISOString();
Expand Down
Loading

0 comments on commit d2be008

Please sign in to comment.