Skip to content

Commit

Permalink
Use redux toolkit
Browse files Browse the repository at this point in the history
Slices are more better for reactivity than reacts useReducer.
  • Loading branch information
OldStarchy committed Aug 17, 2024
1 parent 0e44689 commit 5d77c1f
Show file tree
Hide file tree
Showing 14 changed files with 491 additions and 11 deletions.
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

0 comments on commit 5d77c1f

Please sign in to comment.