diff --git a/package-lock.json b/package-lock.json index 5ffee117..e7a97bf2 100644 --- a/package-lock.json +++ b/package-lock.json @@ -130,6 +130,7 @@ "@storybook/react-vite": "^10.4.3", "@tanstack/form-core": "^2.0.0-alpha.2", "@tanstack/react-form": "^2.0.0-alpha.2", + "@tanstack/react-pacer": "^0.24.0", "@tanstack/react-query": "^5.101.0", "@tanstack/react-query-devtools": "^5.101.0", "@tanstack/react-virtual": "^3.14.3", @@ -5670,6 +5671,23 @@ "node": ">=10" } }, + "node_modules/@tanstack/devtools-event-client": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/@tanstack/devtools-event-client/-/devtools-event-client-0.5.0.tgz", + "integrity": "sha512-H+OH3zC6Vhu/K0NaVfQKknEKawc/+2PT+D3SB3Ox0V8SiMlTo0abbmH2rH0721R2aNYbjdMXA1oENOd8E2UVoA==", + "dev": true, + "license": "MIT", + "bin": { + "intent": "bin/intent.js" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + } + }, "node_modules/@tanstack/form-core": { "version": "2.0.0-alpha.2", "resolved": "https://registry.npmjs.org/@tanstack/form-core/-/form-core-2.0.0-alpha.2.tgz", @@ -5688,6 +5706,24 @@ "url": "https://github.com/sponsors/tannerlinsley" } }, + "node_modules/@tanstack/pacer": { + "version": "0.23.0", + "resolved": "https://registry.npmjs.org/@tanstack/pacer/-/pacer-0.23.0.tgz", + "integrity": "sha512-sUA+uAebh8qeXNiaoPLsS6pawnX5zknhrWMkXNuqx18AOJeDPo5gzx3VlpABl80BiARleUEQeVpWZjElbmkrdw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@tanstack/devtools-event-client": "^0.5.0", + "@tanstack/store": "^0.11.1" + }, + "engines": { + "node": ">=20" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + } + }, "node_modules/@tanstack/pacer-lite": { "version": "0.2.2", "resolved": "https://registry.npmjs.org/@tanstack/pacer-lite/-/pacer-lite-0.2.2.tgz", @@ -5745,6 +5781,28 @@ "react": "^18.0.0 || ^19.0.0" } }, + "node_modules/@tanstack/react-pacer": { + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@tanstack/react-pacer/-/react-pacer-0.24.0.tgz", + "integrity": "sha512-9prCIQjDKF8Pk+ytDzAjmi2uvWaY/LlXkNTjkgMZMyA371YGfcajzpkGYg/jlea3IQp7LHwGpwvKFOBxfMAKRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@tanstack/pacer": "0.23.0", + "@tanstack/react-store": "^0.11.1" + }, + "engines": { + "node": ">=20" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + }, + "peerDependencies": { + "react": ">=16.8", + "react-dom": ">=16.8" + } + }, "node_modules/@tanstack/react-query": { "version": "5.101.0", "resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.101.0.tgz", diff --git a/package.json b/package.json index 59349e63..6ebb5643 100644 --- a/package.json +++ b/package.json @@ -172,6 +172,7 @@ "@storybook/react-vite": "^10.4.3", "@tanstack/form-core": "^2.0.0-alpha.2", "@tanstack/react-form": "^2.0.0-alpha.2", + "@tanstack/react-pacer": "^0.24.0", "@tanstack/react-query": "^5.101.0", "@tanstack/react-query-devtools": "^5.101.0", "@tanstack/react-virtual": "^3.14.3", diff --git a/src/client/components/icons/ChakraIcons.tsx b/src/client/components/icons/ChakraIcons.tsx index b74a9d10..e041067e 100644 --- a/src/client/components/icons/ChakraIcons.tsx +++ b/src/client/components/icons/ChakraIcons.tsx @@ -43,6 +43,7 @@ import type {IconBaseProps, IconType} from "react-icons/lib"; // below are from https://selfh.st/icons/ import LZ from "./custom/listenbrainz.svg?react"; import Musicbrainz from "./custom/musicbrainz.svg?react"; +import Rocksky from "./custom/rocksky.svg?react"; import { SiSpotify, @@ -233,6 +234,8 @@ export const getMusicServiceIcon = (service?: string): IconType => { return SiPlex; case 'listenbrainz': return LZ as unknown as IconType; + case 'rocksky': + return Rocksky as unknown as IconType; default: return LuExternalLink; } diff --git a/src/client/components/icons/custom/rocksky.svg b/src/client/components/icons/custom/rocksky.svg new file mode 100644 index 00000000..0eb9ab33 --- /dev/null +++ b/src/client/components/icons/custom/rocksky.svg @@ -0,0 +1,85 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/client/components/play/ArtistSearch.tsx b/src/client/components/play/ArtistSearch.tsx new file mode 100644 index 00000000..647a9b37 --- /dev/null +++ b/src/client/components/play/ArtistSearch.tsx @@ -0,0 +1,141 @@ +import { Box, Combobox, useListCollection, Stack, Text, Avatar, Portal, HStack, Span, Spinner, Separator, Icon } from "@chakra-ui/react" +import { MSErrorBoundary } from '../ErrorBoundary.js'; +import { useDebouncedState } from '@tanstack/react-pacer' +import { useQuery } from '@tanstack/react-query'; +import { tanQueries } from "../../queries/index.js"; +import type { ArtistSearchResult } from "../../../core/Api.js"; +import { MusicbrainzInfoIcon } from "../musicServices/Musicbrainz.js"; +import { useEffect, useState } from "react"; +import { getMusicServiceIconElement } from "../icons/ChakraIcons.js"; + +export const ArtistSearchResultItem = (props: { data: ArtistSearchResult }) => ( + + {props.data.image !== undefined ? ( + + + + + ) : undefined} + + + + {props.data.name} {props.data.mbid !== undefined ? : null} + + + + + {props.data.score !== undefined ? <>Score {props.data.score} : undefined} + From {getMusicServiceIconElement(props.data.service)} + + + + +) + +export interface ArtistSearchProps { + initial?: string + onChange: (val: ArtistSearchResult) => void +} + +export const ArtistSearch = (props: ArtistSearchProps) => { + + const { + initial = '', + onChange = (val) => console.log(val, 'Selected value for prop') + } = props; + + const [rawInput, setRawInput] = useState(undefined); + const [debouncedQuery, setDebouncedQuery] = useDebouncedState(initial, { wait: 500 }); + + const query = useQuery({ + enabled: debouncedQuery !== '', + ...tanQueries.metadata.artists(debouncedQuery) + }); + + const { collection, set } = useListCollection({ + initialItems: query.data?.data ?? [], + itemToString: (item) => item.name, + itemToValue: (item) => item.id, + }); + + useEffect(() => { + if(query.isSuccess) { + set(query.data.data); + } + },[query, set]) + + return ( + + + { + if(e.key === 'Enter') { + if(rawInput !== undefined) { + console.log('enter and onChange rawInput'); + onChange({id: 'nonce', service: 'user', name: rawInput}); + } else { + console.log('enter noop'); + } + } + }} + collection={collection} + onInteractOutside={(e) => { + if(rawInput !== undefined) { + console.log('outside interact and onChange rawInput'); + onChange({id: 'nonce', service: 'user', name: rawInput}); + } else { + console.log('outside interact noop'); + } + }} + onValueChange={(val) => { + console.log(val, 'value change'); + onChange(val.items[0]); + setRawInput(undefined); + }} + onSelect={(val) => { + console.log(val, 'select') + }} + onInputValueChange={(e) => { + setDebouncedQuery(e.inputValue); + setRawInput(e.inputValue) + }} + width="320px" + > + {/* Select framework */} + + + + + + + + + + + {query.isLoading ? ( + + + Loading... + + ) : query.isError ? ( + + Error fetching + + ) : ( + collection.items?.map((item, i) => ( + + + + + )) + )} + No items found + + + + + + + ); +} \ No newline at end of file diff --git a/src/client/components/play/PlayEdit.tsx b/src/client/components/play/PlayEdit.tsx new file mode 100644 index 00000000..4151eca4 --- /dev/null +++ b/src/client/components/play/PlayEdit.tsx @@ -0,0 +1,71 @@ +import { HStack, Tabs, Box, Float } from "@chakra-ui/react" +import { playEditStrictCreateSchema, type JsonPlayObject, type PlayObjectMinimal } from '../../../core/Atomic.js'; +import { MSErrorBoundary } from '../ErrorBoundary.js'; +import { useForm, formOptions } from '@tanstack/react-form'; + + +export interface PlayEditProps { + initialPlay?: JsonPlayObject | PlayObjectMinimal + initialTab?: 'edit' | 'search' + context?: 'create' | 'edit' + onSubmit: (vals: any) => void +} + +export const PlayEdit = (props: PlayEditProps) => { + + const { + initialPlay: { + data = {}, + meta = {} + } = {} + } = props; + + const opts = formOptions.strictSchema(playEditStrictCreateSchema, { + defaultValues: { + data: { + track: '', + artists: [], + ...data + }, + meta: { + ...meta + } + }, + validators: [{ + run: playEditStrictCreateSchema, + triggers: [], + }], + }); + + const form = useForm({ + ...opts, + onSubmit: ({ schemaOutputs }) => { + props.onSubmit(schemaOutputs[0]); + //props.setOpen(false); + } + }); + + + return ( + + + + + + + + + + {props.context === 'create' ? 'Create' : 'Edit'} + Search + + + + + Search here + + + + + ); +} \ No newline at end of file diff --git a/src/client/queries/index.ts b/src/client/queries/index.ts index 723a4102..d9b0b17f 100644 --- a/src/client/queries/index.ts +++ b/src/client/queries/index.ts @@ -4,7 +4,7 @@ import { useEffect, useState, useMemo } from 'react'; import ky from 'ky'; import qs from 'qs'; import { baseUrl } from "../utils"; -import type {ComponentsApiJson, PaginatedResponse, PlayApiCommonDetailed, PlayStateUI, QueryPlaysOptsJson} from "../../core/Api"; +import type {ArtistSearchResultResponse, ComponentsApiJson, PaginatedResponse, PlayApiCommonDetailed, PlayStateUI, QueryPlaysOptsJson} from "../../core/Api"; import { type SourcePlayerJson } from "../../core/Atomic"; import { queryPlayOptsRefreshableToJson } from "../utils/ComponentUtils"; @@ -74,7 +74,19 @@ const logs = createQueryKeys('logs', { }) }) -export const tanQueries = mergeQueryKeys(components, activities, players, logs); +const metadata = createQueryKeys('metadata', { + artists: (query: string) => ({ + queryKey: ['metadata', 'artists', query], + queryFn: (ctx) => ky.get(`artists`, { + baseUrl, + searchParams: { + q: query + } + }).json() + }) +}); + +export const tanQueries = mergeQueryKeys(components, activities, players, logs, metadata); export const useQueryState = (queryKey: Readonly) => { const queryClient = useQueryClient() diff --git a/src/core/Api.ts b/src/core/Api.ts index 460bb89b..be21469a 100644 --- a/src/core/Api.ts +++ b/src/core/Api.ts @@ -1,5 +1,5 @@ import type { CompareOpKey, ComponentMinimalSelect } from "../backend/common/database/drizzle/drizzleTypes.ts" -import type { ClientType, ComponentAuthType, DeepReplaceValue, MonitoringStatus, QueueContext } from "./Atomic.ts" +import { artistCreditSchema, type ClientType, type ComponentAuthType, type DeepReplaceValue, type MonitoringStatus, type QueueContext } from "./Atomic.ts" import type { SourceType } from "./Atomic.ts" import type { ComponentType, DateLike, ErrorLike, JsonPlayObject, PlayState, QueueName, SOURCE_SOT_TYPES, SourcePlayerJson } from "./Atomic.ts" import type { Dayjs } from "dayjs" @@ -239,4 +239,18 @@ export const playStateBodySchema = z.object({ reason: z.string().optional() }); -export type PlayStateBody = z.infer; \ No newline at end of file +export type PlayStateBody = z.infer; + +export const artistSearchResultSchema = z.object({ + ...artistCreditSchema.shape, + id: z.string(), + image: z.string().optional(), + service: z.string(), + score: z.int().nonnegative().optional(), +}); +export type ArtistSearchResult = z.infer; + +export const artistSearchResultResponseSchema = z.object({ + data: artistSearchResultSchema.array() +}); +export type ArtistSearchResultResponse = z.infer; \ No newline at end of file diff --git a/src/core/Atomic.ts b/src/core/Atomic.ts index b94d75aa..f915fb3e 100644 --- a/src/core/Atomic.ts +++ b/src/core/Atomic.ts @@ -94,6 +94,8 @@ export interface ListenRangeData extends ListenRangeDataAmb { end: PlayProgress } +export const mbidSchema = z.stringFormat('mbid',/[a-zA-Z0-9]{8}-[a-zA-Z0-9]{4}-[a-zA-Z0-9]{4}-[a-zA-Z0-9]{4}-[a-zA-Z0-9]{12}/); + /** https://musicbrainz.org/doc/MusicBrainz_Database/Schema#Overview */ export interface BrainzMeta { /** @@ -130,11 +132,27 @@ export interface BrainzMeta { track?: string } +export const brainzMetaSchema = z.object({ + artists: mbidSchema.array().optional(), + albumArtists: mbidSchema.array().optional(), + album: mbidSchema.optional(), + recording: mbidSchema.optional(), + releaseGroup: mbidSchema.optional(), + trackNumber: z.int().positive().optional(), + track: mbidSchema.optional() +}) + export interface ArtistCredit { name: string mbid?: string } +export const artistCreditSchema = z.object({ + name: z.string(), + mbid: mbidSchema.optional(), + spotifyId: z.string().optional() +}) + export interface SpotifyMeta { artist?: string[] albumArtist?: string[] @@ -142,11 +160,23 @@ export interface SpotifyMeta { track?: string } +export const spotifyMeta = z.object({ + artists: z.string().array().optional(), + albumArtist: z.string().array().optional(), + album: z.string().optional(), + track: z.string().optional() +}) + export interface TrackMeta { brainz?: BrainzMeta spotify?: SpotifyMeta } +export const trackMetaSchema = z.object({ + brainz: brainzMetaSchema.optional(), + spotify: spotifyMeta.optional() +}) + export interface TrackMetaIsrc extends TrackMeta { isrc?: string } @@ -170,6 +200,22 @@ export interface TrackData { isrc?: string } +export const playTrackDataSchema = z.object({ + track: z.string().optional(), + artists: artistCreditSchema.array().optional(), + albumArtists: artistCreditSchema.array().optional(), + album: z.string().optional(), + duration: z.int().nonnegative().optional(), + isrc: z.string().optional(), + meta: trackMetaSchema.optional() +}) + +export const playTrackStrictDataSchema = z.object({ + ...playTrackDataSchema.shape, + track: z.string().nonempty(), + artists: artistCreditSchema.array().min(1) +}) + export interface PlayData extends TrackData { /** * The date the track was played at @@ -182,12 +228,40 @@ export interface PlayData extends TrackData { repeat?: boolean } +export const playDataSchema = z.object({ + ...playTrackDataSchema.shape, + playDate: z.string().optional(), + listenedFor: z.int().nonnegative().optional() +}); + +export const playDataStrictSchema = z.object({ + ...playTrackStrictDataSchema.shape, + playDate: z.string().optional(), + listenedFor: z.int().nonnegative().optional() +}); + + export interface ArtMeta { album?: string track?: string artist?: string } +export const artMetaSchema = z.object({ + album: z.string().optional(), + track: z.string().optional(), + artist: z.string().optional() +}); + +export const playMetaUrlSchema = z.looseObject({ + web: z.string().optional(), + origin: z.string().optional() +}) + +export const parsedFromSchema = z.enum(['backlog','now playing','player','history','ingress']) +export type PARSED_FROM_TYPE = z.infer;// 'backlog' | 'now playing' | 'player' | 'history' | 'ingress'; +export const PARSED_FROM = parsedFromSchema.enum; + export type PlayMeta = Merge, T>; export interface PlayMetaBase { @@ -280,6 +354,15 @@ export interface PlayMetaBase { //[key: string]: any } +export const playMetaSchema = z.looseObject({ + source: z.string().optional(), + musicService: z.string().optional(), + trackId: z.string().optional(), + parsedFrom: parsedFromSchema.optional(), + url: playMetaUrlSchema.optional(), + art: artMetaSchema.optional() +}) + export interface LifecycleInput { type: string, input: (object | string) } @@ -386,6 +469,17 @@ export interface ObjectPlayData extends PlayData { playDateCompleted?: Dayjs } + +export const playEditCreateSchema = z.object({ + data: playDataSchema, + meta: playMetaSchema +}); + +export const playEditStrictCreateSchema = z.object({ + data: playDataStrictSchema, + meta: playMetaSchema +}); + export const logLevelStandaloneSchema = z.enum(['debug','error','verbose','info','silly','silent','log','trace','warn','fatal']) export type LogLevelStandalone = z.infer; @@ -487,15 +581,6 @@ export const SOURCE_SOT = { } as const satisfies Record export const sourceSotTypes: SOURCE_SOT_TYPES[] = ['player','history','ingress']; -export type PARSED_FROM_TYPE = 'backlog' | 'now playing' | 'player' | 'history' | 'ingress'; -export const PARSED_FROM = { - backlog : 'backlog', - nowPlaying: 'now playing', - ingress: 'ingress', - player: 'player', - history: 'history' -} as const satisfies Record - export interface URLData { url: URL normal: string diff --git a/src/core/tests/utils/apiFixtures.ts b/src/core/tests/utils/apiFixtures.ts index c3df356f..e78c86c2 100644 --- a/src/core/tests/utils/apiFixtures.ts +++ b/src/core/tests/utils/apiFixtures.ts @@ -1,7 +1,7 @@ import { faker } from "@faker-js/faker"; -import type {ComponentClientApi, ComponentClientApiJson, ComponentCommonApi, ComponentCommonApiJson, ComponentHistoricalApi, ComponentSourceApi, ComponentSourceApiJson, ComponentState, PlayApiCommon, PlayApiCommonDetailed, PlayInputApi, QueueStateApi} from "../../Api.ts"; -import { INGRESS_QUEUE, COMPONENT_AUTH_TYPE, type ComponentType, type JsonPlayObject, type PlayObject, QUEUE_STATUSES, type SourcePlayerJson, sourceSotTypes } from "../../Atomic.ts"; -import { generatePlay, normalizePlays } from "./PlayTestUtils.ts"; +import type {ArtistSearchResult, ComponentClientApi, ComponentClientApiJson, ComponentCommonApi, ComponentCommonApiJson, ComponentHistoricalApi, ComponentSourceApi, ComponentSourceApiJson, ComponentState, PlayApiCommon, PlayApiCommonDetailed, PlayInputApi, QueueStateApi} from "../../Api.ts"; +import { INGRESS_QUEUE, COMPONENT_AUTH_TYPE, type ComponentType, type JsonPlayObject, type PlayObject, QUEUE_STATUSES, type SourcePlayerJson, sourceSotTypes, type MBID } from "../../Atomic.ts"; +import { generateArtist, generateArtistCredit, generatePlay, normalizePlays } from "./PlayTestUtils.ts"; import { generatePlayInput, generatePlayWithLifecycle, playWithLifecycleScrobble, randomPlayState } from "./fixtures.ts"; import { asJsonPlayObject } from "../../PlayMarshalUtils.ts"; import { generatePlayUid } from "../../StringUtils.ts"; @@ -15,6 +15,7 @@ import { REPORTED_PLAYER_STATUSES } from '../../Atomic.ts'; import { generateArray } from "../../DataUtils.ts"; import type {ErrorIsh} from "../../ErrorUtils.ts"; import { serializeError } from "serialize-error"; +import { stripIndents } from "common-tags"; export const generatePlayApiCommon = (commonData: Partial & {play?: JsonPlayObject | PlayObject } = {}, ...playOpts: Parameters): PlayApiCommon => { let play: JsonPlayObject | PlayObject; @@ -517,4 +518,35 @@ export const generateNormalWSUrl = () => { () => `${faker.word.noun({length :1})}:${faker.helpers.arrayElement([443,80,faker.internet.port()])}` ]) return g(); +} + +export const generateArtistSearchResult = (partial: Partial = {}, opts: {mbidVal?: boolean | MBID} = {}): ArtistSearchResult => ({ + ...generateArtistCredit(partial.name, opts.mbidVal), + id: faker.string.alphanumeric(7), + score: faker.number.int({min: 10, max: 100}), + service: faker.helpers.arrayElement(['spotify','musicbrainz','rocksky']), + image: placeholderImage([300]) +}) + +export const generateArtistSearchResults = (opts: {query?: string, count?: number} = {}): ArtistSearchResult[] => { + const results: ArtistSearchResult[] = []; + if(opts.count === 0) { + return results; + } + const c = opts.count ?? faker.number.int({min: 2, max: 5}); + if(opts.query !== undefined) { + results.push(generateArtistSearchResult({name: opts.query})) + } + while(results.length !== c) { + results.push(generateArtistSearchResult()) + } + results.sort((a,b) => b.score - a.score); + return results; +} + +const placeholderImage = (size: [number, number?], foregroundColor?: string, backgroundColor?: string) => { + const fore = foregroundColor ?? 'black'; + const back = backgroundColor ?? faker.color.rgb().slice(1) + return stripIndents` +https://placehold.co/${size[0]}${size[1] !== undefined ? `x${size[1]}` : ''}/${fore}/${back}` } \ No newline at end of file diff --git a/src/core/vendor/listenbrainz/interfaces.ts b/src/core/vendor/listenbrainz/interfaces.ts index c5ad7070..c19a5c45 100644 --- a/src/core/vendor/listenbrainz/interfaces.ts +++ b/src/core/vendor/listenbrainz/interfaces.ts @@ -1,5 +1,7 @@ import * as z from "zod"; + + /* * https://musicbrainz.org/doc/MusicBrainz_Database/Schema#Overview */ diff --git a/src/stories/plays/ArtistResult.stories.tsx b/src/stories/plays/ArtistResult.stories.tsx new file mode 100644 index 00000000..f11c736f --- /dev/null +++ b/src/stories/plays/ArtistResult.stories.tsx @@ -0,0 +1,34 @@ +import preview from "../../../.storybook/preview.js"; +import React from 'react'; + +import { ArtistSearchResultItem } from "../../client/components/play/ArtistSearch.js"; +import { Provider } from "../../client/components/Provider.js"; +import { Container } from '@chakra-ui/react'; +import { generateArtistSearchResult } from "../../core/tests/utils/apiFixtures.js"; + +type PropsAndCustomArgs = React.ComponentProps & { + initial?: string +}; +// More on how to set up stories at: https://storybook.js.org/docs/writing-stories#default-export +const meta = preview.type<{ args: PropsAndCustomArgs }>().meta({ + title: 'Plays/Search/Artist Search', + component: ArtistSearchResultItem, + parameters: { + // Optional parameter to center the component in the Canvas. More info: https://storybook.js.org/docs/configure/story-layout + layout: 'padded', + }, + // This component will have an automatically generated Autodocs entry: https://storybook.js.org/docs/writing-docs/autodocs + tags: ['autodocs'], + decorators: [ + (Story) => (), + ], + args: { + data: generateArtistSearchResult(), + }, + // Use `fn` to spy on the onClick arg, which will appear in the actions panel once invoked: https://storybook.js.org/docs/essentials/actions#story-args +}); + +// More on writing stories with args: https://storybook.js.org/docs/writing-stories/args +export const ArtistResultStory = meta.story({ + render: (args) => +}); \ No newline at end of file diff --git a/src/stories/plays/ArtistSearch.stories.tsx b/src/stories/plays/ArtistSearch.stories.tsx new file mode 100644 index 00000000..bf0da8f8 --- /dev/null +++ b/src/stories/plays/ArtistSearch.stories.tsx @@ -0,0 +1,47 @@ +import preview from "../../../.storybook/preview.js"; +import React from 'react'; + +import { ArtistSearch } from "../../client/components/play/ArtistSearch.js"; +import { Provider } from "../../client/components/Provider.js"; +import { Container } from '@chakra-ui/react'; +import { generateArtistCredits, generateJsonPlay, withBrainz } from "../../core/tests/utils/PlayTestUtils.js" +import clone from "clone"; +import { asJsonPlayObject } from '../../core/PlayMarshalUtils.js'; +import type { JsonPlayObject } from "../../core/Atomic.js"; +import { http, HttpResponse } from 'msw'; +import { generateArtistSearchResults } from "../../core/tests/utils/apiFixtures.js"; + +type PropsAndCustomArgs = React.ComponentProps & { + initial?: string +}; +// More on how to set up stories at: https://storybook.js.org/docs/writing-stories#default-export +const meta = preview.type<{ args: PropsAndCustomArgs }>().meta({ + title: 'Plays/Search/Artist Search', + component: ArtistSearch, + parameters: { + // Optional parameter to center the component in the Canvas. More info: https://storybook.js.org/docs/configure/story-layout + layout: 'padded', + msw: { + handlers: [ + http.get<{ query: string }>('/api/artists', async ({ params }) => { + return HttpResponse.json({data: generateArtistSearchResults()}); + }), + ], + }, + }, + // This component will have an automatically generated Autodocs entry: https://storybook.js.org/docs/writing-docs/autodocs + tags: ['autodocs'], + decorators: [ + (Story) => (), + ], + args: { + initial: undefined, + onSubmit: (val) => console.log(val) + }, + // Use `fn` to spy on the onClick arg, which will appear in the actions panel once invoked: https://storybook.js.org/docs/essentials/actions#story-args +}); + +// More on writing stories with args: https://storybook.js.org/docs/writing-stories/args +export const ArtistSearchStory = meta.story({ + render: (args) => +}); \ No newline at end of file diff --git a/src/stories/plays/PlayEdit.stories.tsx b/src/stories/plays/PlayEdit.stories.tsx new file mode 100644 index 00000000..53283891 --- /dev/null +++ b/src/stories/plays/PlayEdit.stories.tsx @@ -0,0 +1,59 @@ +import preview from "../../../.storybook/preview.js"; +import React from 'react'; + +import { PlayEdit } from "../../client/components/play/PlayEdit"; +import {Provider} from "../../client/components/Provider.js"; +import { Container } from '@chakra-ui/react'; +import { generateArtistCredits, generateJsonPlay, withBrainz } from "../../core/tests/utils/PlayTestUtils.js" +import clone from "clone"; +import { asJsonPlayObject } from '../../core/PlayMarshalUtils.js'; +import type {JsonPlayObject} from "../../core/Atomic.js"; + +type PropsAndCustomArgs = React.ComponentProps & { + includeAlbumArtists?: boolean; + initialPlay?: boolean + brainz?: boolean + play: JsonPlayObject +}; +// More on how to set up stories at: https://storybook.js.org/docs/writing-stories#default-export +const meta = preview.type<{args: PropsAndCustomArgs}>().meta({ + title: 'Plays/Play Edit', + component: PlayEdit, + parameters: { + // Optional parameter to center the component in the Canvas. More info: https://storybook.js.org/docs/configure/story-layout + layout: 'padded', + }, + // This component will have an automatically generated Autodocs entry: https://storybook.js.org/docs/writing-docs/autodocs + tags: ['autodocs'], +decorators: [ + (Story) => (), + ], +args: { + play: generateJsonPlay(), + includeAlbumArtists: false, + initialPlay: false, + brainz: false + }, + // Use `fn` to spy on the onClick arg, which will appear in the actions panel once invoked: https://storybook.js.org/docs/essentials/actions#story-args +}); + +// More on writing stories with args: https://storybook.js.org/docs/writing-stories/args +export const PlayEditStory = meta.story({ + render: function Render(args) { + + if(args.initialPlay === false) { + return ; + } + + if(args.includeAlbumArtists && (args.play.data.albumArtists === undefined || args.play.data.albumArtists.length === 0)) { + const aa = generateArtistCredits(undefined, 2, {mbidVal: true}); + args.play.data.albumArtists = aa; + } + + if(args.brainz) { + // @ts-ignore + args.play = asJsonPlayObject(withBrainz(args.play, {include: ['album','recording','track','artist']})); + } + return () + } +}); \ No newline at end of file