From 6346e09644217f8d950188498c259655e2fc0df7 Mon Sep 17 00:00:00 2001 From: GregTCLTK Date: Sat, 13 Jan 2024 15:05:18 +0100 Subject: [PATCH] still highly wip: webgen changes --- pages/_legacy/helper.ts | 3 +- pages/_legacy/music/changeDrop.ts | 165 +++++++++++++++-------------- pages/_legacy/music/changeSongs.ts | 95 ++++++++--------- pages/_legacy/music/data.ts | 2 - pages/_legacy/music/edit.ts | 86 ++++++++++----- pages/_legacy/music/table.ts | 17 ++- pages/_legacy/newDrop.ts | 10 +- pages/admin/admin.ts | 6 +- pages/admin/dialog.ts | 100 ++++++++--------- pages/admin/views/entryReview.ts | 23 ++-- pages/admin/views/list.ts | 26 +++-- pages/admin/views/menu.ts | 10 +- spec/music.ts | 5 +- 13 files changed, 297 insertions(+), 251 deletions(-) diff --git a/pages/_legacy/helper.ts b/pages/_legacy/helper.ts index 95ab10b..e49e11c 100644 --- a/pages/_legacy/helper.ts +++ b/pages/_legacy/helper.ts @@ -225,8 +225,7 @@ export const EditArtistsDialog = (state: StateHandler<{ artists: Artist[]; }>) = .setValue(artist[ 0 ]) .onChange(data => artist[ 0 ] = data ?? "") ) - //how to handle without index? - .addColumn("", () => IconButton(MIcon("delete"), "Delete").onClick(() => "remove")), + .addColumn("", (data) => IconButton(MIcon("delete"), "Delete").onClick(() => state.artists = state.artists.filter((_, i) => i != state.artists.indexOf(data)) as typeof state.artists)), Horizontal( Spacer(), Button("Add Artist") diff --git a/pages/_legacy/music/changeDrop.ts b/pages/_legacy/music/changeDrop.ts index 2c7b01a..553ba78 100644 --- a/pages/_legacy/music/changeDrop.ts +++ b/pages/_legacy/music/changeDrop.ts @@ -1,91 +1,98 @@ -import { API, stupidErrorAlert } from "shared/mod.ts"; -import { AdvancedImage, Box, Button, DropAreaInput, DropDownInput, Grid, IconButton, Image, MIcon, Spacer, TextInput, createFilePicker } from "webgen/mod.ts"; +import * as zod from "https://deno.land/x/zod@v3.22.4/mod.ts"; +import { ZodError } from "https://deno.land/x/zod@v3.22.4/mod.ts"; +import { API } from "shared/mod.ts"; +import { AdvancedImage, Box, Button, CenterV, DropAreaInput, DropDownInput, Empty, Grid, Horizontal, IconButton, Image, Label, MIcon, Spacer, StateHandler, TextInput, Validate, createFilePicker, getErrorMessage } from "webgen/mod.ts"; import artwork from "../../../assets/img/template-artwork.png"; import genres from "../../../data/genres.json" with { type: "json" }; import language from "../../../data/language.json" with { type: "json" }; -import { ArtistTypes, Drop } from "../../../spec/music.ts"; +import { Artist, DATE_PATTERN, artist, song, userString } from "../../../spec/music.ts"; import { EditArtistsDialog, allowedImageFormats, getSecondary } from "../helper.ts"; import { uploadArtwork } from "./data.ts"; -export function ChangeDrop(drop: Drop) { - return Wizard({ - submitAction: async (data) => { - let obj = structuredClone(drop); - data.map(x => x.data.data).forEach(x => obj = { ...obj, ...x }); - await API.music.id(drop._id).update(obj); - location.reload(); // Handle this Smarter => Make it a Reload Event. - }, - buttonArrangement: "flex-end", - buttonAlignment: "top", - }, () => [ - Page({ - title: drop.title, - release: drop.release, - language: drop.language, - artists: drop.artists, - primaryGenre: drop.primaryGenre, - secondaryGenre: drop.secondaryGenre, - compositionCopyright: drop.compositionCopyright, - soundRecordingCopyright: drop.soundRecordingCopyright, - - loading: false, - artwork: drop.artwork, - artworkClientData: (drop.artwork ? { type: "direct", source: () => API.music.id(drop._id).artwork().then(stupidErrorAlert) } : undefined), +export function ChangeDrop(state: StateHandler<{ _id: string | undefined, title: string | undefined, release: string | undefined, language: string | undefined, artists: Artist[], artwork: string | undefined, artworkClientData: AdvancedImage | string | undefined; compositionCopyright: string | undefined, soundRecordingCopyright: string | undefined, primaryGenre: string | undefined, secondaryGenre: string | undefined, loading: boolean; validationState: ZodError | undefined; }>) { + const { data, error, validate } = Validate( + state, + zod.object({ + title: userString, + artists: artist.array().refine(x => x.some(([ , , type ]) => type == "PRIMARY"), { message: "At least one primary artist is required" }), + release: zod.string().regex(DATE_PATTERN, { message: "Not a date" }), + language: zod.string(), + primaryGenre: zod.string(), + secondaryGenre: zod.string(), + compositionCopyright: userString, + soundRecordingCopyright: userString, + artwork: zod.string(), + loading: zod.literal(false, { errorMap: () => ({ message: "Artwork is still uploading" }) }).transform(() => undefined), + songs: song.array().min(1, { message: "At least one song is required" }), + }) + ); - uploadingSongs: [], - songs: drop.songs - }, data => [ + return Grid( + [ + { width: 2 }, + Horizontal( + Box(data.$validationState.map(error => error ? CenterV( + Label(getErrorMessage(error)) + .addClass("error-message") + .setMargin("0 0.5rem 0 0") + ) + : Empty()).asRefComponent()), + Spacer(), + Button("Save") + .onClick(async () => { + const validation = validate(); + if (error.getValue()) return data.validationState = error.getValue(); + if (validation) await API.music.id(state._id!).update(validation); + location.reload(); // Handle this Smarter => Make it a Reload Event. + }) + ), + ], + Grid( + data.$artworkClientData.map(artworkData => DropAreaInput( + Box(artworkData ? Image(artworkData, "A Music Album Artwork.") : Image(artwork, "A Default Alubm Artwork."), IconButton(MIcon("edit"), "edit icon")) + .addClass("upload-image"), + allowedImageFormats, + ([ { file } ]) => uploadArtwork(data, file) + ).onClick(() => createFilePicker(allowedImageFormats.join(",")).then(file => uploadArtwork(data, file)))).asRefComponent(), + ).setDynamicColumns(2, "12rem"), + [ + { width: 2 }, + TextInput("text", "Title").sync(data, "title") + ], + TextInput("date", "Release Date").sync(data, "release"), + DropDownInput("Language", Object.keys(language)) + .setRender((key) => language[ key ]) + .sync(data, "language"), + [ + { width: 2 }, + Button("Artists") + .onClick(() => { + EditArtistsDialog(data).open(); + }), + ], + [ { width: 2, height: 2 }, Spacer() ], + [ + { width: 2 }, Grid( - Grid( - data.$artworkClientData.map(() => DropAreaInput( - Box(data.artworkClientData ? Image(data.artworkClientData, "A Music Album Artwork.") : Image(artwork, "A Default Alubm Artwork."), IconButton(MIcon("edit"), "edit icon")) - .addClass("upload-image"), - allowedImageFormats, - ([ { file } ]) => uploadArtwork(data, file) - ).onClick(() => createFilePicker(allowedImageFormats.join(",")).then(file => uploadArtwork(data, file)))).asRefComponent(), - ).setDynamicColumns(2, "12rem"), - [ - { width: 2 }, - TextInput("text", "Title").sync(data, "title") - ], - TextInput("date", "Release Date").sync(data, "release"), - DropDownInput("Language", Object.keys(language)) - .setRender((key) => language[ key ]) - .sync(data, "language"), - [ - { width: 2 }, - // TODO: Make this a nicer component - Button("Artists") - .onClick(() => { - EditArtistsDialog(data.artists ?? [ [ "", "", ArtistTypes.Primary ] ]).open(); - }), - ], - [ { width: 2, height: 2 }, Spacer() ], - [ - { width: 2 }, - Grid( - DropDownInput("Primary Genre", Object.keys(genres)) - .sync(data, "primaryGenre") - .onChange(() => { - data.secondaryGenre = undefined!; - }), - data.$primaryGenre.map(() => DropDownInput("Secondary Genre", getSecondary(genres, data.primaryGenre) ?? []) - .sync(data, "secondaryGenre") - .addClass("border-box") - .setWidth("100%") - ).asRefComponent(), - ) - .setEvenColumns(2, "minmax(2rem, 20rem)") - .setGap("15px") - ], - TextInput("text", "Composition Copyright").sync(data, "compositionCopyright"), - TextInput("text", "Sound Recording Copyright").sync(data, "soundRecordingCopyright") + DropDownInput("Primary Genre", Object.keys(genres)) + .sync(data, "primaryGenre") + .onChange(() => { + data.secondaryGenre = undefined!; + }), + data.$primaryGenre.map(primaryGenre => DropDownInput("Secondary Genre", getSecondary(genres, primaryGenre) ?? []) + .sync(data, "secondaryGenre") + .addClass("border-box") + .setWidth("100%") + ).asRefComponent(), ) .setEvenColumns(2, "minmax(2rem, 20rem)") - .addClass("settings-form") - .addClass("limited-width") .setGap("15px") - ]).setValidator(() => pureDrop) - ] - ); + ], + TextInput("text", "Composition Copyright").sync(data, "compositionCopyright"), + TextInput("text", "Sound Recording Copyright").sync(data, "soundRecordingCopyright") + ) + .setEvenColumns(2, "minmax(2rem, 20rem)") + .addClass("settings-form") + .addClass("limited-width") + .setGap("15px"); } \ No newline at end of file diff --git a/pages/_legacy/music/changeSongs.ts b/pages/_legacy/music/changeSongs.ts index aec84c5..c762b33 100644 --- a/pages/_legacy/music/changeSongs.ts +++ b/pages/_legacy/music/changeSongs.ts @@ -1,65 +1,62 @@ import * as zod from "https://deno.land/x/zod@v3.22.4/mod.ts"; import { API } from "shared/mod.ts"; -import { Button, Grid, Horizontal, Spacer, State, Validate, createFilePicker } from "webgen/mod.ts"; -import { Drop } from "../../../spec/music.ts"; +import { AdvancedImage, Box, Button, CenterV, Empty, Grid, Horizontal, Label, Spacer, StateHandler, Validate, createFilePicker, getErrorMessage } from "webgen/mod.ts"; +import { Artist, DATE_PATTERN, Song, artist, song, userString } from "../../../spec/music.ts"; import { allowedAudioFormats } from "../helper.ts"; import { uploadSongToDrop } from "./data.ts"; import { ManageSongs } from "./table.ts"; -export function ChangeSongs(drop: Drop) { +export function ChangeSongs(state: StateHandler<{ _id: string | undefined, songs: Song[], title: string | undefined, release: string | undefined, language: string | undefined, artists: Artist[], artwork: string | undefined, artworkClientData: AdvancedImage | string | undefined; compositionCopyright: string | undefined, soundRecordingCopyright: string | undefined, primaryGenre: string | undefined, secondaryGenre: string | undefined, loading: boolean; validationState: zod.ZodError | undefined; }>) { const { data, error, validate } = Validate( - State({ - drop - }), + state, zod.object({ - drop: zod.any(), + title: userString, + artists: artist.array().refine(x => x.some(([ , , type ]) => type == "PRIMARY"), { message: "At least one primary artist is required" }), + release: zod.string().regex(DATE_PATTERN, { message: "Not a date" }), + language: zod.string(), + primaryGenre: zod.string(), + secondaryGenre: zod.string(), + compositionCopyright: userString, + soundRecordingCopyright: userString, + artwork: zod.string(), + loading: zod.literal(false, { errorMap: () => ({ message: "Artwork is still uploading" }) }).transform(() => undefined), + songs: song.array().min(1, { message: "At least one song is required" }), }) ); return Grid( - ManageSongs(data), - Horizontal( - Spacer(), - Button("Add a new Song") - .onClick(() => createFilePicker(allowedAudioFormats.join(",")).then(file => uploadSongToDrop(data, file))) - ), - Button("Save Changes").onPromiseClick(async () => { - validate(); - if (error) return; - let obj = structuredClone(drop); - data.map(x => x.data.data).forEach(x => obj = { ...obj, ...x }); - await API.music.id(drop._id).update(obj); - location.reload(); // Handle this Smarter => Make it a Reload Event. - }) + [ + { width: 2 }, + Horizontal( + Box(data.$validationState.map(error => error ? CenterV( + Label(getErrorMessage(error)) + .addClass("error-message") + .setMargin("0 0.5rem 0 0") + ) + : Empty()).asRefComponent()), + Spacer(), + Button("Save") + .onClick(async () => { + const validation = validate(); + if (error.getValue()) return data.validationState = error.getValue(); + if (validation) await API.music.id(state._id!).update(validation); + location.reload(); // Handle this Smarter => Make it a Reload Event. + }) + ), + ], + [ + { width: 2 }, + ManageSongs(data), + ], + [ + { width: 2 }, + Horizontal( + Spacer(), + Button("Add a new Song") + .onClick(() => createFilePicker(allowedAudioFormats.join(",")).then(file => uploadSongToDrop(data, file))) + ) + ], ) .setGap("15px") .setPadding("15px 0 0 0"); - - // return Wizard({ - // submitAction: async (data) => { - // let obj = structuredClone(drop); - // data.map(x => x.data.data).forEach(x => obj = { ...obj, ...x }); - // await API.music.id(drop._id).update(obj); - // location.reload(); // Handle this Smarter => Make it a Reload Event. - // }, - // buttonArrangement: "flex-end", - // buttonAlignment: "top", - // }, ({ PageData }) => [ - // Page({ - // uploadingSongs: [], - // songs: drop.songs - // }, data => [ - // Grid( - // ManageSongs(data), - // Horizontal( - // Spacer(), - // Button("Add a new Song") - // .onClick(() => uploadFilesDialog((list) => uploadSongToDrop(data, getDropFromPages(PageData(), drop), list), allowedAudioFormats.join(","))) - // ) - // ) - // .setGap("15px") - // .setPadding("15px 0 0 0") - // ]).setValidator(() => pageFive) - // ] - // ); } \ No newline at end of file diff --git a/pages/_legacy/music/data.ts b/pages/_legacy/music/data.ts index 1bceb63..729236d 100644 --- a/pages/_legacy/music/data.ts +++ b/pages/_legacy/music/data.ts @@ -3,8 +3,6 @@ import { delay } from "std/async/delay.ts"; import { AdvancedImage, State, StateHandler } from "webgen/mod.ts"; import { Artist, Song } from "../../../spec/music.ts"; -// TODO: Remove all theses spread operator, update values directly, - export function uploadSongToDrop(state: StateHandler<{ uploadingSongs: string[]; songs: Song[]; artists: Artist[], language: string | undefined, primaryGenre: string | undefined, secondaryGenre: string | undefined, _id: string; }>, file: File) { const uploadId = crypto.randomUUID(); state.uploadingSongs.push(uploadId); diff --git a/pages/_legacy/music/edit.ts b/pages/_legacy/music/edit.ts index 8a41f2d..cd517f3 100644 --- a/pages/_legacy/music/edit.ts +++ b/pages/_legacy/music/edit.ts @@ -1,11 +1,13 @@ +import { ZodError } from "https://deno.land/x/zod@v3.22.4/mod.ts"; import { API, LoadingSpinner, Navigation, createActionList, createBreadcrumb, createTagList, stupidErrorAlert } from "shared/mod.ts"; -import { Body, Empty, Grid, Horizontal, Label, Spacer, State, Vertical, WebGen, isMobile } from "webgen/mod.ts"; +import { AdvancedImage, Body, Empty, Grid, Horizontal, Label, Spacer, State, Vertical, WebGen, isMobile } from "webgen/mod.ts"; import '../../../assets/css/main.css'; import '../../../assets/css/music.css'; import { DynaNavigation } from "../../../components/nav.ts"; -import { Drop, DropType } from "../../../spec/music.ts"; +import { Artist, Drop, DropType, Song } from "../../../spec/music.ts"; +import '../../hosting/views/table2.css'; import { DropTypeToText } from "../../music/views/list.ts"; -import { RegisterAuthRefresh, changeThemeColor, permCheck, renewAccessTokenIfNeeded, saveBlob, showPreviewImage } from "../helper.ts"; +import { RegisterAuthRefresh, changeThemeColor, permCheck, renewAccessTokenIfNeeded, saveBlob, sheetStack, showPreviewImage } from "../helper.ts"; import { ChangeDrop } from "./changeDrop.ts"; import { ChangeSongs } from "./changeSongs.ts"; @@ -25,17 +27,33 @@ if (!data.id) { } const state = State({ - drop: undefined, + loaded: false, + _id: data.id, + title: undefined!, + type: undefined, + release: undefined, + language: undefined, + artists: [], + primaryGenre: undefined, + secondaryGenre: undefined, + compositionCopyright: undefined, + soundRecordingCopyright: undefined, + artwork: undefined, + artworkClientData: undefined, + loading: false, + uploadingSongs: [], + songs: [], + validationState: undefined }); -Body(Vertical( +sheetStack.setDefault(Vertical( DynaNavigation("Music"), - state.$drop.map(drop => drop ? Navigation({ - title: drop.title, + state.$loaded.map(loaded => loaded ? Navigation({ + title: state.$title, children: [ Horizontal( //TODO: Make this look better - Label(DropTypeToText(drop.type)).setTextSize("2xl"), + Label(DropTypeToText(state.type!)).setTextSize("2xl"), Spacer() ), { @@ -43,7 +61,7 @@ Body(Vertical( title: "Drop", subtitle: "Change Title, Release Date, ...", children: [ - ChangeDrop(drop) + ChangeDrop(state) ] }, { @@ -51,7 +69,7 @@ Body(Vertical( title: "Songs", subtitle: "Move Songs, Remove Songs, Add Songs, ...", children: [ - ChangeSongs(drop), + ChangeSongs(state), ] }, { @@ -59,37 +77,37 @@ Body(Vertical( title: "Export", subtitle: "Download your complete Drop with every Song", clickHandler: async () => { - const blob = await API.music.id(drop._id).download().then(stupidErrorAlert); - saveBlob(blob, `${drop.title}.tar`); + const blob = await API.music.id(state._id).download().then(stupidErrorAlert); + saveBlob(blob, `${state.title}.tar`); } }, - Permissions.canCancelReview(drop) ? + Permissions.canCancelReview(state.type!) ? { id: "cancel-review", title: "Cancel Review", subtitle: "Need to change Something? Cancel it now", clickHandler: async () => { - await API.music.id(drop._id).type.post(DropType.Private); + await API.music.id(state._id).type.post(DropType.Private); location.reload(); }, } : Empty(), - Permissions.canSubmit(drop) ? + Permissions.canSubmit(state.type!) ? { id: "publish", title: "Publish", subtitle: "Submit your Drop for Approval", clickHandler: async () => { - await API.music.id(drop._id).type.post(DropType.UnderReview); + await API.music.id(state._id).type.post(DropType.UnderReview); location.reload(); }, } : Empty(), - Permissions.canTakedown(drop) ? + Permissions.canTakedown(state.type!) ? { id: "takedown", title: "Takedown", subtitle: "Completely Takedown your Drop", clickHandler: async () => { - await API.music.id(drop._id).type.post(DropType.Private); + await API.music.id(state._id).type.post(DropType.Private); location.reload(); }, } : Empty() @@ -103,7 +121,7 @@ Body(Vertical( const list = Vertical( menu.path.map(x => x == "-/" ? Grid( - showPreviewImage(drop).addClass("image-preview") + showPreviewImage({ _id: state._id, artwork: state.artwork }).addClass("image-preview") ).setEvenColumns(1, "10rem") : Empty() ).asRefComponent(), @@ -120,11 +138,31 @@ Body(Vertical( ).asRefComponent(), )); +Body(sheetStack); + const Permissions = { - canTakedown: (drop: Drop) => drop.type == "PUBLISHED", - canSubmit: (drop: Drop) => ([ "UNSUBMITTED", "PRIVATE" ]).includes(drop.type), - canEdit: (drop: Drop) => (drop.type == "PRIVATE" || drop.type == "UNSUBMITTED") || permCheck("/bbn/manage/drops"), - canCancelReview: (drop: Drop) => drop.type == "UNDER_REVIEW" + canTakedown: (type: DropType) => type == "PUBLISHED", + canSubmit: (type: DropType) => ([ "UNSUBMITTED", "PRIVATE" ]).includes(type), + canEdit: (type: DropType) => (type == "PRIVATE" || type == "UNSUBMITTED") || permCheck("/bbn/manage/drops"), + canCancelReview: (type: DropType) => type == "UNDER_REVIEW" }; -renewAccessTokenIfNeeded().then(async () => state.drop = await API.music.id(data.id).get().then(stupidErrorAlert)); \ No newline at end of file +renewAccessTokenIfNeeded().then(async () => { + await API.music.id(data.id).get() + .then(stupidErrorAlert) + .then(drop => { + state.type = drop.type; + state.title = drop.title; + state.release = drop.release; + state.language = drop.language; + state.artists = State(drop.artists ?? []); + state.primaryGenre = drop.primaryGenre; + state.secondaryGenre = drop.secondaryGenre; + state.compositionCopyright = drop.compositionCopyright; + state.soundRecordingCopyright = drop.soundRecordingCopyright; + state.artwork = drop.artwork; + state.artworkClientData = (drop.artwork ? { type: "direct", source: () => API.music.id(drop._id).artwork().then(stupidErrorAlert) } : undefined); + state.songs = State(drop.songs ?? []); + }) + .then(() => state.loaded = true); +}); \ No newline at end of file diff --git a/pages/_legacy/music/table.ts b/pages/_legacy/music/table.ts index 0e2a9e2..75e3fd7 100644 --- a/pages/_legacy/music/table.ts +++ b/pages/_legacy/music/table.ts @@ -2,7 +2,7 @@ import { Progress } from "shared/mod.ts"; import { Box, ButtonStyle, Checkbox, Color, DropDownInput, IconButton, Image, InlineTextInput, Label, MIcon, StateHandler } from "webgen/mod.ts"; import genres from "../../../data/genres.json" with { type: "json" }; import language from "../../../data/language.json" with { type: "json" }; -import { Song } from "../../../spec/music.ts"; +import { Artist, Song } from "../../../spec/music.ts"; import { Table2 } from "../../hosting/views/table2.ts"; import { EditArtistsDialog, ProfilePicture, getSecondary, getYearList } from "../helper.ts"; @@ -11,14 +11,13 @@ export function ManageSongs(state: StateHandler<{ songs: Song[]; primaryGenre: s .setColumnTemplate("auto max-content max-content max-content max-content max-content max-content min-content") .addColumn("Title", (song) => song.progress !== undefined ? Progress(song.progress) : InlineTextInput("text", "blur").addClass("low-level").sync(song, "title")) - .addColumn("Artists", (song) => Box( - ...song.artists.map(([ name, url, _type ]: string[]) => + .addColumn("Artists", (song) => + song.$artists.map(artists => Box(...artists.map(([ name, url, _type ]: Artist) => ProfilePicture(url ? Image(url, "A profile picture") : Label(""), name) - ), - IconButton(MIcon("add"), "add") - ) - .addClass("artists-list") - .onClick(() => EditArtistsDialog(song).open())) + ), IconButton(MIcon("add"), "add")) + .addClass("artists-list") + .onClick(() => EditArtistsDialog(song).open()) + ).asRefComponent()) .addColumn("Year", (song) => DropDownInput("Year", getYearList()) .setValue(song.year.toString()) .onChange(data => song.year = parseInt(data)) @@ -43,5 +42,5 @@ export function ManageSongs(state: StateHandler<{ songs: Song[]; primaryGenre: s .onClick((_, value) => song.explicit = !value) .addClass("low-level")) .addColumn("", (song) => IconButton(MIcon("delete"), "Delete").onClick(() => state.songs = state.songs.filter((x) => x.id != song.id) as typeof state.songs)) - .addClass("inverted-class", "light-mode"); + .addClass("inverted-class"); } \ No newline at end of file diff --git a/pages/_legacy/newDrop.ts b/pages/_legacy/newDrop.ts index fc5d119..dc9bfaf 100644 --- a/pages/_legacy/newDrop.ts +++ b/pages/_legacy/newDrop.ts @@ -6,7 +6,7 @@ import '../../assets/css/main.css'; import { DynaNavigation } from "../../components/nav.ts"; import genres from "../../data/genres.json" with { type: "json" }; import language from "../../data/language.json" with { type: "json" }; -import { Artist, DATE_PATTERN, DropType, Song, artist, userString } from "../../spec/music.ts"; +import { Artist, DATE_PATTERN, DropType, Song, artist, song, userString } from "../../spec/music.ts"; import '../hosting/views/table2.css'; import { CenterAndRight, EditArtistsDialog, RegisterAuthRefresh, allowedAudioFormats, allowedImageFormats, getSecondary, sheetStack } from "./helper.ts"; import { uploadArtwork, uploadSongToDrop } from "./music/data.ts"; @@ -46,8 +46,6 @@ API.music.id(dropId).get().then(stupidErrorAlert) }) .then(() => state.loaded = true); -// uploadingSongs: zod.array(zod.string()).max(0, { message: "Some uploads are still in progress" }).transform(_ => undefined), - const state = State({ loaded: false, _id: dropId, @@ -268,7 +266,7 @@ const wizard = state.$page.map(page => { state, zod.object({ artwork: zod.string(), - loading: zod.literal(false, { errorMap: () => ({ message: "Artwork is still uploading" }) }) + loading: zod.literal(false, { errorMap: () => ({ message: "Artwork is still uploading" }) }).transform(() => undefined), }) ); @@ -307,8 +305,8 @@ const wizard = state.$page.map(page => { const { error, validate } = Validate( state, zod.object({ - artwork: zod.string(), - loading: zod.literal(false, { errorMap: () => ({ message: "Artwork is still uploading" }) }) + songs: song.array().min(1, { message: "At least one song is required" }), + uploadingSongs: zod.array(zod.string()).max(0, { message: "Some uploads are still in progress" }), }) ); diff --git a/pages/admin/admin.ts b/pages/admin/admin.ts index 9026aa9..521efcc 100644 --- a/pages/admin/admin.ts +++ b/pages/admin/admin.ts @@ -1,7 +1,7 @@ import { Body, Vertical, WebGen } from "webgen/mod.ts"; import '../../assets/css/main.css'; import { DynaNavigation } from "../../components/nav.ts"; -import { RegisterAuthRefresh, changeThemeColor, permCheck, renewAccessTokenIfNeeded } from "../_legacy/helper.ts"; +import { RegisterAuthRefresh, changeThemeColor, permCheck, renewAccessTokenIfNeeded, sheetStack } from "../_legacy/helper.ts"; import './admin.css'; import { refreshState } from "./loading.ts"; import { adminMenu } from "./views/menu.ts"; @@ -21,7 +21,9 @@ WebGen({ } }); -Body(Vertical(DynaNavigation("Admin"), adminMenu)); +sheetStack.setDefault(Vertical(DynaNavigation("Admin"), adminMenu)); + +Body(sheetStack); renewAccessTokenIfNeeded() .then(() => refreshState()); \ No newline at end of file diff --git a/pages/admin/dialog.ts b/pages/admin/dialog.ts index 27a8088..dce945a 100644 --- a/pages/admin/dialog.ts +++ b/pages/admin/dialog.ts @@ -1,7 +1,8 @@ import { API } from "shared/mod.ts"; -import { Box, Checkbox, Custom, Horizontal, Image, Label, Spacer, State, createElement } from "webgen/mod.ts"; +import { Box, Checkbox, Custom, Horizontal, Image, Label, SheetDialog, Spacer, State, createElement } from "webgen/mod.ts"; import reviewTexts from "../../data/reviewTexts.json" with { type: "json" }; import { Drop, ReviewResponse } from "../../spec/music.ts"; +import { sheetStack } from "../_legacy/helper.ts"; import { clientRender, dropPatternMatching, rawTemplate, render } from "./email.ts"; function css(data: TemplateStringsArray, ...expr: string[]) { @@ -55,62 +56,63 @@ const rejectReasons = [ ReviewResponse.DeclineCopyright ]; export const dialogState = State({ drop: "loading" }); -export const ApproveDialog = Dialog(() => +export const ApproveDialog = SheetDialog(sheetStack, "Approve Drop", dialogState.$drop.map(drop => Box( drop === "loading" ? Box(Image({ type: "loading" }, "Loading...")).addClass("test") - : Wizard({ - buttonAlignment: "bottom", - buttonArrangement: 'flex-end', - cancelAction: () => { - ApproveDialog.close(); - }, - submitAction: async ([ { data: { data: { responseText } } } ]) => { - await API.music.id(drop._id).review.post({ - title: dropPatternMatching(reviewTexts.APPROVED.header, drop), - reason: [ "APPROVED" ], - body: rawTemplate(dropPatternMatching(responseText, drop)), - denyEdits: false - }); + : Box( + Box( + Label("Email Response"), + Custom((() => { + const ele = createElement("textarea"); + ele.rows = 10; + ele.value = data.responseText; + ele.style.resize = "vertical"; + ele.oninput = () => { + data.responseText = ele.value; + }; + return ele; + })()), + ) + .addClass("winput", "grayscaled", "has-value", "textarea") + .setMargin("0 0 .5rem"), + Label("Preview") + .setMargin("0 0 0.5rem"), + data.$responseText + .map(() => clientRender(dropPatternMatching(data.responseText, drop))) + .asRefComponent(), + ) + // : Wizard({ + // buttonAlignment: "bottom", + // buttonArrangement: 'flex-end', + // cancelAction: () => { + // ApproveDialog.close(); + // }, + // submitAction: async ([ { data: { data: { responseText } } } ]) => { + // await API.music.id(drop._id).review.post({ + // title: dropPatternMatching(reviewTexts.APPROVED.header, drop), + // reason: [ "APPROVED" ], + // body: rawTemplate(dropPatternMatching(responseText, drop)), + // denyEdits: false + // }); - ApproveDialog.close(); - }, - }, () => [ - Page({ - responseText: reviewTexts.APPROVED.content.join("\n"), - }, (data) => [ - // TODO: Put this Component into webgen directly and clean it up - Box( - Label("Email Response"), - Custom((() => { - const ele = createElement("textarea"); - ele.rows = 10; - ele.value = data.responseText; - ele.style.resize = "vertical"; - ele.oninput = () => { - data.responseText = ele.value; - }; - return ele; - })()), - ) - .addClass("winput", "grayscaled", "has-value", "textarea") - .setMargin("0 0 .5rem"), - Label("Preview") - .setMargin("0 0 0.5rem"), - data.$responseText - .map(() => clientRender(dropPatternMatching(data.responseText, drop))) - .asRefComponent(), - ]).setValidator((v) => v.object({ - responseText: v.string().refine(x => render(dropPatternMatching(x, drop)).errors.length == 0, { message: "Invalid MJML" }) - })) - ]), + // ApproveDialog.close(); + // }, + // }, () => [ + // Page({ + // responseText: reviewTexts.APPROVED.content.join("\n"), + // }, (data) => [ + // // TODO: Put this Component into webgen directly and clean it up + + // ]).setValidator((v) => v.object({ + // responseText: v.string().refine(x => render(dropPatternMatching(x, drop)).errors.length == 0, { message: "Invalid MJML" }) + // })) + // ]), ) .setMargin("0 0 var(--gap)") ).asRefComponent() -) - .allowUserClose() - .setTitle("Approve Drop"); +); export const DeclineDialog = Dialog(() => dialogState.$drop.map(drop => diff --git a/pages/admin/views/entryReview.ts b/pages/admin/views/entryReview.ts index af9b0ed..505e559 100644 --- a/pages/admin/views/entryReview.ts +++ b/pages/admin/views/entryReview.ts @@ -1,7 +1,7 @@ import { API } from "shared/mod.ts"; -import { Button, ButtonStyle, Color, DropDownInput, Entry, State, Vertical } from "webgen/mod.ts"; +import { Button, ButtonStyle, Color, DropDownInput, Entry, Horizontal, SheetDialog, Spacer, State, Vertical } from "webgen/mod.ts"; import { Drop, DropType } from "../../../spec/music.ts"; -import { showPreviewImage } from "../../_legacy/helper.ts"; +import { sheetStack, showPreviewImage } from "../../_legacy/helper.ts"; export function ReviewEntry(x: Drop) { return Entry({ @@ -22,14 +22,15 @@ export const changeState = State({ type: undefined }); -export const changeTypeDialog = Dialog(() => +export const changeTypeDialog = SheetDialog(sheetStack, "Change Drop Type", Vertical( - DropDownInput("Change Type", Object.values(DropType)).sync(changeState, "type") + DropDownInput("Change Type", Object.values(DropType)).sync(changeState, "type"), + Horizontal( + Spacer(), + Button("Change").onClick(() => { + API.music.id(changeState.drop!._id).type.post(changeState.type!); + changeTypeDialog.close(); + }) + ) ) -) - .addButton("Change", () => { - API.music.id(changeState.drop!._id).type.post(changeState.type!); - changeTypeDialog.close(); - }) - .allowUserClose() - .setTitle("Change Drop Type"); \ No newline at end of file +); \ No newline at end of file diff --git a/pages/admin/views/list.ts b/pages/admin/views/list.ts index 791e9a4..472d5de 100644 --- a/pages/admin/views/list.ts +++ b/pages/admin/views/list.ts @@ -1,8 +1,8 @@ import { API, External, fileCache, RenderItem, stupidErrorAlert } from "shared/mod.ts"; -import { Box, Button, Cache, Color, Entry, Grid, IconButton, Image, MIcon, ref, TextInput } from "webgen/mod.ts"; +import { Box, Button, Cache, Color, Entry, Grid, Horizontal, IconButton, Image, MIcon, ref, SheetDialog, Spacer, TextInput, Vertical } from "webgen/mod.ts"; import { templateArtwork } from "../../../assets/imports.ts"; import { File, OAuthApp, Transcript, Wallet } from "../../../spec/music.ts"; -import { saveBlob } from "../../_legacy/helper.ts"; +import { saveBlob, sheetStack } from "../../_legacy/helper.ts"; import { state } from "../state.ts"; export function entryWallet(wallet: Wallet) { @@ -63,14 +63,22 @@ export function entryOAuth(app: OAuthApp) { .addClass("small"); } -const oAuthViewDialog = (oauth: OAuthApp) => Dialog(() => - Grid( - TextInput("text", "Name").setValue(oauth.name).setColor(Color.Disabled), - TextInput("text", "Client ID").setValue(oauth._id).setColor(Color.Disabled), - TextInput("text", "Client Secret").setValue(oauth.secret).setColor(Color.Disabled), - TextInput("text", "Redirect URI").setValue(oauth.redirect.join(",")).setColor(Color.Disabled), +const oAuthViewDialog = (oauth: OAuthApp) => SheetDialog(sheetStack, "OAuth App Details", + Vertical( + Grid( + TextInput("text", "Name").setValue(oauth.name).setColor(Color.Disabled), + TextInput("text", "Client ID").setValue(oauth._id).setColor(Color.Disabled), + TextInput("text", "Client Secret").setValue(oauth.secret).setColor(Color.Disabled), + TextInput("text", "Redirect URI").setValue(oauth.redirect.join(",")).setColor(Color.Disabled), + ), + Horizontal( + Spacer(), + Button("Close").onClick(() => { + oAuthViewDialog(oauth).close(); + }) + ) ) -).allowUserClose().setTitle("OAuth App Details").addButton("Close", "remove"); +); export function entryFile(file: File) { return Entry({ diff --git a/pages/admin/views/menu.ts b/pages/admin/views/menu.ts index cf1a93d..a9b506d 100644 --- a/pages/admin/views/menu.ts +++ b/pages/admin/views/menu.ts @@ -1,8 +1,8 @@ import { API, HeavyList, loadMore, Navigation, placeholder } from "shared/mod.ts"; import { sumOf } from "std/collections/sum_of.ts"; -import { Box, Button, Color, Entry, Grid, Horizontal, isMobile, Label, ref, Spacer, State, Table, TextInput, Vertical } from "webgen/mod.ts"; +import { Box, Button, Color, Entry, Grid, Horizontal, isMobile, Label, ref, SheetDialog, Spacer, State, Table, TextInput, Vertical } from "webgen/mod.ts"; import { DropType } from "../../../spec/music.ts"; -import { activeUser } from "../../_legacy/helper.ts"; +import { activeUser, sheetStack } from "../../_legacy/helper.ts"; import { upload } from "../loading.ts"; import { state } from "../state.ts"; import { ReviewEntry } from "./entryReview.ts"; @@ -198,7 +198,7 @@ const oAuthData = State({ image: "" }); -const addOAuthDialog = Dialog(() => +const addOAuthDialog = SheetDialog(sheetStack, "Create new OAuth Application", Grid( Label("Create new OAuth Application"), TextInput("text", "Name").sync(oAuthData, "name"), @@ -246,6 +246,4 @@ const addOAuthDialog = Dialog(() => }) ).asRefComponent() ).setGap("10px") -) - .setTitle("Create new OAuth Application") - .allowUserClose(); \ No newline at end of file +); \ No newline at end of file diff --git a/spec/music.ts b/spec/music.ts index 268cd5a..bca8c12 100644 --- a/spec/music.ts +++ b/spec/music.ts @@ -52,7 +52,7 @@ export const song = zod.object({ id: zod.string(), isrc: zod.string().optional(), title: userString, - artists: artist.array().min(1), + artists: artist.array().refine(x => x.some(([ , , type ]) => type == "PRIMARY"), { message: "At least one primary artist is required" }), primaryGenre: zod.string(), secondaryGenre: zod.string(), year: zod.number(), @@ -70,10 +70,9 @@ export const drop = zod.object({ _id: zod.string(), upc: zod.string() .transform(x => x?.trim()) - // .transform(x => x?.length == 0 ? undefined : x) .refine(x => x == undefined || [ 12, 13 ].includes(x.length), { message: "Not a valid UPC" }), title: userString, - artists: artist.array().min(1), + artists: artist.array().refine(x => x.some(([ , , type ]) => type == "PRIMARY"), { message: "At least one primary artist is required" }), release: zod.string().regex(DATE_PATTERN, { message: "Not a date" }), language: zod.string(), primaryGenre: zod.string(),