From 4ee86d2bd651226dfb70eadf5975893e40507293 Mon Sep 17 00:00:00 2001 From: Cano Date: Tue, 10 Oct 2023 13:08:23 +0900 Subject: [PATCH] =?UTF-8?q?=E3=83=87=E3=83=BC=E3=82=BF=E3=81=AE=20i/o=20?= =?UTF-8?q?=E3=82=92=E5=AE=9F=E8=A3=85=20(#151)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/common/main-app-bar/index.tsx | 43 +++++++++-- src/components/export/fleet_export_dialog.tsx | 76 +++++++++++++++++++ src/components/export/hooks.ts | 16 ++++ src/components/export/state.ts | 8 ++ src/components/import/fleet_import_dialog.tsx | 67 ++++++++++++++++ src/components/import/hooks.ts | 16 ++++ src/components/import/import_fleet.ts | 17 +++++ src/components/import/state.ts | 8 ++ src/core/persistence/schema.ts | 60 +++++++++++++++ .../util/is-fleet-type/is-fleet-type.test.ts | 2 +- src/models/fleet/types.ts | 10 +-- src/pages/index.tsx | 30 +++++++- 12 files changed, 340 insertions(+), 13 deletions(-) create mode 100644 src/components/export/fleet_export_dialog.tsx create mode 100644 src/components/export/hooks.ts create mode 100644 src/components/export/state.ts create mode 100644 src/components/import/fleet_import_dialog.tsx create mode 100644 src/components/import/hooks.ts create mode 100644 src/components/import/import_fleet.ts create mode 100644 src/components/import/state.ts create mode 100644 src/core/persistence/schema.ts diff --git a/src/components/common/main-app-bar/index.tsx b/src/components/common/main-app-bar/index.tsx index 610baad..9e6ef0d 100644 --- a/src/components/common/main-app-bar/index.tsx +++ b/src/components/common/main-app-bar/index.tsx @@ -1,5 +1,9 @@ -import { AppBar, Box, Toolbar, useScrollTrigger } from '@mui/material' +import { FileDownloadOutlined, FileUploadOutlined } from '@mui/icons-material' +import { AppBar, Box, Button, Toolbar, useScrollTrigger } from '@mui/material' import { FC, useState } from 'react' +import { useEffectOnce } from 'react-use' +import { useExportFleet } from '~/components/export/hooks' +import { useImportFleet } from '~/components/import/hooks' import { UserIconButton } from '../user-icon' import { MainMenuDialog } from './account-dialog' @@ -13,6 +17,14 @@ export const MainAppBar: FC = () => { const openMainMenu = () => setMainMenuOpen(true) const closeMainMenu = () => setMainMenuOpen(false) + const requestExportFleet = useExportFleet() + const requestImportFleet = useImportFleet() + + const [isOldSite, setIsOldSite] = useState(false) + useEffectOnce(() => { + setIsOldSite(window.location.hostname !== 'kcm2cale.tepbyte.dev') + }) + return ( <> { > - + + + {isOldSite ? ( + + ) : ( + + )} + + + diff --git a/src/components/export/fleet_export_dialog.tsx b/src/components/export/fleet_export_dialog.tsx new file mode 100644 index 0000000..f2c4e7e --- /dev/null +++ b/src/components/export/fleet_export_dialog.tsx @@ -0,0 +1,76 @@ +import { ContentCopyOutlined } from '@mui/icons-material' +import { + Button, + Dialog, + DialogActions, + DialogContent, + DialogTitle, + IconButton, + OutlinedInput, + Tooltip, +} from '@mui/material' +import { FC, useEffect, useState } from 'react' +import { useRecoilState } from 'recoil' +import { LocalDatabase } from '~/core/persistence/local-database' +import { FleetExportDialogStateAtom } from './state' + +export const ExportFleetDialog: FC = () => { + const [exportFleetDialogState, setExportFleetDialogState] = useRecoilState( + FleetExportDialogStateAtom, + ) + + const [exportData, setExportData] = useState('') + + const open = exportFleetDialogState !== null + + const onClose = () => { + setExportFleetDialogState(null) + } + + useEffect(() => { + const get = async () => { + if (exportFleetDialogState === null) { + return + } + + if (exportFleetDialogState.mode === 'all') { + setExportData( + JSON.stringify( + (await LocalDatabase.getAllFleet()).map((v) => { + const { id: _, ...other } = v + return other + }), + ), + ) + } + } + get() + }, [exportFleetDialogState]) + + return ( + + エクスポート + + + navigator.clipboard.writeText(exportData)} + > + + + + } + inputProps={{ readOnly: true }} + value={exportData} + /> + + + + + + ) +} diff --git a/src/components/export/hooks.ts b/src/components/export/hooks.ts new file mode 100644 index 0000000..ff4a3dd --- /dev/null +++ b/src/components/export/hooks.ts @@ -0,0 +1,16 @@ +import { useCallback } from 'react' +import { useSetRecoilState } from 'recoil' +import { FleetExportDialogState, FleetExportDialogStateAtom } from './state' + +export const useExportFleet = () => { + const setFleetExportDialogState = useSetRecoilState( + FleetExportDialogStateAtom, + ) + + const requestExport = useCallback( + (option: FleetExportDialogState) => setFleetExportDialogState(option), + [setFleetExportDialogState], + ) + + return requestExport +} diff --git a/src/components/export/state.ts b/src/components/export/state.ts new file mode 100644 index 0000000..98c61b8 --- /dev/null +++ b/src/components/export/state.ts @@ -0,0 +1,8 @@ +import { atom } from 'recoil' + +export type FleetExportDialogState = null | { mode: 'all' } + +export const FleetExportDialogStateAtom = atom({ + key: 'fleet_export_dialog_state', + default: null, +}) diff --git a/src/components/import/fleet_import_dialog.tsx b/src/components/import/fleet_import_dialog.tsx new file mode 100644 index 0000000..ab298d4 --- /dev/null +++ b/src/components/import/fleet_import_dialog.tsx @@ -0,0 +1,67 @@ +import { + Button, + Dialog, + DialogActions, + DialogContent, + DialogTitle, + OutlinedInput, +} from '@mui/material' +import { ChangeEvent, FC, useCallback, useState } from 'react' +import { useRecoilState } from 'recoil' +import { ExportedFleetListSchema } from '~/core/persistence/schema' +import { importFleet } from './import_fleet' +import { FleetImportDialogStateAtom } from './state' + +export const ImportFleetDialog: FC = () => { + const [importFleetDialogState, setImportFleetDialogState] = useRecoilState( + FleetImportDialogStateAtom, + ) + + const [value, setValue] = useState('') + const onInputChange = useCallback((e: ChangeEvent) => { + setValue(e.target.value) + }, []) + + const [hasError, setHasError] = useState(false) + + const open = importFleetDialogState + + const onClose = () => { + setImportFleetDialogState(false) + } + + const processImport = async () => { + const parseResult = ExportedFleetListSchema.safeParse(value) + + if (parseResult.success) { + await importFleet(parseResult.data) + + setHasError(false) + onClose() + } else { + setHasError(true) + } + } + + return ( + + インポート + + + + + + + + + ) +} diff --git a/src/components/import/hooks.ts b/src/components/import/hooks.ts new file mode 100644 index 0000000..dbb872e --- /dev/null +++ b/src/components/import/hooks.ts @@ -0,0 +1,16 @@ +import { useCallback } from 'react' +import { useSetRecoilState } from 'recoil' +import { FleetImportDialogStateAtom } from './state' + +export const useImportFleet = () => { + const setFleetImportDialogState = useSetRecoilState( + FleetImportDialogStateAtom, + ) + + const requestImport = useCallback( + () => setFleetImportDialogState(true), + [setFleetImportDialogState], + ) + + return requestImport +} diff --git a/src/components/import/import_fleet.ts b/src/components/import/import_fleet.ts new file mode 100644 index 0000000..af1194f --- /dev/null +++ b/src/components/import/import_fleet.ts @@ -0,0 +1,17 @@ +import type * as zod from 'zod' +import { LocalDatabase } from '~/core/persistence/local-database' +import { ExportedFleetListSchema } from '~/core/persistence/schema' +import { generateFleetId } from '~/core/util/generate-id' + +export const importFleet = async ( + fleetList: zod.infer, +) => { + await Promise.all( + fleetList.map((v) => { + const id = generateFleetId() + const fleet = { ...v, id } + + return LocalDatabase.setFleet(id, fleet) + }), + ) +} diff --git a/src/components/import/state.ts b/src/components/import/state.ts new file mode 100644 index 0000000..5df9396 --- /dev/null +++ b/src/components/import/state.ts @@ -0,0 +1,8 @@ +import { atom } from 'recoil' + +export type FleetImportDialogState = boolean + +export const FleetImportDialogStateAtom = atom({ + key: 'fleet_import_dialog_state', + default: false, +}) diff --git a/src/core/persistence/schema.ts b/src/core/persistence/schema.ts new file mode 100644 index 0000000..6018301 --- /dev/null +++ b/src/core/persistence/schema.ts @@ -0,0 +1,60 @@ +import { + NEVER, + any, + array, + coerce, + literal, + number, + object, + string, + union, +} from 'zod' + +// util +const coerceJson = union([ + // 文字列なら json としてパースする + string().transform((str, ctx) => { + try { + return JSON.parse(str) + } catch (e) { + ctx.addIssue({ code: 'custom', message: 'Invalid JSON' }) + return NEVER + } + }), + any(), +]) + +export const ExportedFleetSchema = coerceJson.pipe( + object({ + version: literal(1), + type: union([ + literal('Normal'), + literal('Carrier'), + literal('Surface'), + literal('Transport'), + literal('StrikingForce'), + ]), + title: string(), + description: string(), + createdAt: coerce.date(), + updatedAt: coerce.date(), + + ships: array( + object({ + fleetNo: number(), + turnNo: number(), + no: string(), + equipments: array( + object({ + slotNo: number(), + no: number(), + }), + ), + }), + ), + }), +) + +export const ExportedFleetListSchema = coerceJson.pipe( + array(ExportedFleetSchema), +) diff --git a/src/core/util/is-fleet-type/is-fleet-type.test.ts b/src/core/util/is-fleet-type/is-fleet-type.test.ts index 3ab68de..12f69b8 100644 --- a/src/core/util/is-fleet-type/is-fleet-type.test.ts +++ b/src/core/util/is-fleet-type/is-fleet-type.test.ts @@ -10,5 +10,5 @@ test('isFleetType', () => { expect(isFleetType('')).toBe(false) expect(isFleetType('NORMAL')).toBe(false) - expect(isFleetType('Normal')).toBe(false) + expect(isFleetType('normal')).toBe(false) }) diff --git a/src/models/fleet/types.ts b/src/models/fleet/types.ts index ae8b3b3..99c1b20 100644 --- a/src/models/fleet/types.ts +++ b/src/models/fleet/types.ts @@ -1,8 +1,8 @@ export const FleetType = { - Normal: 'normal', - Carrier: 'carrier', - Surface: 'surface', - Transport: 'transport', - Striking: 'striking', + Normal: 'Normal', + Carrier: 'Carrier', + Surface: 'Surface', + Transport: 'Transport', + Striking: 'StrikingForce', } as const export type FleetType = (typeof FleetType)[keyof typeof FleetType] diff --git a/src/pages/index.tsx b/src/pages/index.tsx index 2d42548..c3749fd 100644 --- a/src/pages/index.tsx +++ b/src/pages/index.tsx @@ -1,12 +1,22 @@ -import { Box, Grid } from '@mui/material' +import { Alert, AlertTitle, Box, Container, Grid } from '@mui/material' import { NextPage } from 'next' import Head from 'next/head' +import { useState } from 'react' +import { useEffectOnce } from 'react-use' +import { TextLink } from '~/components/common/TextLink' +import { ExportFleetDialog } from '~/components/export/fleet_export_dialog' +import { ImportFleetDialog } from '~/components/import/fleet_import_dialog' import { MainAppBar } from '../components/common/main-app-bar' import { CreateNewFleet } from '../components/home/create-new-fleet' import { FleetListArea } from '../components/home/fleet-list-area' import { APP_NAME } from '../core/env' const HomePage: NextPage = () => { + const [isOldSite, setIsOldSite] = useState(false) + useEffectOnce(() => { + setIsOldSite(window.location.hostname !== 'kcm2cale.tepbyte.dev') + }) + return ( <> @@ -29,9 +39,27 @@ const HomePage: NextPage = () => { + {isOldSite && ( + + + + このアプリは{' '} + + kcm2cale.tepbyte.dev + {' '} + に移転されます。 + + 今後は新しいサイトをご利用ください。このサイトで保存された編成は、右上のボタンからエクスポート/インポート出来ます。 + + + )} + + + +