Skip to content

Commit

Permalink
データの i/o を実装 (#151)
Browse files Browse the repository at this point in the history
  • Loading branch information
canoypa authored Oct 10, 2023
1 parent 185185d commit 4ee86d2
Show file tree
Hide file tree
Showing 12 changed files with 340 additions and 13 deletions.
43 changes: 37 additions & 6 deletions src/components/common/main-app-bar/index.tsx
Original file line number Diff line number Diff line change
@@ -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'

Expand All @@ -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 (
<>
<AppBar
Expand All @@ -22,11 +34,30 @@ export const MainAppBar: FC = () => {
>
<Toolbar>
<Box flexGrow={1} />
<UserIconButton
edge="end"
onClick={openMainMenu}
aria-label="アカウントメニュー"
/>

<Box display="flex" columnGap={1}>
{isOldSite ? (
<Button
startIcon={<FileDownloadOutlined />}
onClick={() => requestExportFleet({ mode: 'all' })}
>
編成をエクスポート
</Button>
) : (
<Button
startIcon={<FileUploadOutlined />}
onClick={() => requestImportFleet()}
>
編成をインポート
</Button>
)}

<UserIconButton
edge="end"
onClick={openMainMenu}
aria-label="アカウントメニュー"
/>
</Box>
</Toolbar>
</AppBar>

Expand Down
76 changes: 76 additions & 0 deletions src/components/export/fleet_export_dialog.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<Dialog fullWidth maxWidth="sm" open={open} onClose={onClose}>
<DialogTitle>エクスポート</DialogTitle>
<DialogContent>
<OutlinedInput
fullWidth
endAdornment={
<Tooltip title="コピー">
<IconButton
onClick={() => navigator.clipboard.writeText(exportData)}
>
<ContentCopyOutlined />
</IconButton>
</Tooltip>
}
inputProps={{ readOnly: true }}
value={exportData}
/>
</DialogContent>
<DialogActions>
<Button variant="outlined" onClick={onClose}>
とじる
</Button>
</DialogActions>
</Dialog>
)
}
16 changes: 16 additions & 0 deletions src/components/export/hooks.ts
Original file line number Diff line number Diff line change
@@ -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
}
8 changes: 8 additions & 0 deletions src/components/export/state.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { atom } from 'recoil'

export type FleetExportDialogState = null | { mode: 'all' }

export const FleetExportDialogStateAtom = atom<FleetExportDialogState>({
key: 'fleet_export_dialog_state',
default: null,
})
67 changes: 67 additions & 0 deletions src/components/import/fleet_import_dialog.tsx
Original file line number Diff line number Diff line change
@@ -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<HTMLInputElement>) => {
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 (
<Dialog fullWidth maxWidth="sm" open={open} onClose={onClose}>
<DialogTitle>インポート</DialogTitle>
<DialogContent>
<OutlinedInput
fullWidth
value={value}
onChange={onInputChange}
error={hasError}
/>
</DialogContent>
<DialogActions>
<Button variant="text" onClick={onClose}>
キャンセル
</Button>
<Button variant="outlined" onClick={processImport}>
インポートする
</Button>
</DialogActions>
</Dialog>
)
}
16 changes: 16 additions & 0 deletions src/components/import/hooks.ts
Original file line number Diff line number Diff line change
@@ -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
}
17 changes: 17 additions & 0 deletions src/components/import/import_fleet.ts
Original file line number Diff line number Diff line change
@@ -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<typeof ExportedFleetListSchema>,
) => {
await Promise.all(
fleetList.map((v) => {
const id = generateFleetId()
const fleet = { ...v, id }

return LocalDatabase.setFleet(id, fleet)
}),
)
}
8 changes: 8 additions & 0 deletions src/components/import/state.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { atom } from 'recoil'

export type FleetImportDialogState = boolean

export const FleetImportDialogStateAtom = atom<FleetImportDialogState>({
key: 'fleet_import_dialog_state',
default: false,
})
60 changes: 60 additions & 0 deletions src/core/persistence/schema.ts
Original file line number Diff line number Diff line change
@@ -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),
)
2 changes: 1 addition & 1 deletion src/core/util/is-fleet-type/is-fleet-type.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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)
})
10 changes: 5 additions & 5 deletions src/models/fleet/types.ts
Original file line number Diff line number Diff line change
@@ -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]
Loading

0 comments on commit 4ee86d2

Please sign in to comment.