forked from joshwcomeau/beatmapper
-
Notifications
You must be signed in to change notification settings - Fork 15
Expand file tree
/
Copy pathcreate-map.tsx
More file actions
124 lines (114 loc) · 4.93 KB
/
Copy pathcreate-map.tsx
File metadata and controls
124 lines (114 loc) · 4.93 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
import type { UseDialogContext } from "@ark-ui/react/dialog";
import { CharacteristicNameSchema, DifficultyNameSchema } from "bsmap";
import type { CharacteristicName, DifficultyName } from "bsmap/types";
import { useState } from "react";
import { gtValue, minLength, number, object, pipe, string, transform } from "valibot";
import { APP_TOASTER, CHARACTERISTIC_COLLECTION, COVER_ART_FILE_ACCEPT_TYPE, DIFFICULTY_COLLECTION, SONG_FILE_ACCEPT_TYPE } from "$/components/app/constants";
import { Field, FileUpload, useAppForm } from "$/components/ui/compositions";
import { createSongId, resolveBeatmapId } from "$/helpers/song.helpers";
import { addSong } from "$/store/actions";
import { useAppDispatch, useAppSelector } from "$/store/hooks";
import { selectSongIds, selectUsername } from "$/store/selectors";
interface Props {
dialog?: UseDialogContext;
}
function CreateMapForm({ dialog }: Props) {
const dispatch = useAppDispatch();
const currentSongIds = useAppSelector(selectSongIds);
const username = useAppSelector(selectUsername);
// These files are sent to the redux middleware.
// We'll store them on disk (currently in indexeddb, but that may change), and capture a reference to them by a filename, which we'll store in redux.
const [coverArtFile, setCoverArtFile] = useState<File | null>(null);
const [songFile, setSongFile] = useState<File | null>(null);
const Form = useAppForm({
defaultValues: {
name: "",
subName: "",
artistName: "",
bpm: 120,
offset: 0,
characteristic: "Standard" as CharacteristicName,
difficulty: "Easy" as DifficultyName,
},
validators: {
onChange: object({
name: pipe(string(), minLength(1)),
subName: pipe(string()),
artistName: pipe(string(), minLength(1)),
bpm: pipe(number(), gtValue(0)),
offset: pipe(
number(),
transform((input) => (Number.isNaN(input) ? undefined : input)),
),
characteristic: CharacteristicNameSchema,
difficulty: DifficultyNameSchema,
}),
},
onSubmit: async ({ value }) => {
if (!coverArtFile) {
return APP_TOASTER.create({
type: "error",
description: "Please select a cover art file first",
});
}
if (!songFile) {
return APP_TOASTER.create({
type: "error",
description: "Please select a song file first",
});
}
const songId = createSongId(value);
const beatmapId = resolveBeatmapId({ characteristic: value.characteristic, difficulty: value.difficulty });
// Song IDs must be unique, and song IDs are generated from the name.
// TODO: I could probably just append a `-2` or something, if this constraint turns out to be annoying in some cases
if (currentSongIds.some((id) => id === songId)) {
return APP_TOASTER.create({
id: "song-already-exists",
type: "error",
description: "You already have a song with this name. Please choose a unique name.",
});
}
try {
dispatch(addSong({ songId, beatmapId, name: value.name, subName: value.subName, artistName: value.artistName, bpm: value.bpm, offset: value.offset, songFile, coverArtFile, username: username, selectedCharacteristic: value.characteristic, selectedDifficulty: value.difficulty }));
if (dialog) dialog.setOpen(false);
} catch (err) {
console.error("Could not save files to local storage", err);
return APP_TOASTER.create({
description: "Error creating map. See console for more information.",
type: "error",
});
}
},
});
return (
<Form.AppForm>
<Form.Row>
<Field label="Song File">
<FileUpload accept={SONG_FILE_ACCEPT_TYPE} files={songFile ? [songFile] : []} onFileAccept={(details) => setSongFile(details.files[0])}>
Audio File
</FileUpload>
</Field>
<Field label="Cover Art File">
<FileUpload accept={COVER_ART_FILE_ACCEPT_TYPE} files={coverArtFile ? [coverArtFile] : []} onFileAccept={(details) => setCoverArtFile(details.files[0])}>
Image File
</FileUpload>
</Field>
</Form.Row>
<Form.Root>
<Form.Row>
<Form.AppField name="name">{(ctx) => <ctx.Input label="Song Title" required />}</Form.AppField>
<Form.AppField name="subName">{(ctx) => <ctx.Input label="Song Subtitle" />}</Form.AppField>
<Form.AppField name="artistName">{(ctx) => <ctx.Input label="Artist Name" required />}</Form.AppField>
</Form.Row>
<Form.Row>
<Form.AppField name="bpm">{(ctx) => <ctx.NumberInput label="BPM (Beats per Minute)" required />}</Form.AppField>
<Form.AppField name="offset">{(ctx) => <ctx.NumberInput label="Editor Offset" placeholder="0" />}</Form.AppField>
</Form.Row>
<Form.AppField name="characteristic">{(ctx) => <ctx.RadioButtonGroup label="Beatmap Characteristic" required collection={CHARACTERISTIC_COLLECTION} />}</Form.AppField>
<Form.AppField name="difficulty">{(ctx) => <ctx.RadioButtonGroup label="Beatmap Difficulty" required collection={DIFFICULTY_COLLECTION} />}</Form.AppField>
<Form.Submit>Create</Form.Submit>
</Form.Root>
</Form.AppForm>
);
}
export default CreateMapForm;