Skip to content

Commit

Permalink
Merge pull request #19 from RaphaelEscrig/feat/blank-rounds
Browse files Browse the repository at this point in the history
Add blank rounds
  • Loading branch information
RaphaelEscrig authored Aug 26, 2024
2 parents 22be142 + 36b1b01 commit 8c598c3
Show file tree
Hide file tree
Showing 47 changed files with 2,396 additions and 44,364 deletions.
11,090 changes: 0 additions & 11,090 deletions data/2019_ranks.json

This file was deleted.

11,090 changes: 0 additions & 11,090 deletions data/2020_ranks.json

This file was deleted.

11,090 changes: 0 additions & 11,090 deletions data/2022_ranks.json

This file was deleted.

11,090 changes: 0 additions & 11,090 deletions data/2023_ranks.json

This file was deleted.

29 changes: 29 additions & 0 deletions src/app/cities/blank-rounds/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
/** MODELS */
import type { SpecialtyCode } from "@/modules/shared/domain/models";
/** PAGES */
import CitiesBlankRoundsPage from "@/modules/cities/react/pages/blank-rounds/blank-rounds.page";
/** UTILS */
import { isValidSpecialty } from "@/modules/shared/utils/specialty.util";

type Props = {
readonly searchParams?: {
readonly round?: string;
readonly rank?: string;
readonly specialty?: string;
};
};

const NextCitiesBlankRoundsPage = ({ searchParams }: Props) => {
const round = searchParams?.round ? parseInt(searchParams.round) : undefined;
const rank = searchParams?.rank ? parseInt(searchParams.rank) : undefined;
const specialty =
searchParams?.specialty && isValidSpecialty(searchParams.specialty)
? (searchParams.specialty as SpecialtyCode)
: undefined;

return (
<CitiesBlankRoundsPage rank={rank} round={round} specialty={specialty} />
);
};

export default NextCitiesBlankRoundsPage;
16 changes: 16 additions & 0 deletions src/app/specialties/blank-rounds/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
/** PAGES */
import SpecialtiesBlankRoundsPage from "@/modules/specialties/react/pages/blank-rounds/specialties-blank-rounds.page";

type Props = {
readonly searchParams?: {
readonly round?: string;
readonly rank?: string;
};
};

export default async function NextWhiteRoundsPage({ searchParams }: Props) {
const round = searchParams?.round ? parseInt(searchParams.round) : 1;
const rank = searchParams?.rank ? parseInt(searchParams.rank) : undefined;

return <SpecialtiesBlankRoundsPage rank={rank} round={round} />;
}
62 changes: 62 additions & 0 deletions src/modules/cities/core/domain/factories/city.factory.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
/** MODELS */
import type {
CityBlankRound,
CityRank,
CitySimulation,
} from "@/modules/cities/core/domain/models";
Expand Down Expand Up @@ -112,4 +113,65 @@ export class CityFactory {
},
];
}

public static createCitiesBlankRounds(): CityBlankRound.City[] {
return [
{
name: "Bordeaux",
bestRank: 89,
worstRank: 89,
places: 1,
assignedPlaces: 1,
remainingPlaces: 0,
},
{
name: "Reims",
bestRank: 452,
worstRank: 8974,
places: 7,
assignedPlaces: 5,
remainingPlaces: 2,
},
{
name: "Toulouse",
bestRank: 182,
worstRank: 182,
places: 1,
assignedPlaces: 1,
remainingPlaces: 0,
},
{
name: "Montpellier",
bestRank: 387,
worstRank: 523,
places: 3,
assignedPlaces: 3,
remainingPlaces: 0,
},
{
name: "Grenoble",
bestRank: null,
worstRank: null,
places: 2,
assignedPlaces: 0,
remainingPlaces: 2,
},
{
name: "Rennes",
bestRank: null,
worstRank: null,
places: 1,
assignedPlaces: 0,
remainingPlaces: 1,
},
{
name: "Strasbourg",
bestRank: 278,
worstRank: 897,
places: 3,
assignedPlaces: 3,
remainingPlaces: 0,
},
];
}
}
23 changes: 23 additions & 0 deletions src/modules/cities/core/domain/models/city-blank-round.model.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
export type Form = {
readonly round: string;
readonly rank: string;
readonly specialty: string;
};

export type FormErrors = Record<
keyof Pick<Form, "round" | "rank" | "specialty">,
string | null
>;

export type City = {
readonly name: string;
readonly bestRank: number | null;
readonly worstRank: number | null;
readonly places: number;
readonly assignedPlaces: number;
readonly remainingPlaces: number;
};

export type CityWithRankResult = City & {
readonly wouldHaveIt: boolean;
};
1 change: 1 addition & 0 deletions src/modules/cities/core/domain/models/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
/** MODELS */
export * as CityRank from "@/modules/cities/core/domain/models/city-rank.model";
export * as CitySimulation from "@/modules/cities/core/domain/models/city-simulation.model";
export * as CityBlankRound from "@/modules/cities/core/domain/models/city-blank-round.model";
7 changes: 7 additions & 0 deletions src/modules/cities/core/domain/ports/cities.port.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
/** MODELS */
import type {
CityBlankRound,
CityRank,
CitySimulation,
} from "@/modules/cities/core/domain/models";
Expand All @@ -17,4 +18,10 @@ export interface ICitiesGateway {
specialty: SpecialtyCode,
stage: number
): Promise<CitySimulation.City[]>;

