Skip to content

Commit

Permalink
Merge pull request #11 from RaphaelEscrig/v1.1.0
Browse files Browse the repository at this point in the history
Add simulations pages
  • Loading branch information
RaphaelEscrig authored Aug 5, 2024
2 parents 33da5e0 + 3daa8d6 commit 8ae5c0b
Show file tree
Hide file tree
Showing 41 changed files with 2,383 additions and 10 deletions.
29 changes: 29 additions & 0 deletions src/app/cities/simulations/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 CitiesSimulationsPage from "@/modules/cities/react/pages/simulations/cities-simulations.page";
/** UTILS */
import { isValidSpecialty } from "@/modules/shared/utils/specialty.util";

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

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

return (
<CitiesSimulationsPage rank={rank} specialty={specialty} stage={stage} />
);
};

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

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

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

return <SpecialtiesSimulationsPage rank={rank} stage={stage} />;
}
66 changes: 65 additions & 1 deletion src/modules/cities/core/domain/factories/city.factory.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
/** MODELS */
import type { CityRank } from "@/modules/cities/core/domain/models";
import type {
CityRank,
CitySimulation,
} from "@/modules/cities/core/domain/models";

export class CityFactory {
public static createCities(): CityRank.City[] {
Expand Down Expand Up @@ -48,4 +51,65 @@ export class CityFactory {
},
];
}

public static createCitiesSimulation(): CitySimulation.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-simulation.model.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
export type Form = {
readonly stage: string;
readonly rank: string;
readonly specialty: string;
};

export type FormErrors = Record<
keyof Pick<Form, "stage" | "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;
};
3 changes: 2 additions & 1 deletion src/modules/cities/core/domain/models/index.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
/** MODELS */
export * as CityRank from "@/modules/cities/core/domain/models/city-rank";
export * as CityRank from "@/modules/cities/core/domain/models/city-rank.model";
export * as CitySimulation from "@/modules/cities/core/domain/models/city-simulation.model";
11 changes: 10 additions & 1 deletion src/modules/cities/core/domain/ports/cities.port.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
/** MODELS */
import type { CityRank } from "@/modules/cities/core/domain/models";
import type {
CityRank,
CitySimulation,
} from "@/modules/cities/core/domain/models";
import type { SpecialtyCode } from "@/modules/shared/domain/models";

export interface ICitiesGateway {
Expand All @@ -8,4 +11,10 @@ export interface ICitiesGateway {
specialty: SpecialtyCode,
year: number
): Promise<CityRank.City[]>;

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

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

public validate(
state: CitySimulation.Form
): [boolean, CitySimulation.FormErrors] {
const schema = z.object({
rank: z
.string()
.min(1)
.refine((rank) => castStringNumberToNumber(rank) > 0),
stage: z
.string()
.min(1)
.refine(
(stage) =>
castStringNumberToNumber(stage) >=
SPECIALTIES_SIMULATION_MIN_STAGE &&
castStringNumberToNumber(stage) <= SPECIALTIES_SIMULATION_MAX_STAGE
),
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,
stage: errors.stage ? "INVALID_STAGE" : null,
specialty: errors.specialty ? "INVALID_SPECIALTY" : null,
},
];
}

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

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

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

it.each([
{
key: "stage" as keyof CitySimulation.Form,
value: "1",
},
{
key: "rank" as keyof CitySimulation.Form,
value: "1 289",
},
{
key: "rank" as keyof CitySimulation.Form,
value: "47",
},
{
key: "specialty" as keyof CitySimulation.Form,
value: "CMF",
},
{
key: "specialty" as keyof CitySimulation.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 CitySimulation.Form,
value: "",
context: "is empty",
expected: "INVALID_RANK",
},
{
key: "rank" as keyof CitySimulation.Form,
value: "not a number",
context: "is not a number",
expected: "INVALID_RANK",
},
{
key: "stage" as keyof CitySimulation.Form,
value: "99",
context: "is not in the range",
expected: "INVALID_STAGE",
},
{
key: "stage" as keyof CitySimulation.Form,
value: "0",
context: "is not in the range",
expected: "INVALID_STAGE",
},
{
key: "specialty" as keyof CitySimulation.Form,
value: "",
context: "is empty",
expected: "INVALID_SPECIALTY",
},
{
key: "specialty" as keyof CitySimulation.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",
stage: "1",
specialty: "CMF",
},
{
rank: "8 384",
stage: "1",
specialty: "CMF",
},
])("should be valid", ({ rank, stage, specialty }) => {
const [isValid, errors] = form.validate({
rank,
stage,
specialty,
});

expect(isValid).toBeTruthy();
expect(errors).toEqual({
rank: null,
stage: null,
specialty: null,
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,10 @@
import { CityFactory } from "@/modules/cities/core/domain/factories/city.factory";
/** MODELS */
import type { SpecialtyCode } from "@/modules/shared/domain/models";
import type { CityRank } from "@/modules/cities/core/domain/models";
import type {
CityRank,
CitySimulation,
} from "@/modules/cities/core/domain/models";
/** PORTS */
import type { ICitiesGateway } from "@/modules/cities/core/domain/ports/cities.port";

Expand All @@ -14,4 +17,12 @@ export class InMemoryCitiesGateway implements ICitiesGateway {
): Promise<CityRank.City[]> {
return CityFactory.createCities();
}

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

0 comments on commit 8ae5c0b

Please sign in to comment.