Yuki Takei 7 months ago
parent
commit
3fc4ce1758

+ 3 - 16
apps/app/src/pages/admin/_shared/AdminPageFrame.tsx

@@ -1,29 +1,16 @@
-import type { ReactNode, JSX } from 'react';
+import type {, JSX } from 'react';
 import React from 'react';
 
 import dynamic from 'next/dynamic';
 import Head from 'next/head';
-import type { Container } from 'unstated';
+
 import { Provider } from 'unstated';
+import { AdminPageFrameProps } from './types';
 
 // Dynamic imports to avoid SSR issues with admin-only components
 const AdminLayout = dynamic(() => import('~/components/Layout/AdminLayout'), { ssr: false });
 const ForbiddenPage = dynamic(() => import('~/client/components/Admin/ForbiddenPage').then(mod => mod.ForbiddenPage), { ssr: false });
 
-export type AnyContainer = Container<Record<string, unknown>>;
-
-export interface AdminPageFrameProps {
-  /** Page <title> value (after generateCustomTitle) */
-  title: string;
-  /** Visible heading shown in AdminLayout header */
-  componentTitle?: string;
-  /** Access control flag */
-  isAccessDeniedForNonAdminUser: boolean;
-  /** Optional injected unstated containers */
-  containers?: AnyContainer[];
-  children?: ReactNode;
-}
-
 /**
  * Admin page frame that centralizes:
  *  - Forbidden guard

+ 45 - 0
apps/app/src/pages/admin/_shared/admin-ssr.ts

@@ -0,0 +1,45 @@
+import type { GetServerSideProps, GetServerSidePropsContext, GetServerSidePropsResult } from 'next';
+
+import type { CommonInitialProps, CommonEachProps } from '../../common-props';
+import { getServerSideCommonInitialProps, getServerSideCommonEachProps, getServerSideI18nProps } from '../../common-props';
+import { mergeGetServerSidePropsResults } from '../../utils/server-side-props';
+
+/** Base admin page props (allowed or forbidden). */
+export type AdminCommonProps = CommonInitialProps & CommonEachProps & {
+  isAccessDeniedForNonAdminUser: boolean;
+};
+
+/**
+ * Build common admin SSR props (merges common initial/each/i18n and computes admin flag).
+ * Returns redirect / notFound as-is.
+ */
+export const getServerSideAdminCommonProps: GetServerSideProps<AdminCommonProps> = async(context: GetServerSidePropsContext) => {
+  //
+  // STAGE 1
+  //
+
+  const commonEachPropsResult = await getServerSideCommonEachProps(context);
+  // Handle early return cases (redirect/notFound)
+  if ('redirect' in commonEachPropsResult || 'notFound' in commonEachPropsResult) {
+    return commonEachPropsResult;
+  }
+  const commonEachProps = await commonEachPropsResult.props;
+  const { currentUser } = commonEachProps;
+
+  const isAccessDeniedForNonAdminUser = (currentUser == null || !currentUser.admin);
+
+  //
+  // STAGE 2
+  //
+  const [
+    commonInitialResult,
+    i18nResult,
+  ] = await Promise.all([
+    getServerSideCommonInitialProps(context),
+    getServerSideI18nProps(context, ['admin']),
+  ]);
+
+  return mergeGetServerSidePropsResults(commonInitialResult,
+    mergeGetServerSidePropsResults(commonEachPropsResult,
+      mergeGetServerSidePropsResults(i18nResult, { props: { isAccessDeniedForNonAdminUser } })));
+};

+ 44 - 0
apps/app/src/pages/admin/_shared/createAdminPageLayout.tsx

@@ -0,0 +1,44 @@
+import React, { useMemo } from 'react';
+import type { ReactElement, ReactNode } from 'react';
+
+import { useTranslation } from 'next-i18next';
+
+import { useCustomTitle } from '~/pages/utils/page-title-customization';
+
+import { AdminPageFrame } from './AdminPageFrame';
+import type { AdminCommonProps } from './admin-ssr';
+import type { AnyUnstatedContainer } from './types';
+import { useAdminContainers } from './useAdminContainers';
+
+export interface AdminLayoutOptions<P extends AdminCommonProps> {
+  title: string | ((props: P, t: (k: string) => string) => string);
+  containerFactories?: Array<() => Promise<AnyUnstatedContainer>>;
+}
+
+export function createAdminPageLayout<P extends AdminCommonProps>(options: AdminLayoutOptions<P>) {
+  return function getLayout(page: ReactElement<P>): ReactNode {
+    const Wrapper: React.FC = () => {
+      const { t } = useTranslation('admin');
+
+      const rawTitle = typeof options.title === 'function' ? options.title(page.props, t) : options.title;
+      const title = useCustomTitle(rawTitle);
+
+      const factories = useMemo(() => options.containerFactories ?? [], []);
+      const containers = useAdminContainers(factories);
+
+      return (
+        <AdminPageFrame
+          title={title}
+          componentTitle={rawTitle}
+          isAccessDeniedForNonAdminUser={page.props.isAccessDeniedForNonAdminUser}
+          containers={containers}
+        >
+          {page}
+        </AdminPageFrame>
+      );
+    };
+    return <Wrapper />;
+  };
+}
+
+export default createAdminPageLayout;

+ 17 - 0
apps/app/src/pages/admin/_shared/types.ts

