This commit is contained in:
u4pak 2024-01-23 20:28:08 -08:00
commit 961b1588a6
12 changed files with 385 additions and 30 deletions

View File

@ -2,6 +2,7 @@ import { NextFunction, Request, Response } from "express";
import { User } from "../Schemas/User";
import { JwtPayload, verify } from "jsonwebtoken";
import { IS_DEBUG, JWT_KEY } from "./Constants";
import j from "joi";
declare global {
// eslint-disable-next-line @typescript-eslint/no-namespace
@ -33,3 +34,14 @@ export function RequireAuthentication(Relations?: object) {
next();
}
}
export function ValidateBody(Schema: j.Schema) {
return async (req: Request, res: Response, next: NextFunction) => {
try {
req.body = await Schema.validateAsync(req.body);
next();
} catch (err) {
res.status(400).json({ errorMessage: "Body validation failed.", details: err })
}
}
}

View File

@ -1,6 +1,10 @@
import { Router } from "express";
import { ADMIN_KEY } from "../Modules/Constants";
import { ADMIN_KEY, FULL_SERVER_ROOT } from "../Modules/Constants";
import { Song } from "../Schemas/Song";
import { ValidateBody } from "../Modules/Middleware";
import { writeFileSync } from "fs";
import exif from "exif-reader";
import j from "joi";
const App = Router();
@ -22,7 +26,77 @@ App.get("/test", (_, res) => res.send("Permission check OK"));
App.get("/tracks", async (_, res) => res.json((await Song.find()).map(x => x.Package())));
App.post("/create/song", async (req, res) => res.json(await Song.create(req.body).save()));
App.post("/create/song",
ValidateBody(j.object({
ID: j.string().uuid(),
Name: j.string().required().min(3).max(64),
Year: j.number().required().min(1).max(2999),
ArtistName: j.string().required().min(1).max(64),
Length: j.number().required().min(1),
Scale: j.string().valid("Minor", "Major").required(),
Key: j.string().valid("A", "Ab", "B", "Bb", "C", "Cb", "D", "Db", "E", "Eb", "F", "Fb", "G", "Gb").required(),
Album: j.string().required(),
GuitarStarterType: j.string().valid("Keytar", "Guitar").required(),
Tempo: j.number().min(20).max(1250).required(),
Midi: j.string().uri(),
Cover: j.string().uri(),
Lipsync: j.string().uri(),
BassDifficulty: j.number().required().min(0).max(7),
GuitarDifficulty: j.number().required().min(0).max(7),
DrumsDifficulty: j.number().required().min(0).max(7),
VocalsDifficulty: j.number().required().min(0).max(7)
})),
async (req, res) => {
res.json(await Song.create(req.body).save())
});
App.post("/upload/midi",
ValidateBody(j.object({
Data: j.string().hex().required(),
TargetSong: j.string().uuid().required()
})),
async (req, res) => {
const Decoded = Buffer.from(req.body.Data, "hex");
if (!Decoded.toString().startsWith("MThd"))
return res.status(400).send("Uploaded MIDI file is not a valid MIDI.");
if (!await Song.exists({ where: { ID: req.body.TargetSong } }))
return res.status(404).send("The song you're trying to upload a MIDI for does not exist.");
writeFileSync(`./Saved/Songs/${req.body.TargetSong}/Data.mid`, Decoded);
res.send(`${FULL_SERVER_ROOT}/song/download/${req.body.TargetSong}/midi.mid`);
});
App.post("/upload/cover",
ValidateBody(j.object({
Data: j.string().hex().required(),
TargetSong: j.string().uuid().required()
})),
async (req, res) => {
const Decoded = Buffer.from(req.body.Data, "hex");
if (!await Song.exists({ where: { ID: req.body.TargetSong } }))
return res.status(404).send("The song you're trying to upload a cover for does not exist.");
try { // todo: fix
/*const ImageMetadata = exif(Decoded);
if (!ImageMetadata.Image?.ImageWidth || !ImageMetadata.Image?.ImageLength)
throw new Error("Invalid image file.");
if (ImageMetadata.Image.ImageWidth !== ImageMetadata.Image.ImageLength)
return res.status(400).send("Image must have a 1:1 ratio.");
if (ImageMetadata.Image.ImageWidth < 512 || ImageMetadata.Image.ImageWidth > 2048)
return res.status(400).send("Image cannot be smaller than 512 pixels and larger than 2048 pixels.");*/
} catch (err) {
console.error(err)
return res.status(400).send("Invalid image file.");
}
writeFileSync(`./Saved/Songs/${req.body.TargetSong}/Cover.png`, Decoded);
res.send(`${FULL_SERVER_ROOT}/song/download/${req.body.TargetSong}/cover.png`);
});
export default {
App,

View File

@ -1,7 +1,8 @@
import { Router } from "express";
import { RequireAuthentication } from "../Modules/Middleware";
import { RequireAuthentication, ValidateBody } from "../Modules/Middleware";
import { Song } from "../Schemas/Song";
import { OriginalSparks } from "../Modules/FNUtil";
import j from "joi";
const App = Router();
@ -12,13 +13,13 @@ App.get("/me", RequireAuthentication({ BookmarkedSongs: true }), (req, res) => {
})
})
App.post("/me/activate", RequireAuthentication(), async (req, res) => {
if (!req.body.SongID || !req.body.ToOverride)
return res.status(400).json({ errorMessage: "You didn't provide a Song ID." });
if (!/^sid_placeholder_(\d){1,3}$/gi.test(req.body.ToOverride))
return res.status(400).json({ errorMessage: "Field \"ToOverride\" must match \"sid_placeholder_<number>\"" });
App.post("/me/activate",
RequireAuthentication(),
ValidateBody(j.object({
SongID: j.string().uuid().required(),
ToOverride: j.string().pattern(/^sid_placeholder_(\d){1,3}$/i).required()
})),
async (req, res) => {
if (req.user?.Library.findIndex(x => x.SongID.toLowerCase() === req.body.SongID.toLowerCase() || x.Overriding.toLowerCase() === req.body.ToOverride.toLowerCase()) !== -1)
return res.status(400).json({ errorMessage: "This song is already activated." });
@ -31,10 +32,12 @@ App.post("/me/activate", RequireAuthentication(), async (req, res) => {
res.json(req.user?.Library);
})
App.post("/me/deactivate", RequireAuthentication(), async (req, res) => {
if (!req.body.SongID)
return res.status(400).json({ errorMessage: "You didn't provide a Song ID." });
App.post("/me/deactivate",
RequireAuthentication(),
ValidateBody(j.object({
SongID: j.string().uuid().required()
})),
async (req, res) => {
const idx = req.user!.Library.findIndex(x => x.SongID.toLowerCase() === req.body.SongID.toLowerCase());
if (idx === -1)
return res.status(400).json({ errorMessage: "This song is not activated." });
@ -45,10 +48,12 @@ App.post("/me/deactivate", RequireAuthentication(), async (req, res) => {
res.json(req.user?.Library);
})
App.post("/me/bookmark", RequireAuthentication({ BookmarkedSongs: true }), async (req, res) => {
if (!req.body.SongID)
return res.status(400).json({ errorMessage: "You didn't provide a Song ID." });
App.post("/me/bookmark",
RequireAuthentication({ BookmarkedSongs: true }),
ValidateBody(j.object({
SongID: j.string().uuid().required()
})),
async (req, res) => {
if (req.user?.BookmarkedSongs.findIndex(x => x.ID.toLowerCase() === req.body.SongID.toLowerCase()) !== -1)
return res.status(400).json({ errorMessage: "This song is already bookmarked." });
@ -62,10 +67,12 @@ App.post("/me/bookmark", RequireAuthentication({ BookmarkedSongs: true }), async
res.json(req.user?.BookmarkedSongs.map(x => x.Package()));
})
App.post("/me/unbookmark", RequireAuthentication(), async (req, res) => {
if (!req.body.SongID)
return res.status(400).json({ errorMessage: "You didn't provide a Song ID." });
App.post("/me/unbookmark",
RequireAuthentication(),
ValidateBody(j.object({
SongID: j.string().uuid().required()
})),
async (req, res) => {
const idx = req.user!.BookmarkedSongs.findIndex(x => x.ID.toLowerCase() === req.body.SongID.toLowerCase());
if (idx === -1)
return res.status(400).json({ errorMessage: "This song is not bookmarked." });

View File

@ -0,0 +1,18 @@
import { BaseEntity, Column, Entity, ManyToOne, PrimaryGeneratedColumn } from "typeorm";
import { User } from "./User";
import { Song } from "./Song";
@Entity()
export class Rating extends BaseEntity {
@PrimaryGeneratedColumn("uuid")
ID: string;
@ManyToOne(() => User, U => U.Ratings)
Author: User;
@ManyToOne(() => Song, S => S.Ratings)
Rated: Song;
@Column()
Stars: number;
}

View File

@ -1,5 +1,8 @@
import { BaseEntity, BeforeInsert, Column, Entity, PrimaryGeneratedColumn } from "typeorm";
import { BaseEntity, BeforeInsert, Column, Entity, OneToMany, PrimaryGeneratedColumn } from "typeorm";
import { FULL_SERVER_ROOT } from "../Modules/Constants";
import { Rating } from "./Rating";
import { existsSync, mkdirSync } from "fs";
import { v4 } from "uuid";
@Entity()
export class Song extends BaseEntity {
@ -60,8 +63,16 @@ export class Song extends BaseEntity {
@Column({ nullable: true })
Lipsync?: string;
@OneToMany(() => Rating, R => R.Rated)
Ratings: Rating[];
@BeforeInsert()
Setup() {
this.ID = v4();
this.Directory = `./Saved/Songs/${this.ID}`;
if (!existsSync(this.Directory))
mkdirSync(this.Directory);
this.CreationDate = new Date();
}

View File

@ -1,5 +1,6 @@
import { BaseEntity, Column, Entity, JoinTable, ManyToMany, PrimaryColumn } from "typeorm";
import { BaseEntity, Column, Entity, JoinTable, ManyToMany, OneToMany, PrimaryColumn } from "typeorm";
import { Song } from "./Song";
import { Rating } from "./Rating";
@Entity()
export class User extends BaseEntity {
@ -9,6 +10,9 @@ export class User extends BaseEntity {
@Column({ type: "simple-json" })
Library: { SongID: string, Overriding: string }[];
@OneToMany(() => Rating, R => R.Author)
Ratings: Rating[];
@ManyToMany(() => Song, { eager: true })
@JoinTable()
BookmarkedSongs: Song[];

View File

@ -10,16 +10,20 @@
"license": "ISC",
"dependencies": {
"@types/cors": "^2.8.17",
"@types/uuid": "^9.0.7",
"axios": "^1.6.5",
"better-sqlite3": "^9.3.0",
"colorette": "^2.0.20",
"cookie-parser": "^1.4.6",
"cors": "^2.8.5",
"dotenv": "^16.3.1",
"exif-reader": "^2.0.0",
"express": "^4.18.2",
"joi": "^17.12.0",
"jsonwebtoken": "^9.0.2",
"typeorm": "^0.3.19",
"underscore": "^1.13.6"
"underscore": "^1.13.6",
"uuid": "^9.0.1"
},
"devDependencies": {
"@types/cookie-parser": "^1.4.6",
@ -31,6 +35,19 @@
"typescript": "^5.2.2"
}
},
"node_modules/@hapi/hoek": {
"version": "9.3.0",
"resolved": "https://registry.npmjs.org/@hapi/hoek/-/hoek-9.3.0.tgz",
"integrity": "sha512-/c6rf4UJlmHlC9b5BaNvzAcFv7HZ2QHaV0D4/HNlBdvFnvQq8RI4kYdhyPCl7Xj+oWvTWQ8ujhqS53LIgAe6KQ=="
},
"node_modules/@hapi/topo": {
"version": "5.1.0",
"resolved": "https://registry.npmjs.org/@hapi/topo/-/topo-5.1.0.tgz",
"integrity": "sha512-foQZKJig7Ob0BMAYBfcJk8d77QtOe7Wo4ox7ff1lQYoNNAb6jwcY1ncdoy2e9wQZzvNy7ODZCYJkK8kzmcAnAg==",
"dependencies": {
"@hapi/hoek": "^9.0.0"
}
},
"node_modules/@isaacs/cliui": {
"version": "8.0.2",
"resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz",
@ -56,6 +73,24 @@
"node": ">=14"
}
},
"node_modules/@sideway/address": {
"version": "4.1.4",
"resolved": "https://registry.npmjs.org/@sideway/address/-/address-4.1.4.tgz",
"integrity": "sha512-7vwq+rOHVWjyXxVlR76Agnvhy8I9rpzjosTESvmhNeXOXdZZB15Fl+TI9x1SiHZH5Jv2wTGduSxFDIaq0m3DUw==",
"dependencies": {
"@hapi/hoek": "^9.0.0"
}
},
"node_modules/@sideway/formula": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/@sideway/formula/-/formula-3.0.1.tgz",
"integrity": "sha512-/poHZJJVjx3L+zVD6g9KgHfYnb443oi7wLu/XKojDviHy6HOEOA6z1Trk5aR1dGcmPenJEgb2sK2I80LeS3MIg=="
},
"node_modules/@sideway/pinpoint": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/@sideway/pinpoint/-/pinpoint-2.0.0.tgz",
"integrity": "sha512-RNiOoTPkptFtSVzQevY/yWtZwf/RxyVnPy/OcA9HBM3MlGDnBEYL5B41H0MTn0Uec8Hi+2qUtTfG2WWZBmMejQ=="
},
"node_modules/@sqltools/formatter": {
"version": "1.2.5",
"resolved": "https://registry.npmjs.org/@sqltools/formatter/-/formatter-1.2.5.tgz",
@ -186,6 +221,11 @@
"integrity": "sha512-HP38xE+GuWGlbSRq9WrZkousaQ7dragtZCruBVMi0oX1migFZavZ3OROKHSkNp/9ouq82zrWtZpg18jFnVN96g==",
"dev": true
},
"node_modules/@types/uuid": {
"version": "9.0.7",
"resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-9.0.7.tgz",
"integrity": "sha512-WUtIVRUZ9i5dYXefDEAI7sh9/O7jGvHg7Df/5O/gtH3Yabe5odI3UWopVR1qbPXQtvOxWu3mM4XxlYeZtMWF4g=="
},
"node_modules/accepts": {
"version": "1.3.8",
"resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz",
@ -852,6 +892,11 @@
"node": ">= 0.6"
}
},
"node_modules/exif-reader": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/exif-reader/-/exif-reader-2.0.0.tgz",
"integrity": "sha512-/c4JW2Kb4/MdmUJyMKTXhNG524q0pCA1DDCEb2aawd9j2i2+bc+mKGKKyO4z2glRTKW93Ks4BWCgmKeEyKiUeA=="
},
"node_modules/expand-template": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/expand-template/-/expand-template-2.0.3.tgz",
@ -1183,6 +1228,18 @@
"@pkgjs/parseargs": "^0.11.0"
}
},
"node_modules/joi": {
"version": "17.12.0",
"resolved": "https://registry.npmjs.org/joi/-/joi-17.12.0.tgz",
"integrity": "sha512-HSLsmSmXz+PV9PYoi3p7cgIbj06WnEBNT28n+bbBNcPZXZFqCzzvGqpTBPujx/Z0nh1+KNQPDrNgdmQ8dq0qYw==",
"dependencies": {
"@hapi/hoek": "^9.3.0",
"@hapi/topo": "^5.1.0",
"@sideway/address": "^4.1.4",
"@sideway/formula": "^3.0.1",
"@sideway/pinpoint": "^2.0.0"
}
},
"node_modules/jsonwebtoken": {
"version": "9.0.2",
"resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.2.tgz",

View File

@ -39,16 +39,20 @@
},
"dependencies": {
"@types/cors": "^2.8.17",
"@types/uuid": "^9.0.7",
"axios": "^1.6.5",
"better-sqlite3": "^9.3.0",
"colorette": "^2.0.20",
"cookie-parser": "^1.4.6",
"cors": "^2.8.5",
"dotenv": "^16.3.1",
"exif-reader": "^2.0.0",
"express": "^4.18.2",
"joi": "^17.12.0",
"jsonwebtoken": "^9.0.2",
"typeorm": "^0.3.19",
"underscore": "^1.13.6"
"underscore": "^1.13.6",
"uuid": "^9.0.1"
},
"homepage": "https://github.com/McMistrzYT/BasedServer#readme"
}

View File

@ -1,3 +1,4 @@
import { useState } from "react";
import { ToastContainer } from "react-toastify";
import { BaseStyles, ThemeProvider, theme } from "@primer/react";
import { SiteHeader } from "./components/SiteHeader";
@ -11,12 +12,12 @@ import { AdminLogin } from "./routes/AdminLogin";
import { Download } from "./routes/Download";
import { Tracks } from "./routes/Tracks";
import { Profile } from "./routes/Profile";
import { AdminCreateTrack } from "./routes/AdminCreateTrack";
import { SiteContext, SiteState } from "./utils/State";
import merge from "deepmerge";
import "react-toastify/dist/ReactToastify.css";
import "./css/index.css";
import { useState } from "react";
const DefaultTheme = merge(theme, {}); // we'll use this!! eventually!!!
@ -44,6 +45,7 @@ function App() {
<Route path="/admin" element={<VerifyAdmin><AdminHome /></VerifyAdmin>} />
<Route path="/admin/login" element={<AdminLogin />} /> {/* this is the only publically available admin endpoint */}
<Route path="/admin/tracks" element={<VerifyAdmin><AdminTrackList /></VerifyAdmin>} />
<Route path="/admin/tracks/create" element={<VerifyAdmin><AdminCreateTrack /></VerifyAdmin>} />
</Routes>
</div>
</SiteContext.Provider>

View File

@ -0,0 +1,166 @@
import { ActionList, ActionMenu, FormControl, TextInput, Heading, Button } from "@primer/react";
import { useRef, useState } from "react";
import { toast } from "react-toastify";
import { Buffer } from "buffer/";
import axios from "axios";
const style = { paddingTop: 3 };
export function AdminCreateTrack() {
const formRef = useRef<HTMLFormElement>(null);
const [Key, setKey] = useState<string>("Select a key...");
const [Scale, setScale] = useState<string>("Select a scale...");
const [GuitarStarterType, setGuitarStarterType] = useState<string>("Select the starter type...");
return (
<>
<Heading>Create a new song</Heading>
<form method="GET" action="" ref={formRef}>
<FormControl required={true} sx={style}>
<FormControl.Label>Song Name</FormControl.Label>
<TextInput />
</FormControl>
<FormControl required={true} sx={style}>
<FormControl.Label>Artist</FormControl.Label>
<FormControl.Caption>If there are multiple artists, separate them with a comma.</FormControl.Caption>
<TextInput />
</FormControl>
<FormControl required={true} sx={style}>
<FormControl.Label>Album</FormControl.Label>
<TextInput />
</FormControl>
<FormControl required={true} sx={style}>
<FormControl.Label>Release Year</FormControl.Label>
<TextInput type="number" />
</FormControl>
<FormControl required={true} sx={style}>
<FormControl.Label>Length</FormControl.Label>
<TextInput type="time" />
</FormControl>
<FormControl required={true} sx={style}>
<FormControl.Label>Key</FormControl.Label>
<ActionMenu>
<ActionMenu.Button>{Key}</ActionMenu.Button>
<ActionMenu.Overlay width="medium">
{
["A", "Ab", "B", "Bb", "C", "Cb", "D", "Db", "E", "Eb", "F", "Fb", "G", "Gb"].map(x => {
return (
<ActionList.Item onSelect={() => setKey(x)}>
{x}
</ActionList.Item>
)
})
}
</ActionMenu.Overlay>
</ActionMenu>
</FormControl>
<FormControl required={true} sx={style}>
<FormControl.Label>Scale</FormControl.Label>
<ActionMenu>
<ActionMenu.Button>{Scale}</ActionMenu.Button>
<ActionMenu.Overlay width="medium">
<ActionList.Item onSelect={() => setScale("Minor")}>Minor</ActionList.Item>
<ActionList.Item onSelect={() => setScale("Major")}>Major</ActionList.Item>
</ActionMenu.Overlay>
</ActionMenu>
</FormControl>
<FormControl required={true} sx={style}>
<FormControl.Label>Lead Type</FormControl.Label>
<FormControl.Caption>This is defining what lead instrument the song is going to start with. You can change the instrument mid-game with [keytar] and [guitar] text events.</FormControl.Caption>
<ActionMenu>
<ActionMenu.Button>{GuitarStarterType}</ActionMenu.Button>
<ActionMenu.Overlay width="medium">
<ActionList.Item onSelect={() => setGuitarStarterType("Guitar")}>Guitar</ActionList.Item>
<ActionList.Item onSelect={() => setGuitarStarterType("Keytar")}>Keytar</ActionList.Item>
</ActionMenu.Overlay>
</ActionMenu>
</FormControl>
<FormControl required={true} sx={style}>
<FormControl.Label>Tempo</FormControl.Label>
<TextInput type="number" />
</FormControl>
<FormControl required={true} sx={style}>
<FormControl.Label>MIDI File</FormControl.Label>
<TextInput type="file" />
<FormControl.Caption>You can use the #tools-and-resources channel to find useful resources on how to create MIDIs.</FormControl.Caption>
</FormControl>
<FormControl required={true} sx={style}>
<FormControl.Label>Cover Image</FormControl.Label>
<TextInput type="file" />
<FormControl.Caption>Must be a 1:1 ratio. Max: 2048x2048, min: 512x512</FormControl.Caption>
</FormControl>
<FormControl required={true} sx={style}>
<FormControl.Label>Lead Difficulty</FormControl.Label>
<TextInput type="number" />
</FormControl>
<FormControl required={true} sx={style}>
<FormControl.Label>Drums Difficulty</FormControl.Label>
<TextInput type="number" />
</FormControl>
<FormControl required={true} sx={style}>
<FormControl.Label>Vocals Difficulty</FormControl.Label>
<TextInput type="number" />
</FormControl>
<FormControl required={true} sx={style}>
<FormControl.Label>Bass Difficulty</FormControl.Label>
<TextInput type="number" />
</FormControl>
<Button sx={{ marginTop: 2 }} type="submit" onClick={async e => {
e.preventDefault();
console.log(formRef);
if (formRef.current == null)
return;
const Name = (formRef.current[0] as HTMLInputElement).value;
const ArtistName = (formRef.current[1] as HTMLInputElement).value;
const Album = (formRef.current[2] as HTMLInputElement).value;
const Year = (formRef.current[3] as HTMLInputElement).value;
const Length = (formRef.current[4] as HTMLInputElement).valueAsNumber / 1000 / 3600 * 60;
//const Key = (formRef.current[5] as HTMLInputElement).value;
//const Scale = (formRef.current[6] as HTMLInputElement).value;
//const GuitarStarterType = (formRef.current[7] as HTMLInputElement).value;
const Tempo = (formRef.current[8] as HTMLInputElement).valueAsNumber;
const Midi = (formRef.current[9] as HTMLInputElement).files![0];
const Cover = (formRef.current[10] as HTMLInputElement).files![0];
const GuitarDifficulty = (formRef.current[11] as HTMLInputElement).valueAsNumber;
const DrumsDifficulty = (formRef.current[12] as HTMLInputElement).valueAsNumber;
const VocalsDifficulty = (formRef.current[13] as HTMLInputElement).valueAsNumber;
const BassDifficulty = (formRef.current[14] as HTMLInputElement).valueAsNumber;
const SongData = await axios.post("/admin/api/create/song", {
Name,
ArtistName,
Album,
Year,
Length,
Key,
Scale,
GuitarStarterType,
Tempo,
GuitarDifficulty,
DrumsDifficulty,
VocalsDifficulty,
BassDifficulty
});
toast(SongData.data, { type: SongData.status === 200 ? "success" : "error" });
if (SongData.status !== 200)
return;
const MidiRes = await axios.post("/admin/api/upload/midi", {
Data: Buffer.from(
await Midi.arrayBuffer()
).toString("hex"),
TargetSong: SongData.data.ID
});
toast(MidiRes.data, { type: MidiRes.status === 200 ? "success" : "error" });
const CoverRes = await axios.post("/admin/api/upload/cover", { Data: Buffer.from(await Cover.arrayBuffer()).toString("hex"), TargetSong: SongData.data.ID });
toast(CoverRes.data, { type: CoverRes.status === 200 ? "success" : "error" });
}}>Create</Button>
</form>
</>
)
}

View File

@ -21,7 +21,7 @@ export function AdminTrackList() {
return (
<>
<Heading>All tracks (admin) <Button sx={{ marginBottom: 2 }}>Create</Button></Heading>
<Heading>All tracks (admin) <Button sx={{ marginBottom: 2 }} onClick={() => navigate("/admin/tracks/create")}>Create</Button></Heading>
<Box className="songCategory">
{
tracks.map(x => {

View File

@ -58,7 +58,7 @@ export function Profile() {
<Dialog isOpen={isActivateDialogOpen} onDismiss={() => setIsActivateDialogOpen(false)} aria-labelledby="header">
<Dialog.Header id="header">Activate song</Dialog.Header>
<Box p={3}>
<Text fontFamily="sans-serif">In order to activate a song for use, you need to sacrifice another song.<br />Please select a song you own to replace:</Text>
<Text>In order to activate a song for use, you need to sacrifice another song.<br />Please select a song you own to replace:</Text>
<ActionMenu>
<ActionMenu.Button>Select a song...</ActionMenu.Button>
<ActionMenu.Overlay width="medium">