feat: migrate to Next.js for Mini App and enhance project structure

- Replaced the previous webapp with a new Mini App built using Next.js, improving performance and maintainability.
- Updated the `.gitignore` to exclude Next.js build artifacts and node modules.
- Revised documentation in `AGENTS.md`, `README.md`, and `architecture.md` to reflect the new Mini App structure and technology stack.
- Enhanced Dockerfile to support the new build process for the Next.js application.
- Updated CI workflow to build and test the Next.js application.
- Added new configuration options for the Mini App, including `MINI_APP_SHORT_NAME` for improved deep linking.
- Refactored frontend testing setup to accommodate the new structure and testing framework.
- Removed legacy webapp files and dependencies to streamline the project.
This commit is contained in:
2026-03-03 16:04:08 +03:00
parent 2de5c1cb81
commit 16bf1a1043
148 changed files with 20240 additions and 7270 deletions

View File

@@ -0,0 +1,85 @@
/**
* Global application state (Zustand).
* Replaces the mutable state from webapp/js/dom.js.
*/
import { create } from "zustand";
import type { DutyWithUser, CalendarEvent } from "@/types";
import { getStartParamFromUrl } from "@/lib/launch-params";
export type CurrentView = "calendar" | "currentDuty";
export interface AppState {
currentMonth: Date;
lang: "ru" | "en";
duties: DutyWithUser[];
calendarEvents: CalendarEvent[];
loading: boolean;
error: string | null;
accessDenied: boolean;
/** Server detail from API 403 response; shown in AccessDenied component. */
accessDeniedDetail: string | null;
currentView: CurrentView;
selectedDay: string | null;
setCurrentMonth: (d: Date) => void;
nextMonth: () => void;
prevMonth: () => void;
setDuties: (d: DutyWithUser[]) => void;
setCalendarEvents: (e: CalendarEvent[]) => void;
setLoading: (v: boolean) => void;
setError: (msg: string | null) => void;
setAccessDenied: (v: boolean) => void;
setAccessDeniedDetail: (v: string | null) => void;
setLang: (v: "ru" | "en") => void;
setCurrentView: (v: CurrentView) => void;
setSelectedDay: (key: string | null) => void;
/** Batch multiple state updates into a single re-render. */
batchUpdate: (partial: Partial<Pick<AppState, "currentMonth" | "lang" | "duties" | "calendarEvents" | "loading" | "error" | "accessDenied" | "accessDeniedDetail" | "currentView" | "selectedDay">>) => void;
}
const now = new Date();
const initialMonth = new Date(now.getFullYear(), now.getMonth(), 1);
/** Initial view: currentDuty when opened via deep link (startParam=duty), else calendar. */
function getInitialView(): CurrentView {
if (typeof window === "undefined") return "calendar";
return getStartParamFromUrl() === "duty" ? "currentDuty" : "calendar";
}
export const useAppStore = create<AppState>((set) => ({
currentMonth: initialMonth,
lang: "en",
duties: [],
calendarEvents: [],
loading: false,
error: null,
accessDenied: false,
accessDeniedDetail: null,
currentView: getInitialView(),
selectedDay: null,
setCurrentMonth: (d) => set({ currentMonth: d }),
nextMonth: () =>
set((s) => {
const next = new Date(s.currentMonth);
next.setMonth(next.getMonth() + 1);
return { currentMonth: next };
}),
prevMonth: () =>
set((s) => {
const prev = new Date(s.currentMonth);
prev.setMonth(prev.getMonth() - 1);
return { currentMonth: prev };
}),
setDuties: (d) => set({ duties: d }),
setCalendarEvents: (e) => set({ calendarEvents: e }),
setLoading: (v) => set({ loading: v }),
setError: (msg) => set({ error: msg }),
setAccessDenied: (v) => set({ accessDenied: v }),
setAccessDeniedDetail: (v) => set({ accessDeniedDetail: v }),
setLang: (v) => set({ lang: v }),
setCurrentView: (v) => set({ currentView: v }),
setSelectedDay: (key) => set({ selectedDay: key }),
batchUpdate: (partial) => set(partial),
}));