Skip to content
Merged
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
16 changes: 8 additions & 8 deletions apps/web/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,29 +27,29 @@ import { OrganizationEditorPage } from "./pages/OrganizationEditorPage";
import { SetupPage } from "./pages/SetupPage";
import { AnnualPlanEditorPage } from "./pages/AnnualPlanEditorPage";
import { NotFoundPage } from "./pages/NotFoundPage";
import { useI18n } from "./i18n";
function Protected() {
const location = useLocation();
const { t } = useI18n();
const me = useQuery({
queryKey: ["me"],
queryFn: () => api<any>("/api/auth/me"),
retry: false,
});
if (me.isLoading) return <div className="app-loading">Fonat betöltése…</div>;
if (me.isLoading)
return <div className="app-loading">{t("app.loading")}</div>;
if (me.error) {
if (!(me.error instanceof ApiError)) throw me.error;
const authenticationFailed = [401, 403].includes(me.error.status);
if (authenticationFailed)
return <Navigate to="/login" state={{ from: location }} replace />;
return (
<div className="app-loading connection-recovery" role="alert">
<span className="eyebrow">Kapcsolódás szünetel</span>
<h1>A Fonat most nem éri el a szervert</h1>
<p>
A munkamenetedet nem tekintjük lejártnak hálózati vagy szerverhiba
miatt. Ellenőrizd a kapcsolatot, majd próbáld újra.
</p>
<span className="eyebrow">{t("app.connectionEyebrow")}</span>
<h1>{t("app.connectionTitle")}</h1>
<p>{t("app.connectionBody")}</p>
<button disabled={me.isFetching} onClick={() => void me.refetch()}>
{me.isFetching ? "Újracsatlakozás…" : "Újracsatlakozás"}
{me.isFetching ? t("app.reconnecting") : t("app.reconnect")}
</button>
</div>
);
Expand Down
21 changes: 9 additions & 12 deletions apps/web/src/components/ConnectionStatus.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { useSyncExternalStore } from "react";
import { useI18n } from "../i18n";

function subscribeToConnectionStatus(onStoreChange: () => void) {
if (typeof window === "undefined") return () => undefined;
Expand All @@ -24,15 +25,13 @@ export function useOnlineStatus() {

export function ConnectionStatus() {
const online = useOnlineStatus();
const { t } = useI18n();
if (online) return null;

return (
<div className="connection-status" role="status" aria-live="polite">
<strong>Nincs hálózati kapcsolat.</strong>
<span>
A nem mentett változtatások a szerkesztőben maradnak. Mentéshez várd
meg, amíg helyreáll a kapcsolat.
</span>
<strong>{t("connection.offlineTitle")}</strong>
<span>{t("connection.offlineBody")}</span>
</div>
);
}
Expand All @@ -49,17 +48,15 @@ export function EditorSaveStatus({
saved,
}: EditorSaveStatusProps) {
const online = useOnlineStatus();
const { t } = useI18n();
const status = pending
? { label: "Mentés folyamatban…", tone: "pending" }
? { label: t("save.pending"), tone: "pending" }
: dirty && !online
? {
label: "Nincs kapcsolat — a módosítások még nincsenek mentve.",
tone: "warning",
}
? { label: t("save.offlineDirty"), tone: "warning" }
: dirty
? { label: "Nem mentett módosítások", tone: "dirty" }
? { label: t("save.dirty"), tone: "dirty" }
: saved
? { label: "Minden változtatás mentve", tone: "saved" }
? { label: t("save.saved"), tone: "saved" }
: null;

if (!status) return null;
Expand Down
6 changes: 4 additions & 2 deletions apps/web/src/components/ErrorBoundary.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import { Component, type ErrorInfo, type ReactNode } from "react";
import { translateCurrent } from "../i18n";

export class ErrorBoundary extends Component<
{ children: ReactNode },
{ error?: Error }
Expand All @@ -14,10 +16,10 @@ export class ErrorBoundary extends Component<
if (this.state.error)
return (
<div className="center-card">
<h1>Az oldal nem tölthető be</h1>
<h1>{translateCurrent("error.title")}</h1>
<p>{this.state.error.message}</p>
<a className="button" href="/">
Vissza a Today oldalra
{translateCurrent("error.back")}
</a>
</div>
);
Expand Down
68 changes: 37 additions & 31 deletions apps/web/src/components/Shell.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,49 +7,55 @@ import {
} from "react-router-dom";
import { useQueryClient } from "@tanstack/react-query";
import { post } from "../api";
import { useI18n, type TranslationKey } from "../i18n";
import { ConnectionStatus } from "./ConnectionStatus";
import { Logo } from "./Logo";

const nav = [
{
label: "Napi munka",
label: "shell.dailyWork",
items: [
["/", "Ma"],
["/timetable", "Órarend"],
["/lessons", "Óratervek"],
["/assignments", "Kiosztások"],
["/assessments", "Felmérések"],
["/insights", "Elemzések"],
["/", "shell.today"],
["/timetable", "shell.timetable"],
["/lessons", "shell.lessons"],
["/assignments", "shell.assignments"],
["/assessments", "shell.assessments"],
["/insights", "shell.insights"],
],
},
{
label: "Tartalom",
label: "shell.content",
items: [
["/library", "Könyvtár"],
["/exercises", "Gyakorlófeladatok"],
["/annual-plans", "Éves tervek"],
["/projects", "Projektek"],
["/library", "shell.library"],
["/exercises", "shell.exercises"],
["/annual-plans", "shell.annualPlans"],
["/projects", "shell.projects"],
],
},
{
label: "Munkatér",
label: "shell.workspace",
items: [
["/courses", "Kurzusok"],
["/groups", "Csoportok"],
["/learners", "Tanulók"],
["/locations", "Helyszínek"],
["/admin", "Admin"],
["/guide", "Útmutató"],
["/courses", "shell.courses"],
["/groups", "shell.groups"],
["/learners", "shell.learners"],
["/locations", "shell.locations"],
["/admin", "shell.admin"],
["/guide", "shell.guide"],
],
},
] satisfies Array<{ label: string; items: Array<[string, string]> }>;
] satisfies Array<{
label: TranslationKey;
items: Array<[string, TranslationKey]>;
}>;

const routeLabels: Record<string, string> = Object.fromEntries(
nav.flatMap((group) => group.items),
);
export function Shell() {
const location = useLocation();
const navigate = useNavigate();
const qc = useQueryClient();
const { t } = useI18n();
const routeLabels = Object.fromEntries(
nav.flatMap((group) => group.items.map(([route, key]) => [route, t(key)])),
);
const logout = async () => {
await post("/api/auth/logout", {});
qc.clear();
Expand All @@ -64,47 +70,47 @@ export function Shell() {
<nav>
{nav.map((group) => (
<div className="nav-group" key={group.label}>
<span>{group.label}</span>
<span>{t(group.label)}</span>
{group.items.map(([to, label]) => (
<NavLink
key={to}
to={to}
end={to === "/"}
className={({ isActive }) => (isActive ? "active" : "")}
>
{label}
{t(label)}
</NavLink>
))}
</div>
))}
</nav>
<div className="aside-footer">
<Link to="/join">Tanulói csatlakozás</Link>
<Link to="/join">{t("shell.studentJoin")}</Link>
<button className="ghost" onClick={logout}>
Kilépés
{t("shell.logout")}
</button>
</div>
</aside>
<main>
<header className="topbar">
<div>
<span className="eyebrow">Aktuális hely</span>
<span className="eyebrow">{t("shell.currentLocation")}</span>
<strong>
{location.pathname === "/"
? "Ma"
? t("shell.today")
: routeLabels[
Object.keys(routeLabels).find(
(route) =>
route !== "/" && location.pathname.startsWith(route),
) || ""
] || "Szerkesztés"}
] || t("shell.editing")}
</strong>
</div>
<Link
className="button secondary"
to="/presentation/lesson.demo-presentation"
>
Bemutató indítása
{t("shell.startPresentation")}
</Link>
</header>
<ConnectionStatus />
Expand Down
13 changes: 6 additions & 7 deletions apps/web/src/components/UnsavedChangesGuard.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
import { useCallback, useEffect, useRef, useState } from "react";
import { useBeforeUnload, useBlocker } from "react-router-dom";
import { useI18n } from "../i18n";

export function useUnsavedChanges(when: boolean) {
const { t } = useI18n();
const allowNavigationRef = useRef(false);
const stayButtonRef = useRef<HTMLButtonElement>(null);
const [pendingDiscard, setPendingDiscard] = useState<(() => void) | null>(
Expand Down Expand Up @@ -77,17 +79,14 @@ export function useUnsavedChanges(when: boolean) {
aria-labelledby="unsaved-dialog-title"
aria-describedby="unsaved-dialog-description"
>
<h2 id="unsaved-dialog-title">Nem mentett módosítások</h2>
<p id="unsaved-dialog-description">
Ha elhagyod ezt az oldalt, a még el nem mentett változtatások
elvesznek.
</p>
<h2 id="unsaved-dialog-title">{t("unsaved.title")}</h2>
<p id="unsaved-dialog-description">{t("unsaved.body")}</p>
<div className="row-actions">
<button ref={stayButtonRef} onClick={stay}>
Maradok és mentek
{t("unsaved.stay")}
</button>
<button className="danger secondary" onClick={discard}>
Módosítások elvetése
{t("unsaved.discard")}
</button>
</div>
</section>
Expand Down
26 changes: 26 additions & 0 deletions apps/web/src/i18n/core.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import hu from "./hu.json";
import en from "./en.json";

export const supportedLocales = ["hu", "en"] as const;
export type Locale = (typeof supportedLocales)[number];
export type TranslationKey = keyof typeof hu;
export type TranslationParams = Record<string, string | number>;

const catalogs: Record<Locale, Record<TranslationKey, string>> = { hu, en };

export function isLocale(value: unknown): value is Locale {
return supportedLocales.includes(value as Locale);
}

export function translate(
locale: Locale,
key: TranslationKey,
params: TranslationParams = {},
): string {
const template = catalogs[locale][key] ?? catalogs.hu[key] ?? key;
return Object.entries(params).reduce(
(value, [name, replacement]) =>
value.replaceAll(`{${name}}`, String(replacement)),
template,
);
}
Loading
Loading