diff --git a/src/app/api/user/[uid]/playlist/[playlistId]/route.ts b/src/app/api/user/[uid]/playlist/[playlistId]/route.ts index acb411e..0fd9f2d 100644 --- a/src/app/api/user/[uid]/playlist/[playlistId]/route.ts +++ b/src/app/api/user/[uid]/playlist/[playlistId]/route.ts @@ -25,24 +25,15 @@ export async function GET( spClient.setAccessToken(accessToken as string); - const data = await spClient.getPlaylistTracks(params.playlistId); - const tracks = data.body.items; - const total = data.body.total; - const limit = data.body.limit; - let offset = limit; - while (offset < total) { - const data = await spClient.getPlaylistTracks(params.playlistId, { - offset, - }); - tracks.push(...data.body.items); - offset += limit; - } + const response = await spClient.getPlaylist(params.playlistId); const playlist = { - id: params.playlistId, - tracks, - total, - actual: tracks.length, + id: response.body.id, + name: response.body.name, + description: response.body.description, + image: response.body.images?.[0]?.url, + total: response.body.tracks.total, + owner: response.body.owner.display_name, }; return NextResponse.json(playlist); diff --git a/src/app/api/user/[uid]/playlists/route.ts b/src/app/api/user/[uid]/playlists/route.ts index 6ca6a2b..9eca008 100644 --- a/src/app/api/user/[uid]/playlists/route.ts +++ b/src/app/api/user/[uid]/playlists/route.ts @@ -2,6 +2,15 @@ import { type NextRequest, NextResponse } from "next/server"; import SpotifyWebApi from "spotify-web-api-node"; import { env } from "~/env"; import { getAccessTokenFromProviderAccountId } from "~/server/db/helper"; +import Redis from "ioredis"; +import { Logger } from "~/lib/log"; + + +const logger = new Logger("/api/user/[uid]/playlists"); + +const redis = new Redis(env.REDIS_URL, { + maxRetriesPerRequest: null, +}); export async function GET( request: NextRequest, { @@ -15,11 +24,6 @@ export async function GET( return NextResponse.json("No access token found", { status: 500 }); } - // const connection = new Redis(env.REDIS_URL, { - // maxRetriesPerRequest: null, - // }); - - // get user playlists and format them like above const spClient = new SpotifyWebApi({ clientId: env.SPOTIFY_CLIENT_ID, clientSecret: env.SPOTIFY_CLIENT_SECRET, @@ -29,11 +33,28 @@ export async function GET( const q = request.nextUrl.searchParams.get("q"); + const cacheKey = q ? `search:${q}` : `user:${params.uid}`; + const cachedData = await redis.get(cacheKey); + + if (cachedData) { + const ttl = await redis.ttl(cacheKey); + logger.info(`Cache hit for ${cacheKey}`); + return NextResponse.json(JSON.parse(cachedData), { + headers: { + "X-Cache": "HIT", + "X-Cache-TTL": ttl.toString(), + }, + }); + } + + let data; + let playlists; + if (q) { - const data = await spClient.searchPlaylists(q, { + data = await spClient.searchPlaylists(q, { limit: 50, }); - const playlists = data.body.playlists?.items.map((playlist) => ({ + playlists = data.body.playlists?.items.map((playlist) => ({ playlistId: playlist.id, name: playlist.name, description: playlist.description, @@ -41,12 +62,11 @@ export async function GET( total: playlist.tracks.total, owner: playlist.owner.display_name, })); - return NextResponse.json(playlists); } else { - const data = await spClient.getUserPlaylists(params.uid, { + data = await spClient.getUserPlaylists(params.uid, { limit: 50, }); - const playlists = data.body.items.map((playlist) => ({ + playlists = data.body.items.map((playlist) => ({ playlistId: playlist.id, name: playlist.name, description: playlist.description, @@ -54,6 +74,9 @@ export async function GET( total: playlist.tracks.total, owner: playlist.owner.display_name, })); - return NextResponse.json(playlists); } + + await redis.set(cacheKey, JSON.stringify(playlists), "EX", 10); + + return NextResponse.json(playlists); } diff --git a/src/app/states/store.ts b/src/app/states/store.ts index f4f4ecd..0dbfa4c 100644 --- a/src/app/states/store.ts +++ b/src/app/states/store.ts @@ -34,6 +34,9 @@ type RFState = { onConnect: OnConnect; setNodes: (nodes: Node[]) => void; setEdges: (edges: Edge[]) => void; + + setNode: (id: string, node: Node) => void; + addNode: (data: any) => Node; addEdge: (data: any) => void; updateNodeData: (id: string, data: any) => void; @@ -99,6 +102,11 @@ const useStore = create((set, get) => ({ edges: edges, }); }, + setNode(id: string, node: Node) { + set((state) => ({ + nodes: state.nodes.map((n) => (n.id === id ? node : n)), + })); + }, onNodesChange(changes) { set({ nodes: applyNodeChanges(changes, get().nodes), diff --git a/src/app/utils/reactFlowToWorkflow.ts b/src/app/utils/reactFlowToWorkflow.ts index 26da25b..c160d20 100644 --- a/src/app/utils/reactFlowToWorkflow.ts +++ b/src/app/utils/reactFlowToWorkflow.ts @@ -39,9 +39,10 @@ function addNodesToWorkflow(nodes, workflow) { }, }); } else { + const typeWithoutPostfix = node.type!.split("-")[0]; workflow.operations.push({ id: node.id, - type: node.type!, + type: typeWithoutPostfix, params: node.data, position: node.position, sources: [], @@ -117,7 +118,7 @@ export default async function reactFlowToWorkflow({ let [valid, errors] = [true, {}]; if (nodes.length > 0 && edges.length > 0) { - nodes = filterNodes(nodes); + // nodes = filterNodes(nodes); addNodesToWorkflow(nodes, workflowObject); addEdgesToWorkflow(edges, workflowObject); setNodesAsSources(workflowObject); diff --git a/src/app/workflow/Builder.tsx b/src/app/workflow/Builder.tsx index 8059473..9b7f0e0 100644 --- a/src/app/workflow/Builder.tsx +++ b/src/app/workflow/Builder.tsx @@ -19,6 +19,7 @@ import { toast } from "sonner"; import { useRouter } from "next/navigation"; import { useWorkflowData } from "~/hooks/useWorkflowData"; +import React from "react"; function Builder({ params, @@ -203,4 +204,4 @@ const LoadingSVG = () => ( ); -export default Builder; +export default React.memo(Builder); diff --git a/src/app/workflow/Flow.tsx b/src/app/workflow/Flow.tsx index fcd2005..127cfe2 100644 --- a/src/app/workflow/Flow.tsx +++ b/src/app/workflow/Flow.tsx @@ -51,106 +51,108 @@ import { useRouter } from "next/navigation"; import { toast } from "sonner"; import { runWorkflow } from "~/app/utils/runWorkflow"; +import { memo } from "react"; + export const Nodes = { "Combiner.alternate": { title: "Alternate", - node: Alternate, + node: memo(Alternate), description: "Alternate between playlists", }, "Combiner.push": { title: "Push", - node: Push, + node: memo(Push), description: "Append tracks of sources sequentially", }, "Filter.dedupeTracks": { title: "Deduplicate Tracks", - node: DedupeTracks, + node: memo(DedupeTracks), description: "Remove duplicate tracks", }, "Filter.dedupeArtists": { title: "Deduplicate Artists", - node: DedupeArtists, + node: memo(DedupeArtists), description: "Remove duplicate artists", }, "Filter.filter": { title: "Filter", - node: RemoveMatch, + node: memo(RemoveMatch), description: "Match and remove tracks", }, "Filter.limit": { title: "Limit", - node: Limit, + node: memo(Limit), description: "Limit number of tracks", }, "Source.playlist": { title: "Playlist", - node: Playlist, + node: memo(Playlist), description: "Playlist source", }, "Library.likedTracks": { title: "Liked Tracks", - node: LikedTracks, + node: memo(LikedTracks), description: "Liked tracks", }, "Library.saveAsNew": { title: "Save as New", - node: SaveAsNew, + node: memo(SaveAsNew), description: "Saves workflow output to a new playlist", }, "Library.saveAsAppend": { title: "Save as Append", - node: SaveAsAppend, + node: memo(SaveAsAppend), description: "Saves workflow output to an existing playlist by appending", }, "Library.saveAsReplace": { title: "Save as Replace", - node: SaveAsReplace, + node: memo(SaveAsReplace), description: "Saves workflow output to an existing playlist by replacing all tracks", }, "Order.shuffle": { title: "Shuffle", - node: Shuffle, + node: memo(Shuffle), description: "Randomly shuffle tracks", }, "Order.sort": { title: "Sort", - node: Sort, + node: memo(Sort), description: "Sort tracks based on given key", }, "Order.sort-popularity": { title: "Sort Tracks by Popularity", - node: SortPopularity, + node: memo(SortPopularity), description: "Sort tracks based on popularity", }, "Selector.allButFirst": { title: "All But First", - node: AllButFirst, + node: memo(AllButFirst), description: "Selects all but the first item from the input", }, "Selector.allButLast": { title: "All But Last", - node: AllButLast, + node: memo(AllButLast), description: "Selects all but the last item from the input", }, "Selector.first": { title: "First", - node: First, + node: memo(First), description: "Selects the first item from the input", }, "Selector.last": { title: "Last", - node: Last, + node: memo(Last), description: "Selects the last item from the input", }, "Selector.recommend": { title: "Recommend", - node: Recommend, + node: memo(Recommend), description: "Get a list of recommended tracks based on the input.", }, }; -export default function App() { +export function App() { const reactFlowWrapper = useRef(null); const { nodes, @@ -161,6 +163,7 @@ export default function App() { addNode, getEdges, getNodes, + setNode, onNodesDelete, flowState, reactFlowInstance, @@ -175,6 +178,7 @@ export default function App() { addNode: state.addNode, getEdges: state.getEdges, getNodes: state.getNodes, + setNode: state.setNode, onNodesDelete: state.onNodesDelete, flowState: state.flowState, reactFlowInstance: state.reactFlowInstance, @@ -184,32 +188,91 @@ export default function App() { const router = useRouter(); - const nodeTypes = useMemo(() => Object.fromEntries( - Object.entries(Nodes).map(([key, value]) => [key, value.node]) - ), []); - - const onDragDrop = useCallback((event) => { - event.preventDefault(); - event.dataTransfer.dropEffect = "move"; - - const type = event.dataTransfer.getData("application/reactflow"); - - if (typeof type === "undefined" || !type) { - return; - } - - const position = reactFlowInstance!.screenToFlowPosition({ - x: event.clientX, - y: event.clientY, - }); - const newNode = { - type, - position, - data: {}, - }; - - addNode(newNode); - }, [reactFlowInstance, addNode]); + const nodeTypes = useMemo( + () => + Object.fromEntries( + Object.entries(Nodes).map(([key, value]) => [key, value.node]), + ), + [], + ); + + const onDragDrop = useCallback( + (event) => { + event.preventDefault(); + event.dataTransfer.dropEffect = "move"; + + const url = event.dataTransfer.getData("text/plain"); + + if (url?.includes("spotify.com")) { + const position = reactFlowInstance!.screenToFlowPosition({ + x: event.clientX, + y: event.clientY, + }); + + const playlistId = url.split("/playlist/")[1]; + + // Create a placeholder node + const placeholderNode = { + id: `placeholder-${playlistId}`, + type: "Source.playlist", + position, + data: { + id: `placeholder-${playlistId}`, + playlistId: `placeholder-${playlistId}`, + name: "Loading...", + description: "", + image: "", + total: 0, + owner: "", + }, + }; + + const placeholder = addNode(placeholderNode); + + fetch(`/api/user/@me/playlist/${playlistId}`) + .then((response) => response.json()) + .then((data) => { + const newNode = { + ...placeholderNode, + id: data.id, + data: { + id: data.id, + playlistId: data.id, + name: data.name, + description: data.description, + image: data.image, + total: data.total, + owner: data.owner, + }, + }; + + setNode(placeholder.id, newNode); + }) + .catch((error) => { + console.error("Error fetching playlist details:", error); + }); + } else { + const type = event.dataTransfer.getData("application/reactflow"); + + if (typeof type === "undefined" || !type) { + return; + } + + const position = reactFlowInstance!.screenToFlowPosition({ + x: event.clientX, + y: event.clientY, + }); + const newNode = { + type, + position, + data: {}, + }; + + addNode(newNode); + } + }, + [reactFlowInstance, addNode, setNode], + ); const isValidConnection = useCallback( (connection) => { @@ -266,9 +329,9 @@ export default function App() { onNodesDelete={onNodesDelete} isValidConnection={isValidConnection} fitView - snapToGrid={true} + // snapToGrid={true} nodeTypes={nodeTypes} - snapGrid={[20, 20]} + // snapGrid={[20, 20]} zoomOnDoubleClick={false} deleteKeyCode={["Backspace", "Delete"]} onPaneContextMenu={(e) => { @@ -320,3 +383,5 @@ export default function App() { ); } + +export default memo(App); diff --git a/src/components/nodes/Combiner/Alternate.tsx b/src/components/nodes/Combiner/Alternate.tsx index 89d62c0..2c2bf3d 100644 --- a/src/components/nodes/Combiner/Alternate.tsx +++ b/src/components/nodes/Combiner/Alternate.tsx @@ -16,45 +16,39 @@ type PlaylistProps = { data: any; }; -const AlternateComponent: React.FC = React.memo( - ({ id, data }) => { - const { state, isValid, targetConnections, sourceConnections } = - useBasicNodeState(id); +const AlternateComponent: React.FC = ({ id, data }) => { + const { state, isValid, targetConnections, sourceConnections } = + useBasicNodeState(id); - return ( - - + + +
+ + - -
- - -
- - ); - }, -); +
+
+ ); +}; export default AlternateComponent; diff --git a/src/components/nodes/Filter/DedupeArtists.tsx b/src/components/nodes/Filter/DedupeArtists.tsx index f9614d6..23e7917 100644 --- a/src/components/nodes/Filter/DedupeArtists.tsx +++ b/src/components/nodes/Filter/DedupeArtists.tsx @@ -16,55 +16,49 @@ type PlaylistProps = { data: any; }; -const DedupeArtistsComponent: React.FC = React.memo( - ({ id, data }) => { - const { state, isValid, targetConnections, sourceConnections } = - useBasicNodeState(id); +const DedupeArtistsComponent: React.FC = ({ id, data }) => { + const { state, isValid, targetConnections, sourceConnections } = + useBasicNodeState(id); - return ( - - + + +
+ + - -
- - -
- - ); - }, -); +
+
+ ); +}; export default DedupeArtistsComponent; diff --git a/src/components/nodes/Filter/DedupeTracks.tsx b/src/components/nodes/Filter/DedupeTracks.tsx index b790060..d9f269c 100644 --- a/src/components/nodes/Filter/DedupeTracks.tsx +++ b/src/components/nodes/Filter/DedupeTracks.tsx @@ -16,55 +16,49 @@ type PlaylistProps = { data: any; }; -const DedupeTracksComponent: React.FC = React.memo( - ({ id, data }) => { - const { state, isValid, targetConnections, sourceConnections } = - useBasicNodeState(id); +const DedupeTracksComponent: React.FC = ({ id, data }) => { + const { state, isValid, targetConnections, sourceConnections } = + useBasicNodeState(id); - return ( - - + + +
+ + - -
- - -
- - ); - }, -); +
+
+ ); +}; export default DedupeTracksComponent; diff --git a/src/components/nodes/Filter/Limit.tsx b/src/components/nodes/Filter/Limit.tsx index e584cfa..60b99df 100644 --- a/src/components/nodes/Filter/Limit.tsx +++ b/src/components/nodes/Filter/Limit.tsx @@ -29,7 +29,7 @@ const formSchema = z.object({ limit: z.number().int().positive().default(0), }); -const LimitComponent: React.FC = React.memo(({ id, data }) => { +const LimitComponent: React.FC = ({ id, data }) => { const { state, isValid, @@ -124,6 +124,6 @@ const LimitComponent: React.FC = React.memo(({ id, data }) => { /> ); -}); +}; export default LimitComponent; diff --git a/src/components/nodes/Filter/RemoveMatch.tsx b/src/components/nodes/Filter/RemoveMatch.tsx index ac64336..f27a818 100644 --- a/src/components/nodes/Filter/RemoveMatch.tsx +++ b/src/components/nodes/Filter/RemoveMatch.tsx @@ -55,7 +55,7 @@ const selectOptions = [ { label: "Not equal to (!=)", value: "!=" }, ]; -const RemoveMatch: React.FC = React.memo(({ id, data }) => { +const RemoveMatch: React.FC = ({ id, data }) => { const { state, isValid, @@ -196,6 +196,6 @@ const RemoveMatch: React.FC = React.memo(({ id, data }) => { /> ); -}); +}; export default RemoveMatch; diff --git a/src/components/nodes/Library/LikedTracks.tsx b/src/components/nodes/Library/LikedTracks.tsx index bd777b6..3cbd336 100644 --- a/src/components/nodes/Library/LikedTracks.tsx +++ b/src/components/nodes/Library/LikedTracks.tsx @@ -34,116 +34,112 @@ const formSchema = z.object({ offset: z.number().optional(), }); -const PlaylistComponent: React.FC = React.memo( - ({ id, data }) => { - const { - state, - isValid, - targetConnections, - sourceConnections, - form, - formState, - register, - getNodeData, - updateNodeData, - } = useBasicNodeState(id, formSchema); - - React.useEffect(() => { - if (data) { - form!.reset({ - limit: data.limit, - offset: data.offset, - }); - } - }, [data, form]); - - const watch = form!.watch(); - const session = useStore((state) => state.session); - - const formValid = formState!.isValid; - const nodeValid = React.useMemo(() => { - return formValid && isValid; - }, [formValid, isValid]); - - React.useEffect(() => { - updateNodeData(id, { - playlistId: "likedTracks", - name: "Liked Tracks", - description: "A list of the songs saved in your ‘Your Music’ library.", - image: "https://misc.scdn.co/liked-songs/liked-songs-300.png", - total: watch.limit ?? 50, - owner: session.user.name, +const PlaylistComponent: React.FC = ({ id, data }) => { + const { + state, + isValid, + targetConnections, + sourceConnections, + form, + formState, + register, + getNodeData, + updateNodeData, + } = useBasicNodeState(id, formSchema); + + React.useEffect(() => { + if (data) { + form!.reset({ + limit: data.limit, + offset: data.offset, }); - }, [watch.limit, id, updateNodeData, session.user.name]); - - return ( - - - -
- console.info(data))}> -
- - - - Config - - - state.session); + + const formValid = formState!.isValid; + const nodeValid = React.useMemo(() => { + return formValid && isValid; + }, [formValid, isValid]); + + React.useEffect(() => { + updateNodeData(id, { + playlistId: "likedTracks", + name: "Liked Tracks", + description: "A list of the songs saved in your ‘Your Music’ library.", + image: "https://misc.scdn.co/liked-songs/liked-songs-300.png", + total: watch.limit ?? 50, + owner: session.user.name, + }); + }, [watch.limit, id, updateNodeData, session.user.name]); + + return ( + + + + + console.info(data))}> +
+ + + Config + + - + - - - -
- - - -
- ); - }, -); + /> +
+
+
+
+ + + +
+ ); +}; export default PlaylistComponent; diff --git a/src/components/nodes/Library/Playlist.tsx b/src/components/nodes/Library/Playlist.tsx index 9a07b7f..11dd55b 100644 --- a/src/components/nodes/Library/Playlist.tsx +++ b/src/components/nodes/Library/Playlist.tsx @@ -83,7 +83,7 @@ const PlaylistItem = ({ ); -function PlaylistComponent({ id, data }: PlaylistProps) { +const PlaylistComponent: React.FC = ({ id, data }) => { const [open, setOpen] = React.useState(false); const [selectedPlaylist, setSelectedPlaylist] = React.useState(data); @@ -112,8 +112,10 @@ function PlaylistComponent({ id, data }: PlaylistProps) { React.useEffect(() => { if (data) { form?.setValue("playlistId", data.playlistId); + updateNodeData(id, data); + form?.trigger("playlistId"); } - }, [data, form]); + }, [data, form, id, updateNodeData]); const watch = form!.watch(); const prevWatchRef = React.useRef(watch); @@ -351,6 +353,6 @@ function PlaylistComponent({ id, data }: PlaylistProps) { /> ); -} +}; export default PlaylistComponent; diff --git a/src/components/nodes/Library/SaveAsNew.tsx b/src/components/nodes/Library/SaveAsNew.tsx index e983fb3..91f805c 100644 --- a/src/components/nodes/Library/SaveAsNew.tsx +++ b/src/components/nodes/Library/SaveAsNew.tsx @@ -30,112 +30,110 @@ const formSchema = z.object({ description: z.string().optional(), }); -const saveAsNewComponent: React.FC = React.memo( - ({ id, data }) => { - const { - state, - isValid, - targetConnections, - sourceConnections, - form, - nodeData, - formState, - register, - getNodeData, - updateNodeData, - } = useBasicNodeState(id, formSchema); +const saveAsNewComponent: React.FC = ({ id, data }) => { + const { + state, + isValid, + targetConnections, + sourceConnections, + form, + nodeData, + formState, + register, + getNodeData, + updateNodeData, + } = useBasicNodeState(id, formSchema); - const watch = form!.watch(); - const prevWatchRef = React.useRef(watch); + const watch = form!.watch(); + const prevWatchRef = React.useRef(watch); - React.useEffect(() => { - if (JSON.stringify(prevWatchRef.current) !== JSON.stringify(watch)) { - updateNodeData(id, { - name: watch.name, - isPublic: watch.isPublic === "true", - collaborative: watch.collaborative === "true", - description: watch.description, - }); - } - prevWatchRef.current = watch; - }, [id, watch, updateNodeData]); + React.useEffect(() => { + if (JSON.stringify(prevWatchRef.current) !== JSON.stringify(watch)) { + updateNodeData(id, { + name: watch.name, + isPublic: watch.isPublic === "true", + collaborative: watch.collaborative === "true", + description: watch.description, + }); + } + prevWatchRef.current = watch; + }, [id, watch, updateNodeData]); - return ( - + +
+ console.info(data))}> +
+ + + + + + + +
+
+ + + - -
- console.info(data))}> -
- - - - - - - -
-
- - - -
- ); - }, -); + isValid={isValid} + TargetConnections={targetConnections} + SourceConnections={sourceConnections} + /> + + ); +}; export default saveAsNewComponent; diff --git a/src/components/nodes/Order/Shuffle.tsx b/src/components/nodes/Order/Shuffle.tsx index 8f1b36c..69c1c8b 100644 --- a/src/components/nodes/Order/Shuffle.tsx +++ b/src/components/nodes/Order/Shuffle.tsx @@ -16,45 +16,39 @@ type PlaylistProps = { data: any; }; -const DedupeArtistsComponent: React.FC = React.memo( - ({ id, data }) => { - const { state, isValid, targetConnections, sourceConnections } = - useBasicNodeState(id); +const DedupeArtistsComponent: React.FC = ({ id, data }) => { + const { state, isValid, targetConnections, sourceConnections } = + useBasicNodeState(id); - return ( - - + + +
+ + - -
- - -
- - ); - }, -); +
+
+ ); +}; export default DedupeArtistsComponent; diff --git a/src/components/nodes/Order/Sort.tsx b/src/components/nodes/Order/Sort.tsx index e3c4d6e..0a37102 100644 --- a/src/components/nodes/Order/Sort.tsx +++ b/src/components/nodes/Order/Sort.tsx @@ -42,7 +42,7 @@ const sortOptions = [ { label: "Descending", value: "desc" }, ]; -const RemoveMatch: React.FC = React.memo(({ id, data }) => { +const RemoveMatch: React.FC = ({ id, data }) => { const { isValid, targetConnections, @@ -151,6 +151,6 @@ const RemoveMatch: React.FC = React.memo(({ id, data }) => { /> ); -}); +}; export default RemoveMatch; diff --git a/src/components/nodes/Order/SortPopularity.tsx b/src/components/nodes/Order/SortPopularity.tsx index 64ec64d..79d6605 100644 --- a/src/components/nodes/Order/SortPopularity.tsx +++ b/src/components/nodes/Order/SortPopularity.tsx @@ -19,6 +19,9 @@ const sortOptions = [ ]; const SortPopularity: React.FC = ({ id, data }: any) => { + data.sortKey = "popularity"; + data.sortOrder = "desc"; + return ( ; - formFields: (args: { form: any, register: any }) => React.ReactNode; + formFields: (args: { form: any; register: any }) => React.ReactNode; extraContent?: React.ReactNode; handleConnectionType?: "source" | "target" | "both"; } -const NodeBuilder: React.FC = React.memo( - ({ - id, - data, - title, - type, - info, - formSchema, - formFields, - extraContent, - handleConnectionType = "both", - }) => { - const { - state, - isValid, - targetConnections, - sourceConnections, - form, - formState, - register, - getNodeData, - updateNodeData, - } = useBasicNodeState(id, formSchema); +const NodeBuilder: React.FC = ({ + id, + data, + title, + type, + info, + formSchema, + formFields, + extraContent, + handleConnectionType = "both", +}) => { + const { + state, + isValid, + targetConnections, + sourceConnections, + form, + formState, + register, + getNodeData, + updateNodeData, + } = useBasicNodeState(id, formSchema); - // Handle data updates from props or form - React.useEffect(() => { - if (data) { - form!.reset(data); - } - }, [data, form]); + // Handle data updates from props or form + React.useEffect(() => { + if (data) { + form!.reset(data); + } + }, [data, form]); - const watch = form!.watch(); - const prevWatchRef = React.useRef(watch); - React.useEffect(() => { - if (JSON.stringify(prevWatchRef.current) !== JSON.stringify(watch)) { - updateNodeData(id, { - ...watch, - }); - } - prevWatchRef.current = watch; - }, [watch, id, updateNodeData]); + const watch = form!.watch(); + const prevWatchRef = React.useRef(watch); + React.useEffect(() => { + if (JSON.stringify(prevWatchRef.current) !== JSON.stringify(watch)) { + updateNodeData(id, { + ...watch, + }); + } + prevWatchRef.current = watch; + }, [watch, id, updateNodeData]); - const formValid = formState!.isValid; - const nodeValid = React.useMemo(() => { - return formValid && isValid; - }, [formValid, isValid]); + const formValid = formState!.isValid; + const nodeValid = React.useMemo(() => { + return formValid && isValid; + }, [formValid, isValid]); - return ( - - {handleConnectionType !== "target" && ( - - )} - {handleConnectionType !== "source" && ( - - )} - {form && formFields && ( -
- console.info(data))}> - {formFields({ form, register })} -
- - )} - {extraContent} - + {handleConnectionType !== "target" && ( + + )} + {handleConnectionType !== "source" && ( + -
- ); - }, -); + )} + {form && formFields && ( +
+ console.info(data))}> + {formFields({ form, register })} +
+ + )} + {extraContent} + + + ); +}; export default NodeBuilder; diff --git a/src/components/nodes/Selectors/AllButFirst.tsx b/src/components/nodes/Selectors/AllButFirst.tsx index 81005e2..1613444 100644 --- a/src/components/nodes/Selectors/AllButFirst.tsx +++ b/src/components/nodes/Selectors/AllButFirst.tsx @@ -16,45 +16,39 @@ type PlaylistProps = { data: any; }; -const AllButFirstComponent: React.FC = React.memo( - ({ id, data }) => { - const { state, isValid, targetConnections, sourceConnections } = - useBasicNodeState(id); +const AllButFirstComponent: React.FC = ({ id, data }) => { + const { state, isValid, targetConnections, sourceConnections } = + useBasicNodeState(id); - return ( - - + + +
+ + - -
- - -
- - ); - }, -); +
+
+ ); +}; export default AllButFirstComponent; diff --git a/src/components/nodes/Selectors/AllButLast.tsx b/src/components/nodes/Selectors/AllButLast.tsx index da55334..44452e6 100644 --- a/src/components/nodes/Selectors/AllButLast.tsx +++ b/src/components/nodes/Selectors/AllButLast.tsx @@ -16,45 +16,39 @@ type PlaylistProps = { data: any; }; -const AllButLastComponent: React.FC = React.memo( - ({ id, data }) => { - const { state, isValid, targetConnections, sourceConnections } = - useBasicNodeState(id); +const AllButLastComponent: React.FC = ({ id, data }) => { + const { state, isValid, targetConnections, sourceConnections } = + useBasicNodeState(id); - return ( - - + + +
+ + - -
- - -
- - ); - }, -); +
+
+ ); +}; export default AllButLastComponent; diff --git a/src/components/nodes/Selectors/First.tsx b/src/components/nodes/Selectors/First.tsx index 96fa16d..4ac7c0a 100644 --- a/src/components/nodes/Selectors/First.tsx +++ b/src/components/nodes/Selectors/First.tsx @@ -16,7 +16,7 @@ type PlaylistProps = { data: any; }; -const First: React.FC = React.memo(({ id, data }) => { +const First: React.FC = ({ id, data }) => { const { state, isValid, targetConnections, sourceConnections } = useBasicNodeState(id); @@ -49,6 +49,6 @@ const First: React.FC = React.memo(({ id, data }) => { ); -}); +}; export default First; diff --git a/src/components/nodes/Selectors/Last.tsx b/src/components/nodes/Selectors/Last.tsx index d2a0bfd..4d478b0 100644 --- a/src/components/nodes/Selectors/Last.tsx +++ b/src/components/nodes/Selectors/Last.tsx @@ -16,7 +16,7 @@ type PlaylistProps = { data: any; }; -const LastComponent: React.FC = React.memo(({ id, data }) => { +const LastComponent: React.FC = ({ id, data }) => { const { state, isValid, targetConnections, sourceConnections } = useBasicNodeState(id); @@ -49,6 +49,6 @@ const LastComponent: React.FC = React.memo(({ id, data }) => { ); -}); +}; export default LastComponent; diff --git a/src/components/nodes/Selectors/Recommend.tsx b/src/components/nodes/Selectors/Recommend.tsx index 1fd38fd..f593d57 100644 --- a/src/components/nodes/Selectors/Recommend.tsx +++ b/src/components/nodes/Selectors/Recommend.tsx @@ -34,7 +34,7 @@ const selectOptions = [ { label: "Artists", value: "artists" }, ]; -const Recommend: React.FC = React.memo(({ id, data }) => { +const Recommend: React.FC = ({ id, data }) => { const { state, isValid, @@ -145,6 +145,6 @@ const Recommend: React.FC = React.memo(({ id, data }) => { /> ); -}); +}; export default Recommend; diff --git a/src/hooks/useBasicNodeState.ts b/src/hooks/useBasicNodeState.ts index f27ab37..c5e3fff 100644 --- a/src/hooks/useBasicNodeState.ts +++ b/src/hooks/useBasicNodeState.ts @@ -1,6 +1,9 @@ import { useHandleConnections } from "@xyflow/react"; -import { useCallback, useEffect, useMemo, useState } from "react"; +import { useCallback, useEffect, useState } from "react"; import useStore from "~/app/states/store"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { useForm } from "react-hook-form"; +import { type ZodObject } from "zod"; type Playlist = { playlistId?: string; @@ -11,17 +14,12 @@ type Playlist = { total?: number; }; -import { zodResolver } from "@hookform/resolvers/zod"; -import { useForm } from "react-hook-form"; - -import { type ZodObject } from "zod"; - const usePlaylistLogic = (id: string, formSchema?: ZodObject) => { const { nodes, updateNodeData } = useStore((state) => ({ nodes: state.nodes, updateNodeData: state.updateNodeData, })); - const [isValid, setIsValid] = useState(false); + const [state, setState] = useState({ playlists: [] as Playlist[], playlistIds: [] as string[], @@ -31,13 +29,11 @@ const usePlaylistLogic = (id: string, formSchema?: ZodObject) => { }, }); - const form = formSchema - ? useForm({ - resolver: zodResolver(formSchema), - shouldUnregister: false, - mode: "all", - }) - : undefined; + const form = useForm({ + resolver: formSchema ? zodResolver(formSchema) : undefined, + shouldUnregister: false, + mode: "all", + }); const { formState, register } = form ?? {}; @@ -56,13 +52,6 @@ const usePlaylistLogic = (id: string, formSchema?: ZodObject) => { nodeId: id, }); - const total = useMemo( - () => - Array.isArray(state.playlists) - ? state.playlists.reduce((acc, curr) => acc + (curr.total ?? 0), 0) - : 0, - [state.playlists], - ); useEffect(() => { let invalidNodesCount = 0; const playlistIdsSet = new Set(); @@ -102,8 +91,6 @@ const usePlaylistLogic = (id: string, formSchema?: ZodObject) => { invalidNodesCount++; } - setIsValid(hasPlaylistId || hasPlaylistIds); - const playlist: Playlist = { playlistId: playlistId as string, name: name as string, @@ -130,12 +117,14 @@ const usePlaylistLogic = (id: string, formSchema?: ZodObject) => { playlists: combinedPlaylists, invalidNodesCount, summary: { - total, + total: combinedPlaylists.reduce( + (acc, curr) => acc + (curr.total ?? 0), + 0, + ), }, }); - }, [targetConnections, getNodeData, total]); + }, [targetConnections, getNodeData]); - // biome-ignore lint/correctness/useExhaustiveDependencies: useEffect(() => { const currentNodeData = getNodeData(id); if ( @@ -147,23 +136,14 @@ const usePlaylistLogic = (id: string, formSchema?: ZodObject) => { playlists: state.playlists, }); } - }, [ - id, - targetConnections, - getNodeData, - updateNodeData, - total, - targetConnections, - sourceConnections, - state.playlistIds, - state.playlists, - ]); + }, [id, getNodeData, updateNodeData, state.playlistIds, state.playlists]); return { state, - setState, isValid: - isValid && state.invalidNodesCount === 0 && targetConnections.length > 0, + state.invalidNodesCount === 0 && + targetConnections.length > 0 && + state.playlists.length > 0, targetConnections, sourceConnections, nodeData: getNodeData(id), diff --git a/src/middlewares/handlers/prettyPath.ts b/src/middlewares/handlers/prettyPath.ts index d0d86a5..d16ee7d 100644 --- a/src/middlewares/handlers/prettyPath.ts +++ b/src/middlewares/handlers/prettyPath.ts @@ -61,7 +61,10 @@ export const prettyPath = ( ) => { return async (request: NextRequest, _next: NextFetchEvent) => { const { pathname } = request.nextUrl; - if (matchPaths.some((path) => pathname.startsWith(path))) { + if ( + matchPaths.some((path) => pathname.startsWith(path)) && + !pathname.includes("queue") + ) { logger.info("Match!"); if (pathname.startsWith("/workflow")) {