@@ -0,0 +1,17 @@
+import type { ReactNode } from 'react';
+
+import type { Container } from 'unstated';
+
+export type AnyUnstatedContainer = Container<Record<string, unknown>>;
+
+export interface AdminPageFrameProps {
+  /** Page <title> value (after generateCustomTitle) */
+  title: string;
+  /** Visible heading shown in AdminLayout header */
+  componentTitle?: string;
+  /** Access control flag */
+  isAccessDeniedForNonAdminUser: boolean;
+  /** Optional injected unstated containers */
+  containers?: AnyUnstatedContainer[];
+  children?: ReactNode;
+}

+ 3 - 3
apps/app/src/pages/admin/_shared/useAdminContainers.ts

@@ -1,13 +1,13 @@
 import { useEffect, useState } from 'react';
 
-import type { AnyContainer } from './AdminPageFrame';
+import type { AnyUnstatedContainer } from './types';
 
 /**
  * Helper hook to dynamically load and instantiate unstated containers for admin pages.
  * Pass an array of async factory functions returning container instances.
  */
-export const useAdminContainers = (factories: Array<() => Promise<AnyContainer>>): AnyContainer[] => {
-  const [containers, setContainers] = useState<AnyContainer[]>([]);
+export const useAdminContainers = (factories: Array<() => Promise<AnyUnstatedContainer>>): AnyUnstatedContainer[] => {
+  const [containers, setContainers] = useState<AnyUnstatedContainer[]>([]);
 
   useEffect(() => {
     let canceled = false;

+ 11 - 55
apps/app/src/pages/admin/notification.page.tsx

@@ -1,72 +1,28 @@
-/* eslint-disable react/prop-types */
-import type { GetServerSideProps, GetServerSidePropsContext } from 'next';
-import { useTranslation } from 'next-i18next';
+import type { GetServerSideProps } from 'next';
 import dynamic from 'next/dynamic';
 
-import { useCustomTitle } from '~/pages/utils/page-title-customization';
-
 import type { NextPageWithLayout } from '../_app.page';
-import type { CommonInitialProps, CommonEachProps } from '../common-props';
-import { getServerSideCommonInitialProps, getServerSideCommonEachProps, getServerSideI18nProps } from '../common-props';
-import { mergeGetServerSidePropsResults } from '../utils/server-side-props';
 
-import { AdminPageFrame } from './_shared/AdminPageFrame';
-import { useAdminContainers } from './_shared/useAdminContainers';
+import type { AdminCommonProps } from './_shared/admin-ssr';
+import { getServerSideAdminCommonProps } from './_shared/admin-ssr';
+import { createAdminPageLayout } from './_shared/createAdminPageLayout';
 
 const NotificationSetting = dynamic(() => import('~/client/components/Admin/Notification/NotificationSetting'), { ssr: false });
 
-type Props = CommonInitialProps & CommonEachProps & {
-  isAccessDeniedForNonAdminUser: boolean;
-};
+type Props = AdminCommonProps;
 
 const AdminExternalNotificationPage: NextPageWithLayout<Props> = () => <NotificationSetting />;
 
-// A wrapping component to legally use hooks while following getLayout pattern
-interface NotificationPageLayoutProps { page: JSX.Element & { props: Props } }
-const NotificationPageLayout: React.FC<NotificationPageLayoutProps> = (propsWrapper) => {
-  const page = propsWrapper.page;
-  const props = page.props;
-  const { t } = useTranslation('admin');
-  const componentTitle = t('external_notification.external_notification');
-  const title = useCustomTitle(componentTitle);
-  const containers = useAdminContainers([
+AdminExternalNotificationPage.getLayout = createAdminPageLayout<Props>({
+  title: (_p, t) => t('external_notification.external_notification'),
+  containerFactories: [
     async() => {
       const AdminNotificationContainer = (await import('~/client/services/AdminNotificationContainer')).default;
       return new AdminNotificationContainer();
     },
-  ]);
-
-  return (
-    <AdminPageFrame
-      title={title}
-      componentTitle={componentTitle}
-      isAccessDeniedForNonAdminUser={props.isAccessDeniedForNonAdminUser}
-      containers={containers}
-    >
-      {page}
-    </AdminPageFrame>
-  );
-};
-
-AdminExternalNotificationPage.getLayout = page => <NotificationPageLayout page={page} />;
-
-export const getServerSideProps: GetServerSideProps = async(context: GetServerSidePropsContext) => {
-  const [commonInitialResult, commonEachResult, i18nResult] = await Promise.all([
-    getServerSideCommonInitialProps(context),
-    getServerSideCommonEachProps(context),
-    getServerSideI18nProps(context, ['admin']),
-  ]);
-
-  const merged = mergeGetServerSidePropsResults(commonInitialResult,
-    mergeGetServerSidePropsResults(commonEachResult, i18nResult));
-
-  if ('props' in merged) {
-    const mergedProps = merged.props as CommonInitialProps & CommonEachProps & { isAccessDeniedForNonAdminUser?: boolean, currentUser?: { admin?: boolean } };
-    const currentUser = mergedProps.currentUser;
-    mergedProps.isAccessDeniedForNonAdminUser = currentUser == null ? true : !currentUser.admin;
-  }
+  ],
+});
 
-  return merged;
-};
+export const getServerSideProps: GetServerSideProps = getServerSideAdminCommonProps;
 
 export default AdminExternalNotificationPage;