Skip to content

Commit

Permalink
Use olimanager schools
Browse files Browse the repository at this point in the history
  • Loading branch information
bortoz committed Jan 28, 2025
1 parent 38b84c0 commit 56473ca
Show file tree
Hide file tree
Showing 7 changed files with 242 additions and 33 deletions.
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@
"recharts": "^2.12",
"rehype-katex": "^7.0",
"remark-math": "^6.0",
"set-cookie-parser": "^2.7",
"shiki": "^2.1",
"swr": "^2.2",
"tailwindcss": "^3.3",
Expand Down
15 changes: 5 additions & 10 deletions src/app/(training)/(login)/signup/page-client.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,17 +13,12 @@ import {
SubmitButton,
UsernameField,
} from "@olinfo/react-components";
import {
type Contest,
getCities,
getInstitutes,
getProvinces,
getRegions,
} from "@olinfo/training-api";
import type { Contest } from "@olinfo/training-api";
import ReCaptchaWidget, { type ReCAPTCHA } from "react-google-recaptcha";

import { H2 } from "~/components/header";
import { LocationField } from "~/components/location-field";
import { getCities, getProvinces, getRegions, getSchools } from "~/lib/location";
import { useTheme } from "~/lib/theme";

import { signup } from "./actions";
Expand Down Expand Up @@ -53,7 +48,7 @@ export function PageClient({ contest, redirectUrl }: { contest: Contest; redirec
user.password,
user.name,
user.surname,
user.institute,
user.institute.trim(),
captchaRef.current?.getValue() ?? undefined,
redirectUrl,
);
Expand Down Expand Up @@ -93,7 +88,7 @@ export function PageClient({ contest, redirectUrl }: { contest: Contest; redirec
label={_(msg`Regione`)}
field="region"
placeholder={_(msg`Scegli la regione`)}
id="🇮"
id="Italy"
fetcher={getRegions}
optional
/>
Expand Down Expand Up @@ -123,7 +118,7 @@ export function PageClient({ contest, redirectUrl }: { contest: Contest; redirec
field="institute"
placeholder={_(msg`Scegli la scuola`)}
id={city}
fetcher={getInstitutes}
fetcher={getSchools}
optional
/>
)}
Expand Down
6 changes: 3 additions & 3 deletions src/app/(training)/user/[username]/edit/school/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,10 @@
import { Trans, msg } from "@lingui/macro";
import { useLingui } from "@lingui/react";
import { Form, SubmitButton } from "@olinfo/react-components";
import { getCities, getInstitutes, getProvinces, getRegions } from "@olinfo/training-api";

import { H2 } from "~/components/header";
import { LocationField } from "~/components/location-field";
import { getCities, getProvinces, getRegions, getSchools } from "~/lib/location";

import { changeSchool } from "./actions";

Expand Down Expand Up @@ -39,7 +39,7 @@ export default function Page({ params: { username } }: Props) {
label={_(msg`Regione`)}
field="region"
placeholder={_(msg`Scegli la regione`)}
id="🇮"
id="Italy"
fetcher={getRegions}
/>
{({ region }) => (
Expand All @@ -66,7 +66,7 @@ export default function Page({ params: { username } }: Props) {
field="institute"
placeholder={_(msg`Scegli la scuola`)}
id={city}
fetcher={getInstitutes}
fetcher={getSchools}
/>
)}
<SubmitButton>
Expand Down
10 changes: 5 additions & 5 deletions src/app/(training)/user/[username]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import { orderBy } from "lodash-es";

import { H1 } from "~/components/header";
import { loadLocale } from "~/lib/locale";
import { getSchool } from "~/lib/location";

type Props = {
params: { username: string };
Expand Down Expand Up @@ -56,6 +57,9 @@ export default async function Page({ params: { username } }: Props) {
const [_i18n, user, me] = await Promise.all([loadLocale(), getUser(username), getMe()]);
if (!user) notFound();

const schoolId = user.institute?.id;
const school = schoolId && (await getSchool(schoolId));

const { _ } = useLingui();

return (
Expand All @@ -79,11 +83,7 @@ export default async function Page({ params: { username } }: Props) {
<div>
{user.first_name} {user.last_name}
</div>
{user.institute && (
<div className="text-sm text-base-content/80">
{user.institute.name}, {user.institute.city}, {user.institute.region}
</div>
)}
{school && <div className="text-sm text-base-content/80">{school}</div>}
<div className="text-xl font-bold">
<Trans>{user.score} punti</Trans>
</div>
Expand Down
9 changes: 5 additions & 4 deletions src/components/location-field.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import { SelectField } from "@olinfo/react-components";
import type { Location } from "@olinfo/training-api";
import { sortBy } from "lodash-es";
import useSWR from "swr";

import type { Location } from "~/lib/location";

type Props = {
label: string;
field: string;
Expand All @@ -13,9 +14,9 @@ type Props = {
};

export function LocationField({ label, field, placeholder, id, fetcher, optional }: Props) {
const { data } = useSWR<Location[], Error, [string, string] | undefined>(
const { data, isLoading } = useSWR<Location[], Error, [string, string] | undefined>(
id ? [`locations/${field}`, id] : undefined,
([, id]) => fetcher(id),
([, id]) => fetcher(id.trim()),
{
revalidateIfStale: false,
revalidateOnFocus: false,
Expand All @@ -32,7 +33,7 @@ export function LocationField({ label, field, placeholder, id, fetcher, optional
field={field}
placeholder={placeholder}
options={options}
disabled={!id}
disabled={!id || isLoading}
optional={optional}
/>
);
Expand Down
212 changes: 212 additions & 0 deletions src/lib/location.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,212 @@
"use server";

import { parse } from "set-cookie-parser";
import { type ZodType, z } from "zod";

const OLIMANAGER_URL = "https://olimpiadi-scientifiche.it/graphql";

async function getCsrf() {
const res = await fetch(OLIMANAGER_URL, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
});
const cookies = parse(res.headers.getSetCookie());
const csrf = cookies.find((cookie) => cookie.name === "csrftoken")?.value;
if (!csrf) {
throw new Error("No CSRF token found");
}
return csrf;
}

async function query<T>(query: string, schema: ZodType<T, any, any>): Promise<T> {
const csrf = await getCsrf();
const res = await fetch(OLIMANAGER_URL, {
method: "POST",
headers: {
"Content-Type": "application/json",
"X-CSRFToken": csrf,
Cookie: `csrftoken=${csrf}`,
},
body: JSON.stringify({ query }),
});

const responseSchema = z.union([
z.object({ errors: z.array(z.object({ message: z.string() })) }),
z.object({ data: schema }),
]);

const data = responseSchema.parse(await res.json());
if ("errors" in data) {
throw new Error(data.errors[0].message);
}
return data.data as T;
}

const locationSchema = z.object({
id: z.coerce.string(),
name: z.string(),
});

export type Location = z.infer<typeof locationSchema>;

// biome-ignore lint/suspicious/useAwait: server actions must be async
export async function getRegions(_: string): Promise<Location[]> {
return [
{ id: "ABR", name: "Abruzzo" },
{ id: "BAS", name: "Basilicata" },
{ id: "CAL", name: "Calabria" },
{ id: "CAM", name: "Campania" },
{ id: "EMI", name: "Emilia-Romagna" },
{ id: "FRI", name: "Friuli-Venezia Giulia" },
{ id: "LAZ", name: "Lazio" },
{ id: "LIG", name: "Liguria" },
{ id: "LOM", name: "Lombardia" },
{ id: "MAR", name: "Marche" },
{ id: "MOL", name: "Molise" },
{ id: "PIE", name: "Piemonte" },
{ id: "PUG", name: "Puglia" },
{ id: "SAR", name: "Sardegna" },
{ id: "SIC", name: "Sicilia" },
{ id: "TOS", name: "Toscana" },
{ id: "TRE", name: "Trentino-Alto Adige" },
{ id: "UMB", name: "Umbria" },
{ id: "VAL", name: "Valle d'Aosta" },
{ id: "VEN", name: "Veneto" },
];
}

const provincesSchema = z.object({
provinces: z.object({
provinces: locationSchema.array(),
}),
});

export async function getProvinces(regionId: string): Promise<Location[]> {
const data = await query(
`{
provinces {
provinces(filters: {region: {id: {exact: "${regionId}"}}}) {
id
name
}
}
}`,
provincesSchema,
);
return data.provinces.provinces;
}

const citiesSchema = z.object({
cities: z.object({
cities: locationSchema.array(),
}),
});

export async function getCities(provinceId: string): Promise<Location[]> {
const data = await query(
`{
cities {
cities(filters: {province: {id: {exact: "${provinceId}"}}}) {
id
name
}
}
}`,
citiesSchema,
);
return data.cities.cities;
}

const schoolsSchema = z.object({
schools: z.object({
schools: z.object({
edges: z.array(
z.object({
node: locationSchema,
}),
),
}),
}),
});

export async function getSchools(cityId: string): Promise<Location[]> {
const data = await query(
`{
schools {
schools(filters: {location: {city: {id: {exact: "${cityId}"}}}}) {
edges {
node {
id
name
}
}
}
}
}`,
schoolsSchema,
);
return data.schools.schools.edges.map((edge) => edge.node);
}

const schoolSchema = z.object({
schools: z.object({
schools: z.object({
edges: z
.object({
node: z.object({
id: z.string(),
name: z.string(),
type: z.object({
name: z.string(),
}),
location: z.object({
city: z.object({
name: z.string(),
province: z.object({
region: z.object({
name: z.string(),
}),
}),
}),
}),
}),
})
.array(),
}),
}),
});

export async function getSchool(schoolId: string | number): Promise<string> {
const data = await query(
`{
schools {
schools(filters: {id: {exact: "${schoolId}"}}) {
edges {
node {
id
name
type {
name
}
location {
city {
name
province {
region {
name
}
}
}
}
}
}
}
}
}`,
schoolSchema,
);
const school = data.schools.schools.edges[0].node;
return `${school.type.name} ${school.name}, ${school.location.city.name}, ${school.location.city.province.region.name}`;
}
Loading

0 comments on commit 56473ca

Please sign in to comment.