diff --git a/src/app/(app)/layout.tsx b/src/app/(app)/layout.tsx index ec542c4..252c5ad 100644 --- a/src/app/(app)/layout.tsx +++ b/src/app/(app)/layout.tsx @@ -1,7 +1,7 @@ import type { ReactNode } from "react"; import { cookies } from "next/headers"; import { redirect } from "next/navigation"; -import { TOKEN_COOKIE_KEY } from "@/features/auth/model/constants"; +import { TOKEN_COOKIE_KEY } from "@/shared/config/authTokens"; interface AppLayoutProps { children: ReactNode; diff --git a/src/app/admin/page.tsx b/src/app/admin/page.tsx index f403aea..8cc65a2 100644 --- a/src/app/admin/page.tsx +++ b/src/app/admin/page.tsx @@ -1,712 +1,7 @@ 'use client'; -import { FormEvent, useEffect, useState } from 'react'; -import { adminApi, type SceneMediaAssetUploadResponse, type SoundMediaAssetUploadResponse } from '@/features/admin/api/adminApi'; -import type { AuthResponse } from '@/features/auth/types'; -import { copy } from '@/shared/i18n'; -import { Button } from '@/shared/ui'; - -const ADMIN_STORAGE_KEY = 'vr_admin_session'; - -type AdminView = 'scene' | 'sound'; - -type UploadResult = - | { type: 'scene'; payload: SceneMediaAssetUploadResponse } - | { type: 'sound'; payload: SoundMediaAssetUploadResponse }; - -type NavItem = { - id: AdminView; - title: string; - subtitle: string; - section: string; -}; - -const navItems: NavItem[] = [...copy.admin.navItems]; - -const readStoredSession = (): AuthResponse | null => { - if (typeof window === 'undefined') { - return null; - } - - const rawValue = window.localStorage.getItem(ADMIN_STORAGE_KEY); - if (!rawValue) { - return null; - } - - try { - return JSON.parse(rawValue) as AuthResponse; - } catch { - window.localStorage.removeItem(ADMIN_STORAGE_KEY); - return null; - } -}; - -const storeSession = (session: AuthResponse | null) => { - if (typeof window === 'undefined') { - return; - } - - if (!session) { - window.localStorage.removeItem(ADMIN_STORAGE_KEY); - return; - } - - window.localStorage.setItem(ADMIN_STORAGE_KEY, JSON.stringify(session)); -}; - -const fieldClassName = - 'h-11 w-full rounded-xl border border-slate-200 bg-white px-3 text-sm text-slate-900 outline-none transition placeholder:text-slate-400 focus:border-sky-400 focus:ring-2 focus:ring-sky-100'; - -const fileClassName = - 'block w-full rounded-xl border border-dashed border-slate-200 bg-slate-50 px-3 py-3 text-sm text-slate-600 file:mr-3 file:rounded-lg file:border-0 file:bg-slate-900 file:px-3 file:py-2 file:text-sm file:font-medium file:text-white hover:file:bg-slate-800'; - -const textareaClassName = - 'min-h-32 w-full rounded-xl border border-slate-200 bg-white px-3 py-3 text-sm text-slate-900 outline-none transition placeholder:text-slate-400 focus:border-sky-400 focus:ring-2 focus:ring-sky-100'; - -const getViewMeta = (view: AdminView) => { - return copy.admin.views[view]; -}; - -const formatResultSummary = (uploadResult: UploadResult | null) => { - if (!uploadResult) { - return copy.admin.inspector.noUploadSummary; - } - - if (uploadResult.type === 'scene') { - return copy.admin.messages.sceneSummary(uploadResult.payload.sceneId, uploadResult.payload.assetVersion); - } - - return copy.admin.messages.soundSummary(uploadResult.payload.presetId, uploadResult.payload.assetVersion); -}; +import { AdminConsoleWidget } from '@/widgets/admin-console'; export default function AdminPage() { - const [session, setSession] = useState(null); - const [activeView, setActiveView] = useState('scene'); - const [isDurationOverrideEnabled, setIsDurationOverrideEnabled] = useState(false); - const [loginId, setLoginId] = useState(copy.admin.defaultLoginId); - const [password, setPassword] = useState(copy.admin.defaultPassword); - const [loginError, setLoginError] = useState(null); - const [loginPending, setLoginPending] = useState(false); - const [scenePending, setScenePending] = useState(false); - const [soundPending, setSoundPending] = useState(false); - const [sceneMessage, setSceneMessage] = useState(null); - const [soundMessage, setSoundMessage] = useState(null); - const [uploadResult, setUploadResult] = useState(null); - - useEffect(() => { - setSession(readStoredSession()); - }, []); - - const handleLogin = async (event: FormEvent) => { - event.preventDefault(); - setLoginPending(true); - setLoginError(null); - - try { - const response = await adminApi.login({ - loginId: loginId.trim(), - password, - }); - - if (response.user?.grade !== 'ADMIN') { - throw new Error(copy.admin.messages.nonAdmin); - } - - setSession(response); - storeSession(response); - } catch (error) { - setLoginError(error instanceof Error ? error.message : copy.admin.messages.loginFailed); - } finally { - setLoginPending(false); - } - }; - - const handleLogout = () => { - setSession(null); - setActiveView('scene'); - setIsDurationOverrideEnabled(false); - setSceneMessage(null); - setSoundMessage(null); - setUploadResult(null); - storeSession(null); - }; - - const handleSceneUpload = async (event: FormEvent) => { - event.preventDefault(); - if (!session?.accessToken) { - setSceneMessage(copy.admin.messages.loginRequired); - return; - } - - setScenePending(true); - setSceneMessage(null); - - try { - const form = event.currentTarget; - const source = new FormData(form); - const sceneId = String(source.get('sceneId') ?? '').trim(); - - if (!sceneId) { - throw new Error(copy.admin.messages.sceneIdRequired); - } - - const formData = new FormData(); - const sourceImageFile = source.get('sourceImageFile'); - - if (sourceImageFile instanceof File && sourceImageFile.size > 0) { - formData.append('sourceImageFile', sourceImageFile); - } - - const blurDataUrl = String(source.get('blurDataUrl') ?? '').trim(); - if (blurDataUrl) { - formData.append('blurDataUrl', blurDataUrl); - } - - const response = await adminApi.uploadScene(sceneId, formData, session.accessToken); - setUploadResult({ type: 'scene', payload: response }); - setSceneMessage(copy.admin.messages.sceneUploadDone(sceneId)); - form.reset(); - } catch (error) { - setSceneMessage(error instanceof Error ? error.message : copy.admin.messages.sceneUploadFailed); - } finally { - setScenePending(false); - } - }; - - const handleSoundUpload = async (event: FormEvent) => { - event.preventDefault(); - if (!session?.accessToken) { - setSoundMessage(copy.admin.messages.loginRequired); - return; - } - - setSoundPending(true); - setSoundMessage(null); - - try { - const form = event.currentTarget; - const source = new FormData(form); - const presetId = String(source.get('presetId') ?? '').trim(); - - if (!presetId) { - throw new Error(copy.admin.messages.presetIdRequired); - } - - const formData = new FormData(); - for (const fieldName of ['loopFile', 'previewFile', 'fallbackLoopFile']) { - const file = source.get(fieldName); - if (file instanceof File && file.size > 0) { - formData.append(fieldName, file); - } - } - - const defaultVolume = String(source.get('defaultVolume') ?? '').trim(); - if (defaultVolume) { - formData.append('defaultVolume', defaultVolume); - } - - if (isDurationOverrideEnabled) { - const durationSec = String(source.get('durationSec') ?? '').trim(); - if (durationSec) { - formData.append('durationSec', durationSec); - } - } - - const response = await adminApi.uploadSound(presetId, formData, session.accessToken); - setUploadResult({ type: 'sound', payload: response }); - setSoundMessage(copy.admin.messages.soundUploadDone(presetId)); - form.reset(); - setIsDurationOverrideEnabled(false); - } catch (error) { - setSoundMessage(error instanceof Error ? error.message : copy.admin.messages.soundUploadFailed); - } finally { - setSoundPending(false); - } - }; - - const activeMeta = getViewMeta(activeView); - const currentMessage = activeView === 'scene' ? sceneMessage : soundMessage; - const lastExtractedDurationSec = - uploadResult?.type === 'sound' ? uploadResult.payload.durationSec ?? null : null; - - if (!session) { - return ( -
-
- - -
-
-
-

{copy.admin.signInEyebrow}

-

- {copy.admin.loginTitle} -

-

- {copy.admin.loginDescription} -

-
- -
-
- - setLoginId(event.target.value)} - autoComplete="username" - placeholder={copy.admin.defaultLoginId} - /> -
-
- - setPassword(event.target.value)} - autoComplete="current-password" - placeholder={copy.admin.defaultPassword} - /> -
- {loginError ? ( -
- {loginError} -
- ) : null} - -
-
-
-
-
- ); - } - - return ( -
-
- - -
-
-
-
- -
- - - ⌕ - -
-
- -
-
- - {copy.admin.manifestReady} -
-
-
- A -
-
-

{session.user?.name ?? copy.common.admin}

-

{session.user?.email}

-
-
-
-
-
- -
-
-
-
-
-

- {activeMeta.eyebrow} -

-

- {activeMeta.title} -

-

- {activeMeta.description} -

-
-
-

{activeMeta.statTitle}

-

{activeMeta.statValue}

-

{activeMeta.statHint}

-
-
-

{copy.admin.inspector.currentRoleTitle}

-

- {session.user?.grade ?? 'ADMIN'} -

-

{copy.admin.inspector.bearerTokenSession}

-
-
- - {activeView === 'scene' ? ( -
-
-

{copy.admin.views.scene.workspaceTitle}

-

- {copy.admin.views.scene.workspaceDescription} -

-
- -
-
-
-
- - -

{copy.admin.views.scene.sceneIdHint}

-
-
- -
-
- - -

{copy.admin.views.scene.sourceImageHint}

-

{copy.admin.views.scene.sourceImageDerivedHint}

-
-
- -
- -