Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Use redux toolkit #8

Draft
wants to merge 1 commit into
base: main
Choose a base branch
from
Draft
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
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,11 +21,13 @@
"@fortawesome/free-regular-svg-icons": "^6.5.2",
"@fortawesome/free-solid-svg-icons": "^6.5.2",
"@fortawesome/react-fontawesome": "^0.2.2",
"@reduxjs/toolkit": "^2.2.7",
"@sentry/cli": "^2.32.1",
"@sentry/react": "^8.15.0",
"fuzzy": "^0.1.3",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"react-redux": "^9.1.2",
"styled-components": "^6.1.11"
},
"devDependencies": {
Expand Down
2 changes: 1 addition & 1 deletion src/context/universe/CardUtil.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { Universe } from '../../store/universeSlice';
import { Card } from './Card';
import { Universe } from './Universe';

export default class CardUtil {
static getCardName(universe: Universe, id: Card['id']): string | undefined {
Expand Down
12 changes: 6 additions & 6 deletions src/context/universe/Deck.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,15 @@ import { Card } from './Card';
import { Group } from './Group';

export interface Deck {
readonly id: string;
readonly items: readonly DeckItem[];
id: string;
items: DeckItem[];
}
export type DeckItem =
| {
readonly type: 'card';
readonly cardId: Card['id'];
type: 'card';
cardId: Card['id'];
}
| {
readonly type: 'group';
readonly groupId: Group['id'];
type: 'group';
groupId: Group['id'];
};
4 changes: 2 additions & 2 deletions src/context/universe/Group.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { Card } from './Card';

export interface Group {
readonly id: string;
readonly cardIds: ReadonlySet<Card['id']>;
id: string;
cardIds: Set<Card['id']>;
}
1 change: 1 addition & 0 deletions src/context/universe/Universe.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { Card } from './Card';
import { Deck } from './Deck';
import { Group } from './Group';

/** @deprecated moving to redux */
export interface Universe {
readonly decks: readonly Deck[];
readonly cards: readonly Card[];
Expand Down
93 changes: 93 additions & 0 deletions src/store/cardReducers.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
import { PayloadAction } from '@reduxjs/toolkit';
import { Deck, DeckItem } from '../context/universe/Deck';
import { Universe } from './universeSlice';

export function createCards(
state: Universe,
{
payload,
}: PayloadAction<{
targetDeckId: Deck['id'];
targetIndex: number;
names: string[];
}>,
): void {
const newCards = payload.names.map((name) => ({
id: crypto.randomUUID(),
name,
}));

const deck = state.decks.find((deck) => deck.id === payload.targetDeckId);

if (!deck)
throw new Error(
`Target deck ${payload.targetDeckId} not found when creating cards`,
);

deck.items.splice(
payload.targetIndex,
0,
...newCards.map(
(card): DeckItem => ({
type: 'card',
cardId: card.id,
}),
),
);

state.cards.push(...newCards);
}

export function destroyCards(
state: Universe,
{
payload,
}: PayloadAction<{
cardIds: string[];
}>,
): void {
const cardIds = new Set(payload.cardIds);

for (const id of cardIds) {
const card = state.cards.find((card) => card.id === id);

if (!card)
throw new Error(`Card ${id} not found when destroying cards`);

cardIds.add(id);
}

// Remove all the cards from the global card list
state.cards = state.cards.filter((card) => !cardIds.has(card.id));

// Remove the cards from the groups. If removed from a group, instances of that group need to be removed from decks
const groupItemsRemovedById: Record<string, number> = {};
const emptyGroupIds = new Set<string>();
for (const group of state.groups) {
const toRemove = group.cardIds.intersection(cardIds);

if (toRemove.size > 0) {
group.cardIds = group.cardIds.difference(toRemove);
groupItemsRemovedById[group.id] = toRemove.size;
if (group.cardIds.size === 0) {
emptyGroupIds.add(group.id);
}
}
}
state.groups = state.groups.filter((group) => !emptyGroupIds.has(group.id));

// Remove the cards from the decks, and remove instances of groups that have also had cards removed
for (const deck of state.decks) {
deck.items = deck.items.filter((c) => {
if (c.type === 'card' && cardIds.has(c.cardId)) return false;
if (c.type === 'group') {
if (groupItemsRemovedById[c.groupId] > 0) {
groupItemsRemovedById[c.groupId]--;
return false;
}
}

return true;
});
}
}
145 changes: 145 additions & 0 deletions src/store/deckReducers.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
import { PayloadAction } from '@reduxjs/toolkit';
import { Card } from '../context/universe/Card';
import CardUtil from '../context/universe/CardUtil';
import { Deck, DeckItem } from '../context/universe/Deck';
import { Group } from '../context/universe/Group';
import { Universe } from './universeSlice';

export function createDeck(
state: Universe,
{
payload: { id },
}: PayloadAction<{
id: Deck['id'];
}>,
): void {
if (state.decks.some((deck) => deck.id === id)) {
throw new Error(`Deck ${id} already exists`);
}

state.decks.push({
id,
items: [],
});
}

export function moveCard(
state: Universe,
{
payload: { fromDeckId, fromIndex, toDeckId, toIndex, count },
}: PayloadAction<{
fromDeckId: Deck['id'];
fromIndex: number;
toDeckId: Deck['id'];
toIndex: number;
count: number;
}>,
): void {
const fromDeck = state.decks.find((deck) => deck.id === fromDeckId);

if (!fromDeck) {
throw new Error(`Deck ${fromDeckId} not found when moving card`);
}

const toDeck = state.decks.find((deck) => deck.id === toDeckId);

if (!toDeck) {
throw new Error(`Deck ${toDeckId} not found when moving card`);
}

const cards = fromDeck.items.splice(fromIndex, count);

toDeck.items.splice(toIndex, 0, ...cards);
}

export function shuffleDeck(
state: Universe,
{
payload: { deckId },
}: PayloadAction<{
deckId: Deck['id'];
}>,
): void {
const deck = state.decks.find((deck) => deck.id === deckId);

if (!deck) {
throw new Error(`Deck ${deckId} not found when shuffling`);
}

const uniqueCardNames = new Set(
deck.items.flatMap((item) => {
if (item.type === 'card')
return CardUtil.getCardName(state, item.cardId);

const group = state.groups.find(
(group) => group.id === item.groupId,
)!;
return Array.from(group.cardIds).map((cardId) =>
CardUtil.getCardName(state, cardId),
);
}),
);

if (uniqueCardNames.size <= 0) {
return;
}

const numberOfItemsFromEachGroup: Record<string, number> = {};
for (const item of deck.items) {
if (item.type === 'group') {
numberOfItemsFromEachGroup[item.groupId] =
(numberOfItemsFromEachGroup[item.groupId] ?? 0) + 1;
}
}

const allGroupsInThisDeckAreFullyInThisDeck = Array.from(
Object.entries(numberOfItemsFromEachGroup),
).every(([groupId, count]) => {
const group = state.groups.find((group) => group.id === groupId);

if (!group) return false;

if (count === group.cardIds.size) return true;

return false;
});

if (!allGroupsInThisDeckAreFullyInThisDeck) {
throw new Error(
'Cannot shuffle deck with incomplete groups. Some items in this deck come from shuffle groups that have cards elsewhere, entanglement is not supported',
);
}

const groupsToDelete = Object.keys(numberOfItemsFromEachGroup);

const cardsInNewGroup: Card['id'][] = [];

for (const item of deck.items) {
if (item.type === 'card') {
cardsInNewGroup.push(item.cardId);
}
}

for (const groupId of groupsToDelete) {
const group = state.groups.find((group) => group.id === groupId)!;

cardsInNewGroup.push(...group.cardIds);
}

const newGroup: Group = {
id: crypto.randomUUID(),
cardIds: new Set(cardsInNewGroup),
};

state.groups = [
...state.groups.filter((group) => !groupsToDelete.includes(group.id)),
newGroup,
];

deck.items = cardsInNewGroup.map(
(): DeckItem => ({
type: 'group',
groupId: newGroup.id,
}),
);
}
Loading