findPerBlankRound(
rank: number,
specialty: SpecialtyCode,
round: number
): Promise<CityBlankRound.City[]>;
}
73 changes: 73 additions & 0 deletions src/modules/cities/core/forms/cities-blank-rounds.form.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
/** CONSTANTS */
import {
SPECIALTIES_BLANK_ROUND_MAX_ROUND,
SPECIALTIES_BLANK_ROUND_MIN_ROUND,
} from "@/modules/specialties/core/domain/constants";
import { SPECIALTIES } from "@/modules/shared/domain/constants";
/** IMMER */
import { produce } from "immer";
/** MODELS */
import type { CityBlankRound } from "@/modules/cities/core/domain/models";
/** UTILS */
import { castStringNumberToNumber } from "@/modules/shared/utils/numbers.util";
/** ZOD */
import { z } from "zod";

export class CitiesBlankRoundsForm {
public update<T extends keyof CityBlankRound.Form>(
state: CityBlankRound.Form,
key: T,
value: CityBlankRound.Form[T]
): CityBlankRound.Form {
return produce(state, (draft) => {
draft[key] = value;
});
}

public validate(
state: CityBlankRound.Form
): [boolean, CityBlankRound.FormErrors] {
const schema = z.object({
rank: z
.string()
.min(1)
.refine((rank) => castStringNumberToNumber(rank) > 0),
round: z
.string()
.min(1)
.refine(
(round) =>
castStringNumberToNumber(round) >=
SPECIALTIES_BLANK_ROUND_MIN_ROUND &&
castStringNumberToNumber(round) <= SPECIALTIES_BLANK_ROUND_MAX_ROUND
),
specialty: z.enum(
Array.from(new Set(SPECIALTIES.values())) as [string, ...string[]]
),
});

const res = schema.safeParse(state);

if (!res.success) {
const errors = res.error.flatten().fieldErrors;

return [
false,
{
rank: errors.rank ? "INVALID_RANK" : null,
round: errors.round ? "INVALID_ROUND" : null,
specialty: errors.specialty ? "INVALID_SPECIALTY" : null,
},
];
}

return [
true,
{
rank: null,
round: null,
specialty: null,
},
];
}
}
122 changes: 122 additions & 0 deletions src/modules/cities/core/forms/specs/cities-blank-rounds.form.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
/** FORMS */
import { CitiesBlankRoundsForm } from "../cities-blank-rounds.form";
/** MODELS */
import type { CityBlankRound } from "@/modules/cities/core/domain/models";

const emptyInitialState: CityBlankRound.Form = {
rank: "",
round: "",
specialty: "",
};
const completedState: CityBlankRound.Form = {
rank: "1 289",
round: "1",
specialty: "CMF",
};

describe("Cities rank form", () => {
const form = new CitiesBlankRoundsForm();

it.each([
{
key: "round" as keyof CityBlankRound.Form,
value: "1",
},
{
key: "rank" as keyof CityBlankRound.Form,
value: "1 289",
},
{
key: "rank" as keyof CityBlankRound.Form,
value: "47",
},
{
key: "specialty" as keyof CityBlankRound.Form,
value: "CMF",
},
{
key: "specialty" as keyof CityBlankRound.Form,
value: "",
},
])("should change the form when $key is $value", ({ key, value }) => {
const state = form.update(emptyInitialState, key, value);

expect(state[key]).toBe(value);
});

it.each([
{
key: "rank" as keyof CityBlankRound.Form,
value: "",
context: "is empty",
expected: "INVALID_RANK",
},
{
key: "rank" as keyof CityBlankRound.Form,
value: "not a number",
context: "is not a number",
expected: "INVALID_RANK",
},
{
key: "round" as keyof CityBlankRound.Form,
value: "99",
context: "is not in the range",
expected: "INVALID_ROUND",
},
{
key: "round" as keyof CityBlankRound.Form,
value: "0",
context: "is not in the range",
expected: "INVALID_ROUND",
},
{
key: "specialty" as keyof CityBlankRound.Form,
value: "",
context: "is empty",
expected: "INVALID_SPECIALTY",
},
{
key: "specialty" as keyof CityBlankRound.Form,
value: "CMFIJ",
context: "does not exist",
expected: "INVALID_SPECIALTY",
},
])(
"should not be submittable when $key $context",
({ key, value, context, expected }) => {
const [isValid, errors] = form.validate({
...completedState,
[key]: value,
});

expect(isValid).toBeFalsy();
expect(errors[key]).toBe(expected);
}
);

it.each([
{
rank: "100",
round: "1",
specialty: "CMF",
},
{
rank: "8 384",
round: "1",
specialty: "CMF",
},
])("should be valid", ({ rank, round, specialty }) => {
const [isValid, errors] = form.validate({
rank,
round,
specialty,
});

expect(isValid).toBeTruthy();
expect(errors).toEqual({
rank: null,
round: null,
specialty: null,
});
});
});
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
// /** FORMS */
/** FORMS */
import { CitiesSimulationForm } from "@/modules/cities/core/forms/cities-simulation.form";
// /** MODELS */
/** MODELS */
import type { CitySimulation } from "@/modules/cities/core/domain/models";

const emptyInitialState: CitySimulation.Form = {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { CityFactory } from "@/modules/cities/core/domain/factories/city.factory
/** MODELS */
import type { SpecialtyCode } from "@/modules/shared/domain/models";
import type {
CityBlankRound,
CityRank,
CitySimulation,
} from "@/modules/cities/core/domain/models";
Expand All @@ -25,4 +26,12 @@ export class InMemoryCitiesGateway implements ICitiesGateway {
): Promise<CitySimulation.City[]> {
return CityFactory.createCitiesSimulation();
}

public async findPerBlankRound(
_: number,
__: SpecialtyCode,
___: number
): Promise<CityBlankRound.City[]> {
return CityFactory.createCitiesBlankRounds();
}
}
Loading

0 comments on commit 8c598c3

Please sign in to comment.