feat/Update_Controllers_to_Allow_for_Premium_Membership

This commit is contained in:
cesnimda
2026-08-03 09:17:28 +02:00
parent de937d25dc
commit c3f4a57195
187 changed files with 26062 additions and 991 deletions
+34 -7
View File
@@ -33,10 +33,12 @@ import LandingPage from "./views/LandingPage";
import ForgotPasswordPage from "./views/ForgotPasswordPage";
import ResetPasswordPage from "./views/ResetPasswordPage";
import VerifyEmailPage from "./views/VerifyEmailPage";
import MicrosoftLegacyRelinkPage from "./views/MicrosoftLegacyRelinkPage";
import RouteErrorPage from "./views/RouteErrorPage";
import { api } from "./api";
import { resolveCaptureUrl } from "./captureUrl";
import { clearAuthClientState, setAuthUserKey } from "./auth";
import { AccountPlanProvider } from "./accountPlan";
import AppShell, { NavItem } from "./layout/AppShell";
import { getThemeModePref, setThemeModePref, ThemeModePref } from "./themePrefs";
@@ -60,6 +62,7 @@ const AdminUsersPage = lazy(() => import("./views/AdminUsersPage"));
const AdminSystemPage = lazy(() => import("./views/AdminSystemPage"));
const CorrespondenceInboxPage = lazy(() => import("./views/CorrespondenceInboxPage"));
const GmailReviewPage = lazy(() => import("./views/GmailReviewPage"));
const OperationsPage = lazy(() => import("./views/OperationsPage"));
const NotFoundPage = lazy(() => import("./views/NotFoundPage"));
type AuthConfig = { requireAuth: boolean };
@@ -73,6 +76,8 @@ type MeResponse = {
displayName?: string;
avatarImageDataUrl?: string;
roles?: string[];
plan?: "free" | "pro";
entitlements?: { ai?: boolean; proThemes?: boolean };
};
function breadcrumbsFor(path: string, t: (k: any) => string): string[] {
@@ -80,6 +85,7 @@ function breadcrumbsFor(path: string, t: (k: any) => string): string[] {
if (path.startsWith("/discover")) return [t("home"), "Discover jobs"];
if (path.startsWith("/jobs")) return [t("home"), t("jobApplications")];
if (path.startsWith("/reminders")) return [t("home"), t("reminders")];
if (path.startsWith("/operations")) return [t("home"), "Operations"];
if (path.startsWith("/kanban")) return [t("home"), t("kanbanBoard")];
if (path.startsWith("/companies")) return [t("home"), t("companies")];
if (path.startsWith("/correspondence/review")) return [t("home"), "Gmail review queue"];
@@ -99,6 +105,7 @@ function breadcrumbsFor(path: string, t: (k: any) => string): string[] {
function titleFor(path: string, t: (k: any) => string): string {
if (path === "/dashboard") return t("dashboard");
if (path.startsWith("/reminders")) return t("reminders");
if (path.startsWith("/operations")) return "Operations";
if (path.startsWith("/discover")) return "Discover jobs";
if (path.startsWith("/jobs")) return t("jobApplications");
if (path.startsWith("/kanban")) return t("kanbanBoard");
@@ -147,7 +154,8 @@ function Shell({ jobPageSize, setJobPageSize, jobColumns, setJobColumns, themeMo
const [isAdmin, setIsAdmin] = useState(false);
const [me, setMe] = useState<MeResponse | null>(null);
const [mobileDrawerOpen, setMobileDrawerOpen] = useState(false);
const [notifCount, setNotifCount] = useState(0);
const [reminderCount, setReminderCount] = useState(0);
const [notificationCount, setNotificationCount] = useState(0);
const path = location.pathname;
const isJobs = path.startsWith("/jobs");
@@ -195,12 +203,24 @@ function Shell({ jobPageSize, setJobPageSize, jobColumns, setJobColumns, themeMo
}, []);
useEffect(() => {
const load = () => {
api.get<any[]>("/jobapplications/reminders", { params: { upcomingDays: 14 } }).then((r) => setNotifCount(Array.isArray(r.data) ? r.data.length : 0)).catch(() => setNotifCount(0));
api.get<any[]>("/jobapplications/reminders", { params: { upcomingDays: 14 } }).then((r) => setReminderCount(Array.isArray(r.data) ? r.data.length : 0)).catch(() => setReminderCount(0));
};
load();
const id = window.setInterval(load, 60000);
return () => window.clearInterval(id);
}, []);
useEffect(() => {
const load = () => {
api.get<{ count: number }>("/notifications/unread-count").then((r) => setNotificationCount(Math.max(0, Number(r.data?.count) || 0))).catch(() => setNotificationCount(0));
};
load();
const id = window.setInterval(load, 60000);
window.addEventListener("notifications-changed", load);
return () => {
window.clearInterval(id);
window.removeEventListener("notifications-changed", load);
};
}, []);
useEffect(() => {
const onAuthChanged = () => {
setAuthResolved(false);
@@ -247,7 +267,7 @@ function Shell({ jobPageSize, setJobPageSize, jobColumns, setJobColumns, themeMo
{ to: "/dashboard", label: t("dashboard"), icon: <DashboardIcon fontSize="small" />, section: t("manage") },
{ to: "/jobs", label: t("jobApplications"), icon: <WorkOutlineIcon fontSize="small" />, section: t("manage") },
{ to: "/discover", label: "Discover jobs", icon: <SearchIcon fontSize="small" />, section: t("manage") },
{ to: "/reminders", label: t("reminders"), icon: <AlarmIcon fontSize="small" />, badgeCount: notifCount, section: t("manage") },
{ to: "/reminders", label: t("reminders"), icon: <AlarmIcon fontSize="small" />, badgeCount: reminderCount, section: t("manage") },
{ to: "/kanban", label: t("kanbanBoard"), icon: <ViewKanbanIcon fontSize="small" />, section: t("manage") },
{ to: "/companies", label: t("companies"), icon: <BusinessIcon fontSize="small" />, section: t("manage") },
{ to: "/correspondence", label: "Correspondence", icon: <MailOutlineIcon fontSize="small" />, section: t("manage") },
@@ -304,7 +324,11 @@ function Shell({ jobPageSize, setJobPageSize, jobColumns, setJobColumns, themeMo
);
return (
<>
<AccountPlanProvider value={{
plan: me?.plan === "pro" ? "pro" : "free",
canUseAi: Boolean(me?.entitlements?.ai),
canUseProThemes: Boolean(me?.entitlements?.proThemes),
}}>
<AppShell
pageTitle={pageTitle}
pageSubtitle={pageSubtitle}
@@ -316,8 +340,8 @@ function Shell({ jobPageSize, setJobPageSize, jobColumns, setJobColumns, themeMo
onToggleDrawer={setMobileDrawerOpen}
onNavigate={(to) => { setMobileDrawerOpen(false); navigate(to); }}
user={{ email: me?.email, userName: me?.userName, displayName: me?.displayName || fullName || undefined, avatarImageDataUrl: me?.avatarImageDataUrl, roleLabel: isAdmin ? t("superAdmin") : t("user") }}
notificationsCount={notifCount}
onOpenNotifications={() => navigate("/reminders")}
notificationsCount={notificationCount}
onOpenNotifications={() => navigate("/operations")}
onOpenSettings={() => navigate("/settings")}
onOpenProfile={() => navigate("/profile")}
onSignOut={() => { void api.post("/auth/logout").catch(() => undefined).finally(() => { clearAuthClientState(); navigate("/login"); }); }}
@@ -330,6 +354,7 @@ function Shell({ jobPageSize, setJobPageSize, jobColumns, setJobColumns, themeMo
<Route path="/discover" element={<JobDiscoveryPage />} />
<Route path="/jobs" element={<JobTable refreshToken={refreshToken} pageSize={jobPageSize} onPageSizeChange={setAndPersistPageSize} columns={jobColumns} onColumnsChange={setAndPersistColumns} mode="jobs" />} />
<Route path="/reminders" element={<RemindersView />} />
<Route path="/operations" element={<OperationsPage />} />
<Route path="/kanban" element={<KanbanBoard />} />
<Route path="/companies" element={<CompaniesTable />} />
<Route path="/correspondence" element={<CorrespondenceInboxPage />} />
@@ -354,7 +379,7 @@ function Shell({ jobPageSize, setJobPageSize, jobColumns, setJobColumns, themeMo
<AddJobModal open={addOpen} initialUrl={captureUrl} onClose={() => { setAddOpen(false); setCaptureUrl(undefined); }} onCreated={() => { setRefreshToken((t) => t + 1); }} />
<QuickCommandDialog open={quickOpen} onClose={() => setQuickOpen(false)} onNavigate={(to) => navigate(to)} onOpenAddJob={() => setAddOpen(true)} />
</Suspense>
</>
</AccountPlanProvider>
);
}
@@ -395,6 +420,8 @@ export default function App() {
{ path: "/forgot-password", element: <ForgotPasswordPage />, errorElement: <RouteErrorPage /> },
{ path: "/reset-password", element: <ResetPasswordPage />, errorElement: <RouteErrorPage /> },
{ path: "/verify-email", element: <VerifyEmailPage />, errorElement: <RouteErrorPage /> },
{ path: "/confirm-email-change", element: <VerifyEmailPage emailChange />, errorElement: <RouteErrorPage /> },
{ path: "/microsoft-legacy-relink", element: <MicrosoftLegacyRelinkPage />, errorElement: <RouteErrorPage /> },
{ path: "/cv/:slug", element: <PublicCvPage />, errorElement: <RouteErrorPage /> },
{ path: "/*", element: <Shell jobPageSize={jobPageSize} setJobPageSize={setJobPageSize} jobColumns={jobColumns} setJobColumns={setJobColumns} themeMode={themeMode} onThemeModeChange={onThemeModeChange} />, errorElement: <RouteErrorPage /> },
], { future: { v7_relativeSplatPath: true } }), [jobColumns, jobPageSize, themeMode]);