react-youtube-playlist-grid 1.0.5 → 1.0.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.mts CHANGED
@@ -1,11 +1,11 @@
1
1
  import React from 'react';
2
2
 
3
- type VideoColumn = 'thumbnail' | 'title' | 'description' | 'publishedAt';
3
+ type VideoField = 'description' | 'publishedAt';
4
4
  interface PlaylistConfig {
5
5
  id: string;
6
6
  showDescription: boolean;
7
7
  title: string;
8
- columns?: VideoColumn[];
8
+ extraVideoFields?: VideoField[];
9
9
  gridColumns?: number;
10
10
  }
11
11
  interface Video {
@@ -43,7 +43,7 @@ interface VideoCardProps {
43
43
  publishedAt?: string;
44
44
  }
45
45
  interface VideoCardPropsInternal extends VideoCardProps {
46
- columns?: VideoColumn[];
46
+ extraVideoFields?: VideoField[];
47
47
  }
48
48
  declare const VideoCard: React.FC<VideoCardPropsInternal>;
49
49
 
@@ -55,4 +55,4 @@ interface FetchVideosResult {
55
55
  declare function fetchPlaylistVideos(playlistId: string, apiKey: string, pageToken?: string): Promise<FetchVideosResult>;
56
56
  declare function getPlaylist(playlistId: string, apiKey: string): Promise<Playlist | null>;
57
57
 
58
- export { type Playlist, type PlaylistConfig, PlaylistsExplorer, type Video, VideoCard, type VideoColumn, PlaylistsExplorer as default, fetchPlaylistVideos, getPlaylist };
58
+ export { type Playlist, type PlaylistConfig, PlaylistsExplorer, type Video, VideoCard, type VideoField, PlaylistsExplorer as default, fetchPlaylistVideos, getPlaylist };
package/dist/index.d.ts CHANGED
@@ -1,11 +1,11 @@
1
1
  import React from 'react';
2
2
 
3
- type VideoColumn = 'thumbnail' | 'title' | 'description' | 'publishedAt';
3
+ type VideoField = 'description' | 'publishedAt';
4
4
  interface PlaylistConfig {
5
5
  id: string;
6
6
  showDescription: boolean;
7
7
  title: string;
8
- columns?: VideoColumn[];
8
+ extraVideoFields?: VideoField[];
9
9
  gridColumns?: number;
10
10
  }
11
11
  interface Video {
@@ -43,7 +43,7 @@ interface VideoCardProps {
43
43
  publishedAt?: string;
44
44
  }
45
45
  interface VideoCardPropsInternal extends VideoCardProps {
46
- columns?: VideoColumn[];
46
+ extraVideoFields?: VideoField[];
47
47
  }
48
48
  declare const VideoCard: React.FC<VideoCardPropsInternal>;
49
49
 
@@ -55,4 +55,4 @@ interface FetchVideosResult {
55
55
  declare function fetchPlaylistVideos(playlistId: string, apiKey: string, pageToken?: string): Promise<FetchVideosResult>;
56
56
  declare function getPlaylist(playlistId: string, apiKey: string): Promise<Playlist | null>;
57
57
 
58
- export { type Playlist, type PlaylistConfig, PlaylistsExplorer, type Video, VideoCard, type VideoColumn, PlaylistsExplorer as default, fetchPlaylistVideos, getPlaylist };
58
+ export { type Playlist, type PlaylistConfig, PlaylistsExplorer, type Video, VideoCard, type VideoField, PlaylistsExplorer as default, fetchPlaylistVideos, getPlaylist };
package/dist/index.js CHANGED
@@ -1,3 +1,3 @@
1
- 'use strict';Object.defineProperty(exports,'__esModule',{value:true});var react=require('react'),jsxRuntime=require('react/jsx-runtime');var P=({title:i,thumbnailUrl:a,videoUrl:l,description:n,publishedAt:d,columns:r=["thumbnail","title"]})=>{let f=o=>o?new Date(o).toLocaleDateString("en-US",{year:"numeric",month:"short",day:"numeric"}):"";return jsxRuntime.jsxs("a",{href:l,target:"_blank",rel:"noopener noreferrer",className:"rypg-card",children:[r.includes("thumbnail")&&jsxRuntime.jsxs("div",{className:"rypg-card-media",children:[jsxRuntime.jsx("img",{src:a,alt:i,className:"rypg-card-img",loading:"lazy"}),jsxRuntime.jsx("div",{className:"rypg-card-overlay",children:jsxRuntime.jsx("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 24 24",fill:"currentColor",className:"rypg-play-icon",children:jsxRuntime.jsx("path",{fillRule:"evenodd",d:"M4.5 5.653c0-1.426 1.529-2.33 2.779-1.643l11.54 6.348c1.295.712 1.295 2.573 0 3.285L7.28 19.991c-1.25.687-2.779-.217-2.779-1.643V5.653z",clipRule:"evenodd"})})})]}),jsxRuntime.jsxs("div",{className:"rypg-card-content",children:[r.includes("title")&&jsxRuntime.jsx("h3",{className:"rypg-card-title",children:i}),r.includes("description")&&n&&jsxRuntime.jsx("p",{className:"rypg-card-description",style:{fontSize:"0.875rem",marginTop:"0.5rem",opacity:.8},children:n}),r.includes("publishedAt")&&d&&jsxRuntime.jsx("p",{className:"rypg-card-date",style:{fontSize:"0.75rem",marginTop:"0.5rem",opacity:.6},children:f(d)})]})]})};var k="https://www.googleapis.com/youtube/v3";async function u(i,a,l){if(!a)return {error:"API Key is missing"};try{let n=`${k}/playlistItems?part=snippet,contentDetails&playlistId=${i}&maxResults=10&key=${a}`;l&&(n+=`&pageToken=${l}`);let r=await(await fetch(n)).json();return r.items?{videos:r.items.map(o=>({id:o.snippet.resourceId.videoId,title:o.snippet.title,thumbnailUrl:o.snippet.thumbnails.medium?.url||o.snippet.thumbnails.default?.url,videoUrl:`https://www.youtube.com/watch?v=${o.snippet.resourceId.videoId}`,description:o.snippet.description,publishedAt:o.snippet.publishedAt})).filter(o=>o.title!=="Private video"&&o.title!=="Deleted video"),nextPageToken:r.nextPageToken}:(console.error(`Failed to fetch items for playlist: ${i}`,r),{error:`YouTube API Error: ${r.error?.message||JSON.stringify(r)}`})}catch(n){return console.error(`Error fetching playlist items ${i}:`,n),{error:`Fetch Exception: ${n}`}}}async function U(i,a){if(!a)return console.warn("API Key is not set."),null;try{let n=await(await fetch(`${k}/playlists?part=snippet&id=${i}&key=${a}`)).json();if(!n.items||n.items.length===0)return console.error(`Playlist not found: ${i}`),null;let d=n.items[0].snippet,r=await u(i,a);return !r||r.error||!r.videos?null:{id:i,title:d.title,description:d.description,videos:r.videos,nextPageToken:r.nextPageToken}}catch(l){return console.error(`Error fetching playlist ${i}:`,l),null}}var V=i=>`repeat(auto-fill, minmax(${{2:280,3:220,4:180,5:150,6:130}[i]||180}px, 1fr))`,T=({playlists:i,apiKey:a,onLoadMore:l})=>{let[n,d]=react.useState(i),[r,f]=react.useState(i[0]?.id||""),[o,b]=react.useState(false),[w,x]=react.useState(false),t=n.find(e=>e.id===r);react.useEffect(()=>{!t||!a||t.videos||(async()=>{x(true);try{let c=await u(t.id,a);c.videos&&d(p=>p.map(g=>g.id===t.id?{...g,videos:c.videos||[],nextPageToken:c.nextPageToken}:g));}catch(c){console.error("Failed to fetch initial videos",c);}finally{x(false);}})();},[r,a]);let N=async()=>{if(!(!t||!t.nextPageToken)){b(true);try{let e=[],c;if(l){let p=await l(t.id,t.nextPageToken);e=p.videos,c=p.nextPageToken;}else if(a){let p=await u(t.id,a,t.nextPageToken);p.videos&&(e=p.videos,c=p.nextPageToken);}else {console.error("No apiKey or onLoadMore handler provided");return}e.length>0&&d(p=>p.map(g=>g.id===t.id?{...g,videos:[...g.videos||[],...e],nextPageToken:c}:g));}catch(e){console.error("Failed to load more videos",e);}finally{b(false);}}};return t?jsxRuntime.jsxs("div",{children:[jsxRuntime.jsx("div",{className:"rypg-flex rypg-flex-col rypg-sm-flex-row rypg-justify-between rypg-items-center rypg-mb-8 rypg-gap-4",children:jsxRuntime.jsx("div",{className:"rypg-flex rypg-space-x-2 rypg-overflow-x-auto rypg-pb-2 rypg-sm-pb-0 rypg-w-full rypg-sm-w-auto",children:n.map(e=>jsxRuntime.jsx("button",{onClick:()=>f(e.id),className:`rypg-btn-pill ${r===e.id?"rypg-btn-active":"rypg-btn-inactive"}`,children:e.config?.title||e.title},e.id))})}),jsxRuntime.jsxs("div",{className:"rypg-animate-fade-in",children:[jsxRuntime.jsxs("div",{className:"rypg-mb-6",children:[jsxRuntime.jsx("h2",{className:"rypg-title rypg-mb-2",children:t.title}),t.config?.showDescription&&t.description&&jsxRuntime.jsx("p",{className:"rypg-desc",children:t.description})]}),w?jsxRuntime.jsx("div",{className:"rypg-flex rypg-items-center rypg-justify-center rypg-mb-8",style:{minHeight:"200px"},children:jsxRuntime.jsxs("svg",{className:"rypg-spinner",style:{width:"2rem",height:"2rem",color:"white"},xmlns:"http://www.w3.org/2000/svg",fill:"none",viewBox:"0 0 24 24",children:[jsxRuntime.jsx("circle",{className:"opacity-25",cx:"12",cy:"12",r:"10",stroke:"currentColor",strokeWidth:"4"}),jsxRuntime.jsx("path",{className:"opacity-75",fill:"currentColor",d:"M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"})]})}):jsxRuntime.jsx("div",{className:"rypg-grid rypg-gap-6",style:{gridTemplateColumns:V(t.config?.gridColumns??4)},children:t.videos?.map(e=>jsxRuntime.jsx(P,{title:e.title,thumbnailUrl:e.thumbnailUrl,videoUrl:e.videoUrl,description:e.description,publishedAt:e.publishedAt,columns:t.config?.columns},e.id))}),!w&&t.nextPageToken&&jsxRuntime.jsx("div",{className:"mt-12 rypg-text-center",children:jsxRuntime.jsx("button",{onClick:N,disabled:o,className:"rypg-btn-load",children:o?jsxRuntime.jsxs(jsxRuntime.Fragment,{children:[jsxRuntime.jsxs("svg",{className:"rypg-spinner",xmlns:"http://www.w3.org/2000/svg",fill:"none",viewBox:"0 0 24 24",children:[jsxRuntime.jsx("circle",{className:"opacity-25",cx:"12",cy:"12",r:"10",stroke:"currentColor",strokeWidth:"4"}),jsxRuntime.jsx("path",{className:"opacity-75",fill:"currentColor",d:"M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"})]}),"Loading..."]}):"Load More Videos"})})]})]}):jsxRuntime.jsx("div",{children:"No playlists available."})};
2
- exports.PlaylistsExplorer=T;exports.VideoCard=P;exports.default=T;exports.fetchPlaylistVideos=u;exports.getPlaylist=U;//# sourceMappingURL=index.js.map
1
+ 'use strict';Object.defineProperty(exports,'__esModule',{value:true});var react=require('react'),jsxRuntime=require('react/jsx-runtime');var P=({title:i,thumbnailUrl:a,videoUrl:l,description:n,publishedAt:d,extraVideoFields:t=[]})=>{let f=o=>o?new Date(o).toLocaleDateString("en-US",{year:"numeric",month:"short",day:"numeric"}):"";return jsxRuntime.jsxs("a",{href:l,target:"_blank",rel:"noopener noreferrer",className:"rypg-card",children:[jsxRuntime.jsxs("div",{className:"rypg-card-media",children:[jsxRuntime.jsx("img",{src:a,alt:i,className:"rypg-card-img",loading:"lazy"}),jsxRuntime.jsx("div",{className:"rypg-card-overlay",children:jsxRuntime.jsx("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 24 24",fill:"currentColor",className:"rypg-play-icon",children:jsxRuntime.jsx("path",{fillRule:"evenodd",d:"M4.5 5.653c0-1.426 1.529-2.33 2.779-1.643l11.54 6.348c1.295.712 1.295 2.573 0 3.285L7.28 19.991c-1.25.687-2.779-.217-2.779-1.643V5.653z",clipRule:"evenodd"})})})]}),jsxRuntime.jsxs("div",{className:"rypg-card-content",children:[jsxRuntime.jsx("h3",{className:"rypg-card-title",children:i}),t.includes("description")&&n&&jsxRuntime.jsx("p",{className:"rypg-card-description",style:{fontSize:"0.875rem",marginTop:"0.5rem",opacity:.8},children:n}),t.includes("publishedAt")&&d&&jsxRuntime.jsx("p",{className:"rypg-card-date",style:{fontSize:"0.75rem",marginTop:"0.5rem",opacity:.6},children:f(d)})]})]})};var k="https://www.googleapis.com/youtube/v3";async function u(i,a,l){if(!a)return {error:"API Key is missing"};try{let n=`${k}/playlistItems?part=snippet,contentDetails&playlistId=${i}&maxResults=10&key=${a}`;l&&(n+=`&pageToken=${l}`);let t=await(await fetch(n)).json();return t.items?{videos:t.items.map(o=>({id:o.snippet.resourceId.videoId,title:o.snippet.title,thumbnailUrl:o.snippet.thumbnails.medium?.url||o.snippet.thumbnails.default?.url,videoUrl:`https://www.youtube.com/watch?v=${o.snippet.resourceId.videoId}`,description:o.snippet.description,publishedAt:o.snippet.publishedAt})).filter(o=>o.title!=="Private video"&&o.title!=="Deleted video"),nextPageToken:t.nextPageToken}:(console.error(`Failed to fetch items for playlist: ${i}`,t),{error:`YouTube API Error: ${t.error?.message||JSON.stringify(t)}`})}catch(n){return console.error(`Error fetching playlist items ${i}:`,n),{error:`Fetch Exception: ${n}`}}}async function I(i,a){if(!a)return console.warn("API Key is not set."),null;try{let n=await(await fetch(`${k}/playlists?part=snippet&id=${i}&key=${a}`)).json();if(!n.items||n.items.length===0)return console.error(`Playlist not found: ${i}`),null;let d=n.items[0].snippet,t=await u(i,a);return !t||t.error||!t.videos?null:{id:i,title:d.title,description:d.description,videos:t.videos,nextPageToken:t.nextPageToken}}catch(l){return console.error(`Error fetching playlist ${i}:`,l),null}}var C=i=>`repeat(auto-fill, minmax(${{2:280,3:220,4:180,5:150,6:130}[i]||180}px, 1fr))`,T=({playlists:i,apiKey:a,onLoadMore:l})=>{let[n,d]=react.useState(i),[t,f]=react.useState(i[0]?.id||""),[o,b]=react.useState(false),[w,x]=react.useState(false),r=n.find(e=>e.id===t);react.useEffect(()=>{!r||!a||r.videos||(async()=>{x(true);try{let c=await u(r.id,a);c.videos&&d(p=>p.map(g=>g.id===r.id?{...g,videos:c.videos||[],nextPageToken:c.nextPageToken}:g));}catch(c){console.error("Failed to fetch initial videos",c);}finally{x(false);}})();},[t,a]);let N=async()=>{if(!(!r||!r.nextPageToken)){b(true);try{let e=[],c;if(l){let p=await l(r.id,r.nextPageToken);e=p.videos,c=p.nextPageToken;}else if(a){let p=await u(r.id,a,r.nextPageToken);p.videos&&(e=p.videos,c=p.nextPageToken);}else {console.error("No apiKey or onLoadMore handler provided");return}e.length>0&&d(p=>p.map(g=>g.id===r.id?{...g,videos:[...g.videos||[],...e],nextPageToken:c}:g));}catch(e){console.error("Failed to load more videos",e);}finally{b(false);}}};return r?jsxRuntime.jsxs("div",{children:[jsxRuntime.jsx("div",{className:"rypg-flex rypg-flex-col rypg-sm-flex-row rypg-justify-between rypg-items-center rypg-mb-8 rypg-gap-4",children:jsxRuntime.jsx("div",{className:"rypg-flex rypg-space-x-2 rypg-overflow-x-auto rypg-pb-2 rypg-sm-pb-0 rypg-w-full rypg-sm-w-auto",children:n.map(e=>jsxRuntime.jsx("button",{onClick:()=>f(e.id),className:`rypg-btn-pill ${t===e.id?"rypg-btn-active":"rypg-btn-inactive"}`,children:e.config?.title||e.title},e.id))})}),jsxRuntime.jsxs("div",{className:"rypg-animate-fade-in",children:[jsxRuntime.jsxs("div",{className:"rypg-mb-6",children:[jsxRuntime.jsx("h2",{className:"rypg-title rypg-mb-2",children:r.title}),r.config?.showDescription&&r.description&&jsxRuntime.jsx("p",{className:"rypg-desc",children:r.description})]}),w?jsxRuntime.jsx("div",{className:"rypg-flex rypg-items-center rypg-justify-center rypg-mb-8",style:{minHeight:"200px"},children:jsxRuntime.jsxs("svg",{className:"rypg-spinner",style:{width:"2rem",height:"2rem",color:"white"},xmlns:"http://www.w3.org/2000/svg",fill:"none",viewBox:"0 0 24 24",children:[jsxRuntime.jsx("circle",{className:"opacity-25",cx:"12",cy:"12",r:"10",stroke:"currentColor",strokeWidth:"4"}),jsxRuntime.jsx("path",{className:"opacity-75",fill:"currentColor",d:"M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"})]})}):jsxRuntime.jsx("div",{className:"rypg-grid rypg-gap-6",style:{gridTemplateColumns:C(r.config?.gridColumns??4)},children:r.videos?.map(e=>jsxRuntime.jsx(P,{title:e.title,thumbnailUrl:e.thumbnailUrl,videoUrl:e.videoUrl,description:e.description,publishedAt:e.publishedAt,extraVideoFields:r.config?.extraVideoFields},e.id))}),!w&&r.nextPageToken&&jsxRuntime.jsx("div",{className:"mt-12 rypg-text-center",children:jsxRuntime.jsx("button",{onClick:N,disabled:o,className:"rypg-btn-load",children:o?jsxRuntime.jsxs(jsxRuntime.Fragment,{children:[jsxRuntime.jsxs("svg",{className:"rypg-spinner",xmlns:"http://www.w3.org/2000/svg",fill:"none",viewBox:"0 0 24 24",children:[jsxRuntime.jsx("circle",{className:"opacity-25",cx:"12",cy:"12",r:"10",stroke:"currentColor",strokeWidth:"4"}),jsxRuntime.jsx("path",{className:"opacity-75",fill:"currentColor",d:"M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"})]}),"Loading..."]}):"Load More Videos"})})]})]}):jsxRuntime.jsx("div",{children:"No playlists available."})};
2
+ exports.PlaylistsExplorer=T;exports.VideoCard=P;exports.default=T;exports.fetchPlaylistVideos=u;exports.getPlaylist=I;//# sourceMappingURL=index.js.map
3
3
  //# sourceMappingURL=index.js.map
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/components/VideoCard.tsx","../src/utils/youtube.ts","../src/components/PlaylistsExplorer.tsx"],"names":["VideoCard","title","thumbnailUrl","videoUrl","description","publishedAt","columns","formatDate","dateString","jsxs","jsx","YOUTUBE_API_BASE","fetchPlaylistVideos","playlistId","apiKey","pageToken","url","itemsData","item","v","error","getPlaylist","playlistData","playlistSnippet","videosData","getGridColumns","n","PlaylistsExplorer","initialPlaylists","onLoadMore","playlists","setPlaylists","useState","activePlaylistId","setActivePlaylistId","loadingMore","setLoadingMore","initialLoading","setInitialLoading","activePlaylist","p","useEffect","result","prev","handleLoadMore","newVideos","newNextPageToken","prevPlaylists","playlist","video","Fragment"],"mappings":"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","file":"index.js","sourcesContent":["import React from 'react';\nimport type { VideoColumn } from '../types';\n\ninterface VideoCardProps {\n title: string;\n thumbnailUrl: string;\n videoUrl: string;\n description?: string;\n publishedAt?: string;\n}\n\ninterface VideoCardPropsInternal extends VideoCardProps {\n columns?: VideoColumn[];\n}\n\nexport const VideoCard: React.FC<VideoCardPropsInternal> = ({ title, thumbnailUrl, videoUrl, description, publishedAt, columns = ['thumbnail', 'title'] }) => {\n const formatDate = (dateString?: string) => {\n if (!dateString) return '';\n return new Date(dateString).toLocaleDateString('en-US', {\n year: 'numeric',\n month: 'short',\n day: 'numeric'\n });\n };\n\n return (\n <a\n href={videoUrl}\n target=\"_blank\"\n rel=\"noopener noreferrer\"\n className=\"rypg-card\"\n >\n {columns.includes('thumbnail') && (\n <div className=\"rypg-card-media\">\n <img\n src={thumbnailUrl}\n alt={title}\n className=\"rypg-card-img\"\n loading=\"lazy\"\n />\n <div className=\"rypg-card-overlay\">\n <svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 24 24\" fill=\"currentColor\" className=\"rypg-play-icon\">\n <path fillRule=\"evenodd\" d=\"M4.5 5.653c0-1.426 1.529-2.33 2.779-1.643l11.54 6.348c1.295.712 1.295 2.573 0 3.285L7.28 19.991c-1.25.687-2.779-.217-2.779-1.643V5.653z\" clipRule=\"evenodd\" />\n </svg>\n </div>\n </div>\n )}\n <div className=\"rypg-card-content\">\n {columns.includes('title') && (\n <h3 className=\"rypg-card-title\">\n {title}\n </h3>\n )}\n {columns.includes('description') && description && (\n <p className=\"rypg-card-description\" style={{ fontSize: '0.875rem', marginTop: '0.5rem', opacity: 0.8 }}>\n {description}\n </p>\n )}\n {columns.includes('publishedAt') && publishedAt && (\n <p className=\"rypg-card-date\" style={{ fontSize: '0.75rem', marginTop: '0.5rem', opacity: 0.6 }}>\n {formatDate(publishedAt)}\n </p>\n )}\n </div>\n </a>\n );\n};\n","import type { Playlist, Video } from \"../types\";\n\nconst YOUTUBE_API_BASE = \"https://www.googleapis.com/youtube/v3\";\n\ninterface FetchVideosResult {\n videos?: Video[];\n nextPageToken?: string;\n error?: string;\n}\n\nexport async function fetchPlaylistVideos(playlistId: string, apiKey: string, pageToken?: string): Promise<FetchVideosResult> {\n if (!apiKey) return { error: \"API Key is missing\" };\n\n try {\n let url = `${YOUTUBE_API_BASE}/playlistItems?part=snippet,contentDetails&playlistId=${playlistId}&maxResults=10&key=${apiKey}`;\n if (pageToken) {\n url += `&pageToken=${pageToken}`;\n }\n\n const itemsResponse = await fetch(url);\n const itemsData = await itemsResponse.json();\n\n if (!itemsData.items) {\n console.error(`Failed to fetch items for playlist: ${playlistId}`, itemsData);\n return { error: `YouTube API Error: ${itemsData.error?.message || JSON.stringify(itemsData)}` };\n }\n\n const videos: Video[] = itemsData.items.map((item: any) => ({\n id: item.snippet.resourceId.videoId,\n title: item.snippet.title,\n thumbnailUrl: item.snippet.thumbnails.medium?.url || item.snippet.thumbnails.default?.url,\n videoUrl: `https://www.youtube.com/watch?v=${item.snippet.resourceId.videoId}`,\n description: item.snippet.description,\n publishedAt: item.snippet.publishedAt,\n })).filter((v: Video) => v.title !== \"Private video\" && v.title !== \"Deleted video\");\n\n return {\n videos,\n nextPageToken: itemsData.nextPageToken\n };\n } catch (error) {\n console.error(`Error fetching playlist items ${playlistId}:`, error);\n return { error: `Fetch Exception: ${error}` };\n }\n}\n\nexport async function getPlaylist(playlistId: string, apiKey: string): Promise<Playlist | null> {\n if (!apiKey) {\n console.warn(\"API Key is not set.\");\n return null;\n }\n\n try {\n // 1. Get Playlist Details\n const playlistResponse = await fetch(\n `${YOUTUBE_API_BASE}/playlists?part=snippet&id=${playlistId}&key=${apiKey}`\n );\n const playlistData = await playlistResponse.json();\n\n if (!playlistData.items || playlistData.items.length === 0) {\n console.error(`Playlist not found: ${playlistId}`);\n return null;\n }\n\n const playlistSnippet = playlistData.items[0].snippet;\n\n // 2. Get First Batch of Videos\n const videosData = await fetchPlaylistVideos(playlistId, apiKey);\n\n if (!videosData || videosData.error || !videosData.videos) return null;\n\n return {\n id: playlistId,\n title: playlistSnippet.title,\n description: playlistSnippet.description,\n videos: videosData.videos,\n nextPageToken: videosData.nextPageToken\n };\n\n } catch (error) {\n console.error(`Error fetching playlist ${playlistId}:`, error);\n return null;\n }\n}\n","import React, { useState, useEffect } from 'react';\nimport type { Playlist, Video } from '../types';\nimport { VideoCard } from './VideoCard';\nimport { fetchPlaylistVideos } from '../utils/youtube';\n\nconst getGridColumns = (n: number): string => {\n const widthMap: { [key: number]: number } = {\n 2: 280,\n 3: 220,\n 4: 180,\n 5: 150,\n 6: 130,\n };\n const minWidth = widthMap[n] || 180;\n return `repeat(auto-fill, minmax(${minWidth}px, 1fr))`;\n};\n\ninterface PlaylistsExplorerProps {\n playlists: Playlist[];\n apiKey?: string;\n onLoadMore?: (playlistId: string, pageToken: string) => Promise<{ videos: Video[], nextPageToken?: string }>;\n}\n\nexport const PlaylistsExplorer: React.FC<PlaylistsExplorerProps> = ({ playlists: initialPlaylists, apiKey, onLoadMore }) => {\n // We maintain a local state of playlists to store fetched videos\n const [playlists, setPlaylists] = useState<Playlist[]>(initialPlaylists);\n const [activePlaylistId, setActivePlaylistId] = useState<string>(initialPlaylists[0]?.id || \"\");\n const [loadingMore, setLoadingMore] = useState<boolean>(false);\n const [initialLoading, setInitialLoading] = useState<boolean>(false);\n\n const activePlaylist = playlists.find(p => p.id === activePlaylistId);\n\n // Initial Fetch Effect: When active playlist changes, if it has no videos, fetch them\n useEffect(() => {\n if (!activePlaylist || !apiKey) return;\n\n // If videos are undefined or empty array (and we haven't fetched yet - we can check if nextPageToken is missing and videos empty implies not fetched if it's a fresh config)\n // A better check: if videos is undefined, we definitely need to fetch. \n // If videos is [], it might be empty playlist or not fetched. \n // Let's assume if it's undefined we fetch.\n if (!activePlaylist.videos) {\n const fetchInitial = async () => {\n setInitialLoading(true);\n try {\n const result = await fetchPlaylistVideos(activePlaylist.id, apiKey);\n if (result.videos) {\n setPlaylists(prev => prev.map(p => {\n if (p.id === activePlaylist.id) {\n return {\n ...p,\n videos: result.videos || [], // Ensure it's an array\n nextPageToken: result.nextPageToken,\n // Also fill in title/desc if missing from config? The API returns items, not playlist details here.\n // Ideally we would also fetch playlist details but for now let's just get videos.\n };\n }\n return p;\n }));\n }\n } catch (error) {\n console.error(\"Failed to fetch initial videos\", error);\n } finally {\n setInitialLoading(false);\n }\n };\n fetchInitial();\n }\n }, [activePlaylistId, apiKey]); // We purposefully don't include activePlaylist to avoid loops, rely on ID check or check content inside\n\n const handleLoadMore = async () => {\n if (!activePlaylist || !activePlaylist.nextPageToken) return;\n\n setLoadingMore(true);\n try {\n let newVideos: Video[] = [];\n let newNextPageToken: string | undefined;\n\n if (onLoadMore) {\n const result = await onLoadMore(activePlaylist.id, activePlaylist.nextPageToken);\n newVideos = result.videos;\n newNextPageToken = result.nextPageToken;\n } else if (apiKey) {\n const result = await fetchPlaylistVideos(activePlaylist.id, apiKey, activePlaylist.nextPageToken);\n if (result.videos) {\n newVideos = result.videos;\n newNextPageToken = result.nextPageToken;\n }\n } else {\n console.error(\"No apiKey or onLoadMore handler provided\");\n return;\n }\n\n if (newVideos.length > 0) {\n setPlaylists(prevPlaylists => prevPlaylists.map(p => {\n if (p.id === activePlaylist.id) {\n return {\n ...p,\n videos: [...(p.videos || []), ...newVideos],\n nextPageToken: newNextPageToken\n };\n }\n return p;\n }));\n }\n } catch (error) {\n console.error(\"Failed to load more videos\", error);\n } finally {\n setLoadingMore(false);\n }\n };\n\n if (!activePlaylist) return <div>No playlists available.</div>;\n\n return (\n <div>\n {/* Navigation & Tabs */}\n <div className=\"rypg-flex rypg-flex-col rypg-sm-flex-row rypg-justify-between rypg-items-center rypg-mb-8 rypg-gap-4\">\n <div className=\"rypg-flex rypg-space-x-2 rypg-overflow-x-auto rypg-pb-2 rypg-sm-pb-0 rypg-w-full rypg-sm-w-auto\">\n {playlists.map(playlist => (\n <button\n key={playlist.id}\n onClick={() => setActivePlaylistId(playlist.id)}\n className={`rypg-btn-pill ${activePlaylistId === playlist.id\n ? 'rypg-btn-active'\n : 'rypg-btn-inactive'\n }`}\n >\n {playlist.config?.title || playlist.title}\n </button>\n ))}\n </div>\n </div>\n\n {/* Playlist Content */}\n <div className=\"rypg-animate-fade-in\">\n {/* Always show title and description if enabled */}\n <div className=\"rypg-mb-6\">\n <h2 className=\"rypg-title rypg-mb-2\">{activePlaylist.title}</h2>\n {activePlaylist.config?.showDescription && activePlaylist.description && (\n <p className=\"rypg-desc\">{activePlaylist.description}</p>\n )}\n </div>\n\n {initialLoading ? (\n <div className=\"rypg-flex rypg-items-center rypg-justify-center rypg-mb-8\" style={{ minHeight: '200px' }}>\n <svg className=\"rypg-spinner\" style={{ width: '2rem', height: '2rem', color: 'white' }} xmlns=\"http://www.w3.org/2000/svg\" fill=\"none\" viewBox=\"0 0 24 24\">\n <circle className=\"opacity-25\" cx=\"12\" cy=\"12\" r=\"10\" stroke=\"currentColor\" strokeWidth=\"4\"></circle>\n <path className=\"opacity-75\" fill=\"currentColor\" d=\"M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z\"></path>\n </svg>\n </div>\n ) : (\n <div className=\"rypg-grid rypg-gap-6\" style={{ gridTemplateColumns: getGridColumns(activePlaylist.config?.gridColumns ?? 4) }}>\n {activePlaylist.videos?.map((video) => (\n <VideoCard\n key={video.id}\n title={video.title}\n thumbnailUrl={video.thumbnailUrl}\n videoUrl={video.videoUrl}\n description={video.description}\n publishedAt={video.publishedAt}\n columns={activePlaylist.config?.columns}\n />\n ))}\n </div>\n )}\n\n {/* Load More */}\n {!initialLoading && activePlaylist.nextPageToken && (\n <div className=\"mt-12 rypg-text-center\">\n <button\n onClick={handleLoadMore}\n disabled={loadingMore}\n className=\"rypg-btn-load\"\n >\n {loadingMore ? (\n <>\n <svg className=\"rypg-spinner\" xmlns=\"http://www.w3.org/2000/svg\" fill=\"none\" viewBox=\"0 0 24 24\">\n <circle className=\"opacity-25\" cx=\"12\" cy=\"12\" r=\"10\" stroke=\"currentColor\" strokeWidth=\"4\"></circle>\n <path className=\"opacity-75\" fill=\"currentColor\" d=\"M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z\"></path>\n </svg>\n Loading...\n </>\n ) : (\n 'Load More Videos'\n )}\n </button>\n </div>\n )}\n </div>\n </div>\n );\n};\n"]}
1
+ {"version":3,"sources":["../src/components/VideoCard.tsx","../src/utils/youtube.ts","../src/components/PlaylistsExplorer.tsx"],"names":["VideoCard","title","thumbnailUrl","videoUrl","description","publishedAt","extraVideoFields","formatDate","dateString","jsxs","jsx","YOUTUBE_API_BASE","fetchPlaylistVideos","playlistId","apiKey","pageToken","url","itemsData","item","v","error","getPlaylist","playlistData","playlistSnippet","videosData","getGridColumns","n","PlaylistsExplorer","initialPlaylists","onLoadMore","playlists","setPlaylists","useState","activePlaylistId","setActivePlaylistId","loadingMore","setLoadingMore","initialLoading","setInitialLoading","activePlaylist","p","useEffect","result","prev","handleLoadMore","newVideos","newNextPageToken","prevPlaylists","playlist","video","Fragment"],"mappings":"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","file":"index.js","sourcesContent":["import React from 'react';\nimport type { VideoField } from '../types';\n\ninterface VideoCardProps {\n title: string;\n thumbnailUrl: string;\n videoUrl: string;\n description?: string;\n publishedAt?: string;\n}\n\ninterface VideoCardPropsInternal extends VideoCardProps {\n extraVideoFields?: VideoField[];\n}\n\nexport const VideoCard: React.FC<VideoCardPropsInternal> = ({ title, thumbnailUrl, videoUrl, description, publishedAt, extraVideoFields = [] }) => {\n const formatDate = (dateString?: string) => {\n if (!dateString) return '';\n return new Date(dateString).toLocaleDateString('en-US', {\n year: 'numeric',\n month: 'short',\n day: 'numeric'\n });\n };\n\n return (\n <a\n href={videoUrl}\n target=\"_blank\"\n rel=\"noopener noreferrer\"\n className=\"rypg-card\"\n >\n <div className=\"rypg-card-media\">\n <img\n src={thumbnailUrl}\n alt={title}\n className=\"rypg-card-img\"\n loading=\"lazy\"\n />\n <div className=\"rypg-card-overlay\">\n <svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 24 24\" fill=\"currentColor\" className=\"rypg-play-icon\">\n <path fillRule=\"evenodd\" d=\"M4.5 5.653c0-1.426 1.529-2.33 2.779-1.643l11.54 6.348c1.295.712 1.295 2.573 0 3.285L7.28 19.991c-1.25.687-2.779-.217-2.779-1.643V5.653z\" clipRule=\"evenodd\" />\n </svg>\n </div>\n </div>\n <div className=\"rypg-card-content\">\n <h3 className=\"rypg-card-title\">\n {title}\n </h3>\n {extraVideoFields.includes('description') && description && (\n <p className=\"rypg-card-description\" style={{ fontSize: '0.875rem', marginTop: '0.5rem', opacity: 0.8 }}>\n {description}\n </p>\n )}\n {extraVideoFields.includes('publishedAt') && publishedAt && (\n <p className=\"rypg-card-date\" style={{ fontSize: '0.75rem', marginTop: '0.5rem', opacity: 0.6 }}>\n {formatDate(publishedAt)}\n </p>\n )}\n </div>\n </a>\n );\n};\n","import type { Playlist, Video } from \"../types\";\n\nconst YOUTUBE_API_BASE = \"https://www.googleapis.com/youtube/v3\";\n\ninterface FetchVideosResult {\n videos?: Video[];\n nextPageToken?: string;\n error?: string;\n}\n\nexport async function fetchPlaylistVideos(playlistId: string, apiKey: string, pageToken?: string): Promise<FetchVideosResult> {\n if (!apiKey) return { error: \"API Key is missing\" };\n\n try {\n let url = `${YOUTUBE_API_BASE}/playlistItems?part=snippet,contentDetails&playlistId=${playlistId}&maxResults=10&key=${apiKey}`;\n if (pageToken) {\n url += `&pageToken=${pageToken}`;\n }\n\n const itemsResponse = await fetch(url);\n const itemsData = await itemsResponse.json();\n\n if (!itemsData.items) {\n console.error(`Failed to fetch items for playlist: ${playlistId}`, itemsData);\n return { error: `YouTube API Error: ${itemsData.error?.message || JSON.stringify(itemsData)}` };\n }\n\n const videos: Video[] = itemsData.items.map((item: any) => ({\n id: item.snippet.resourceId.videoId,\n title: item.snippet.title,\n thumbnailUrl: item.snippet.thumbnails.medium?.url || item.snippet.thumbnails.default?.url,\n videoUrl: `https://www.youtube.com/watch?v=${item.snippet.resourceId.videoId}`,\n description: item.snippet.description,\n publishedAt: item.snippet.publishedAt,\n })).filter((v: Video) => v.title !== \"Private video\" && v.title !== \"Deleted video\");\n\n return {\n videos,\n nextPageToken: itemsData.nextPageToken\n };\n } catch (error) {\n console.error(`Error fetching playlist items ${playlistId}:`, error);\n return { error: `Fetch Exception: ${error}` };\n }\n}\n\nexport async function getPlaylist(playlistId: string, apiKey: string): Promise<Playlist | null> {\n if (!apiKey) {\n console.warn(\"API Key is not set.\");\n return null;\n }\n\n try {\n // 1. Get Playlist Details\n const playlistResponse = await fetch(\n `${YOUTUBE_API_BASE}/playlists?part=snippet&id=${playlistId}&key=${apiKey}`\n );\n const playlistData = await playlistResponse.json();\n\n if (!playlistData.items || playlistData.items.length === 0) {\n console.error(`Playlist not found: ${playlistId}`);\n return null;\n }\n\n const playlistSnippet = playlistData.items[0].snippet;\n\n // 2. Get First Batch of Videos\n const videosData = await fetchPlaylistVideos(playlistId, apiKey);\n\n if (!videosData || videosData.error || !videosData.videos) return null;\n\n return {\n id: playlistId,\n title: playlistSnippet.title,\n description: playlistSnippet.description,\n videos: videosData.videos,\n nextPageToken: videosData.nextPageToken\n };\n\n } catch (error) {\n console.error(`Error fetching playlist ${playlistId}:`, error);\n return null;\n }\n}\n","import React, { useState, useEffect } from 'react';\nimport type { Playlist, Video } from '../types';\nimport { VideoCard } from './VideoCard';\nimport { fetchPlaylistVideos } from '../utils/youtube';\n\nconst getGridColumns = (n: number): string => {\n const widthMap: { [key: number]: number } = {\n 2: 280,\n 3: 220,\n 4: 180,\n 5: 150,\n 6: 130,\n };\n const minWidth = widthMap[n] || 180;\n return `repeat(auto-fill, minmax(${minWidth}px, 1fr))`;\n};\n\ninterface PlaylistsExplorerProps {\n playlists: Playlist[];\n apiKey?: string;\n onLoadMore?: (playlistId: string, pageToken: string) => Promise<{ videos: Video[], nextPageToken?: string }>;\n}\n\nexport const PlaylistsExplorer: React.FC<PlaylistsExplorerProps> = ({ playlists: initialPlaylists, apiKey, onLoadMore }) => {\n // We maintain a local state of playlists to store fetched videos\n const [playlists, setPlaylists] = useState<Playlist[]>(initialPlaylists);\n const [activePlaylistId, setActivePlaylistId] = useState<string>(initialPlaylists[0]?.id || \"\");\n const [loadingMore, setLoadingMore] = useState<boolean>(false);\n const [initialLoading, setInitialLoading] = useState<boolean>(false);\n\n const activePlaylist = playlists.find(p => p.id === activePlaylistId);\n\n // Initial Fetch Effect: When active playlist changes, if it has no videos, fetch them\n useEffect(() => {\n if (!activePlaylist || !apiKey) return;\n\n // If videos are undefined or empty array (and we haven't fetched yet - we can check if nextPageToken is missing and videos empty implies not fetched if it's a fresh config)\n // A better check: if videos is undefined, we definitely need to fetch. \n // If videos is [], it might be empty playlist or not fetched. \n // Let's assume if it's undefined we fetch.\n if (!activePlaylist.videos) {\n const fetchInitial = async () => {\n setInitialLoading(true);\n try {\n const result = await fetchPlaylistVideos(activePlaylist.id, apiKey);\n if (result.videos) {\n setPlaylists(prev => prev.map(p => {\n if (p.id === activePlaylist.id) {\n return {\n ...p,\n videos: result.videos || [], // Ensure it's an array\n nextPageToken: result.nextPageToken,\n // Also fill in title/desc if missing from config? The API returns items, not playlist details here.\n // Ideally we would also fetch playlist details but for now let's just get videos.\n };\n }\n return p;\n }));\n }\n } catch (error) {\n console.error(\"Failed to fetch initial videos\", error);\n } finally {\n setInitialLoading(false);\n }\n };\n fetchInitial();\n }\n }, [activePlaylistId, apiKey]); // We purposefully don't include activePlaylist to avoid loops, rely on ID check or check content inside\n\n const handleLoadMore = async () => {\n if (!activePlaylist || !activePlaylist.nextPageToken) return;\n\n setLoadingMore(true);\n try {\n let newVideos: Video[] = [];\n let newNextPageToken: string | undefined;\n\n if (onLoadMore) {\n const result = await onLoadMore(activePlaylist.id, activePlaylist.nextPageToken);\n newVideos = result.videos;\n newNextPageToken = result.nextPageToken;\n } else if (apiKey) {\n const result = await fetchPlaylistVideos(activePlaylist.id, apiKey, activePlaylist.nextPageToken);\n if (result.videos) {\n newVideos = result.videos;\n newNextPageToken = result.nextPageToken;\n }\n } else {\n console.error(\"No apiKey or onLoadMore handler provided\");\n return;\n }\n\n if (newVideos.length > 0) {\n setPlaylists(prevPlaylists => prevPlaylists.map(p => {\n if (p.id === activePlaylist.id) {\n return {\n ...p,\n videos: [...(p.videos || []), ...newVideos],\n nextPageToken: newNextPageToken\n };\n }\n return p;\n }));\n }\n } catch (error) {\n console.error(\"Failed to load more videos\", error);\n } finally {\n setLoadingMore(false);\n }\n };\n\n if (!activePlaylist) return <div>No playlists available.</div>;\n\n return (\n <div>\n {/* Navigation & Tabs */}\n <div className=\"rypg-flex rypg-flex-col rypg-sm-flex-row rypg-justify-between rypg-items-center rypg-mb-8 rypg-gap-4\">\n <div className=\"rypg-flex rypg-space-x-2 rypg-overflow-x-auto rypg-pb-2 rypg-sm-pb-0 rypg-w-full rypg-sm-w-auto\">\n {playlists.map(playlist => (\n <button\n key={playlist.id}\n onClick={() => setActivePlaylistId(playlist.id)}\n className={`rypg-btn-pill ${activePlaylistId === playlist.id\n ? 'rypg-btn-active'\n : 'rypg-btn-inactive'\n }`}\n >\n {playlist.config?.title || playlist.title}\n </button>\n ))}\n </div>\n </div>\n\n {/* Playlist Content */}\n <div className=\"rypg-animate-fade-in\">\n {/* Always show title and description if enabled */}\n <div className=\"rypg-mb-6\">\n <h2 className=\"rypg-title rypg-mb-2\">{activePlaylist.title}</h2>\n {activePlaylist.config?.showDescription && activePlaylist.description && (\n <p className=\"rypg-desc\">{activePlaylist.description}</p>\n )}\n </div>\n\n {initialLoading ? (\n <div className=\"rypg-flex rypg-items-center rypg-justify-center rypg-mb-8\" style={{ minHeight: '200px' }}>\n <svg className=\"rypg-spinner\" style={{ width: '2rem', height: '2rem', color: 'white' }} xmlns=\"http://www.w3.org/2000/svg\" fill=\"none\" viewBox=\"0 0 24 24\">\n <circle className=\"opacity-25\" cx=\"12\" cy=\"12\" r=\"10\" stroke=\"currentColor\" strokeWidth=\"4\"></circle>\n <path className=\"opacity-75\" fill=\"currentColor\" d=\"M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z\"></path>\n </svg>\n </div>\n ) : (\n <div className=\"rypg-grid rypg-gap-6\" style={{ gridTemplateColumns: getGridColumns(activePlaylist.config?.gridColumns ?? 4) }}>\n {activePlaylist.videos?.map((video) => (\n <VideoCard\n key={video.id}\n title={video.title}\n thumbnailUrl={video.thumbnailUrl}\n videoUrl={video.videoUrl}\n description={video.description}\n publishedAt={video.publishedAt}\n extraVideoFields={activePlaylist.config?.extraVideoFields}\n />\n ))}\n </div>\n )}\n\n {/* Load More */}\n {!initialLoading && activePlaylist.nextPageToken && (\n <div className=\"mt-12 rypg-text-center\">\n <button\n onClick={handleLoadMore}\n disabled={loadingMore}\n className=\"rypg-btn-load\"\n >\n {loadingMore ? (\n <>\n <svg className=\"rypg-spinner\" xmlns=\"http://www.w3.org/2000/svg\" fill=\"none\" viewBox=\"0 0 24 24\">\n <circle className=\"opacity-25\" cx=\"12\" cy=\"12\" r=\"10\" stroke=\"currentColor\" strokeWidth=\"4\"></circle>\n <path className=\"opacity-75\" fill=\"currentColor\" d=\"M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z\"></path>\n </svg>\n Loading...\n </>\n ) : (\n 'Load More Videos'\n )}\n </button>\n </div>\n )}\n </div>\n </div>\n );\n};\n"]}
package/dist/index.mjs CHANGED
@@ -1,3 +1,3 @@
1
- import {useState,useEffect}from'react';import {jsxs,jsx,Fragment}from'react/jsx-runtime';var P=({title:i,thumbnailUrl:a,videoUrl:l,description:n,publishedAt:d,columns:r=["thumbnail","title"]})=>{let f=o=>o?new Date(o).toLocaleDateString("en-US",{year:"numeric",month:"short",day:"numeric"}):"";return jsxs("a",{href:l,target:"_blank",rel:"noopener noreferrer",className:"rypg-card",children:[r.includes("thumbnail")&&jsxs("div",{className:"rypg-card-media",children:[jsx("img",{src:a,alt:i,className:"rypg-card-img",loading:"lazy"}),jsx("div",{className:"rypg-card-overlay",children:jsx("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 24 24",fill:"currentColor",className:"rypg-play-icon",children:jsx("path",{fillRule:"evenodd",d:"M4.5 5.653c0-1.426 1.529-2.33 2.779-1.643l11.54 6.348c1.295.712 1.295 2.573 0 3.285L7.28 19.991c-1.25.687-2.779-.217-2.779-1.643V5.653z",clipRule:"evenodd"})})})]}),jsxs("div",{className:"rypg-card-content",children:[r.includes("title")&&jsx("h3",{className:"rypg-card-title",children:i}),r.includes("description")&&n&&jsx("p",{className:"rypg-card-description",style:{fontSize:"0.875rem",marginTop:"0.5rem",opacity:.8},children:n}),r.includes("publishedAt")&&d&&jsx("p",{className:"rypg-card-date",style:{fontSize:"0.75rem",marginTop:"0.5rem",opacity:.6},children:f(d)})]})]})};var k="https://www.googleapis.com/youtube/v3";async function u(i,a,l){if(!a)return {error:"API Key is missing"};try{let n=`${k}/playlistItems?part=snippet,contentDetails&playlistId=${i}&maxResults=10&key=${a}`;l&&(n+=`&pageToken=${l}`);let r=await(await fetch(n)).json();return r.items?{videos:r.items.map(o=>({id:o.snippet.resourceId.videoId,title:o.snippet.title,thumbnailUrl:o.snippet.thumbnails.medium?.url||o.snippet.thumbnails.default?.url,videoUrl:`https://www.youtube.com/watch?v=${o.snippet.resourceId.videoId}`,description:o.snippet.description,publishedAt:o.snippet.publishedAt})).filter(o=>o.title!=="Private video"&&o.title!=="Deleted video"),nextPageToken:r.nextPageToken}:(console.error(`Failed to fetch items for playlist: ${i}`,r),{error:`YouTube API Error: ${r.error?.message||JSON.stringify(r)}`})}catch(n){return console.error(`Error fetching playlist items ${i}:`,n),{error:`Fetch Exception: ${n}`}}}async function U(i,a){if(!a)return console.warn("API Key is not set."),null;try{let n=await(await fetch(`${k}/playlists?part=snippet&id=${i}&key=${a}`)).json();if(!n.items||n.items.length===0)return console.error(`Playlist not found: ${i}`),null;let d=n.items[0].snippet,r=await u(i,a);return !r||r.error||!r.videos?null:{id:i,title:d.title,description:d.description,videos:r.videos,nextPageToken:r.nextPageToken}}catch(l){return console.error(`Error fetching playlist ${i}:`,l),null}}var V=i=>`repeat(auto-fill, minmax(${{2:280,3:220,4:180,5:150,6:130}[i]||180}px, 1fr))`,T=({playlists:i,apiKey:a,onLoadMore:l})=>{let[n,d]=useState(i),[r,f]=useState(i[0]?.id||""),[o,b]=useState(false),[w,x]=useState(false),t=n.find(e=>e.id===r);useEffect(()=>{!t||!a||t.videos||(async()=>{x(true);try{let c=await u(t.id,a);c.videos&&d(p=>p.map(g=>g.id===t.id?{...g,videos:c.videos||[],nextPageToken:c.nextPageToken}:g));}catch(c){console.error("Failed to fetch initial videos",c);}finally{x(false);}})();},[r,a]);let N=async()=>{if(!(!t||!t.nextPageToken)){b(true);try{let e=[],c;if(l){let p=await l(t.id,t.nextPageToken);e=p.videos,c=p.nextPageToken;}else if(a){let p=await u(t.id,a,t.nextPageToken);p.videos&&(e=p.videos,c=p.nextPageToken);}else {console.error("No apiKey or onLoadMore handler provided");return}e.length>0&&d(p=>p.map(g=>g.id===t.id?{...g,videos:[...g.videos||[],...e],nextPageToken:c}:g));}catch(e){console.error("Failed to load more videos",e);}finally{b(false);}}};return t?jsxs("div",{children:[jsx("div",{className:"rypg-flex rypg-flex-col rypg-sm-flex-row rypg-justify-between rypg-items-center rypg-mb-8 rypg-gap-4",children:jsx("div",{className:"rypg-flex rypg-space-x-2 rypg-overflow-x-auto rypg-pb-2 rypg-sm-pb-0 rypg-w-full rypg-sm-w-auto",children:n.map(e=>jsx("button",{onClick:()=>f(e.id),className:`rypg-btn-pill ${r===e.id?"rypg-btn-active":"rypg-btn-inactive"}`,children:e.config?.title||e.title},e.id))})}),jsxs("div",{className:"rypg-animate-fade-in",children:[jsxs("div",{className:"rypg-mb-6",children:[jsx("h2",{className:"rypg-title rypg-mb-2",children:t.title}),t.config?.showDescription&&t.description&&jsx("p",{className:"rypg-desc",children:t.description})]}),w?jsx("div",{className:"rypg-flex rypg-items-center rypg-justify-center rypg-mb-8",style:{minHeight:"200px"},children:jsxs("svg",{className:"rypg-spinner",style:{width:"2rem",height:"2rem",color:"white"},xmlns:"http://www.w3.org/2000/svg",fill:"none",viewBox:"0 0 24 24",children:[jsx("circle",{className:"opacity-25",cx:"12",cy:"12",r:"10",stroke:"currentColor",strokeWidth:"4"}),jsx("path",{className:"opacity-75",fill:"currentColor",d:"M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"})]})}):jsx("div",{className:"rypg-grid rypg-gap-6",style:{gridTemplateColumns:V(t.config?.gridColumns??4)},children:t.videos?.map(e=>jsx(P,{title:e.title,thumbnailUrl:e.thumbnailUrl,videoUrl:e.videoUrl,description:e.description,publishedAt:e.publishedAt,columns:t.config?.columns},e.id))}),!w&&t.nextPageToken&&jsx("div",{className:"mt-12 rypg-text-center",children:jsx("button",{onClick:N,disabled:o,className:"rypg-btn-load",children:o?jsxs(Fragment,{children:[jsxs("svg",{className:"rypg-spinner",xmlns:"http://www.w3.org/2000/svg",fill:"none",viewBox:"0 0 24 24",children:[jsx("circle",{className:"opacity-25",cx:"12",cy:"12",r:"10",stroke:"currentColor",strokeWidth:"4"}),jsx("path",{className:"opacity-75",fill:"currentColor",d:"M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"})]}),"Loading..."]}):"Load More Videos"})})]})]}):jsx("div",{children:"No playlists available."})};
2
- export{T as PlaylistsExplorer,P as VideoCard,T as default,u as fetchPlaylistVideos,U as getPlaylist};//# sourceMappingURL=index.mjs.map
1
+ import {useState,useEffect}from'react';import {jsxs,jsx,Fragment}from'react/jsx-runtime';var P=({title:i,thumbnailUrl:a,videoUrl:l,description:n,publishedAt:d,extraVideoFields:t=[]})=>{let f=o=>o?new Date(o).toLocaleDateString("en-US",{year:"numeric",month:"short",day:"numeric"}):"";return jsxs("a",{href:l,target:"_blank",rel:"noopener noreferrer",className:"rypg-card",children:[jsxs("div",{className:"rypg-card-media",children:[jsx("img",{src:a,alt:i,className:"rypg-card-img",loading:"lazy"}),jsx("div",{className:"rypg-card-overlay",children:jsx("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 24 24",fill:"currentColor",className:"rypg-play-icon",children:jsx("path",{fillRule:"evenodd",d:"M4.5 5.653c0-1.426 1.529-2.33 2.779-1.643l11.54 6.348c1.295.712 1.295 2.573 0 3.285L7.28 19.991c-1.25.687-2.779-.217-2.779-1.643V5.653z",clipRule:"evenodd"})})})]}),jsxs("div",{className:"rypg-card-content",children:[jsx("h3",{className:"rypg-card-title",children:i}),t.includes("description")&&n&&jsx("p",{className:"rypg-card-description",style:{fontSize:"0.875rem",marginTop:"0.5rem",opacity:.8},children:n}),t.includes("publishedAt")&&d&&jsx("p",{className:"rypg-card-date",style:{fontSize:"0.75rem",marginTop:"0.5rem",opacity:.6},children:f(d)})]})]})};var k="https://www.googleapis.com/youtube/v3";async function u(i,a,l){if(!a)return {error:"API Key is missing"};try{let n=`${k}/playlistItems?part=snippet,contentDetails&playlistId=${i}&maxResults=10&key=${a}`;l&&(n+=`&pageToken=${l}`);let t=await(await fetch(n)).json();return t.items?{videos:t.items.map(o=>({id:o.snippet.resourceId.videoId,title:o.snippet.title,thumbnailUrl:o.snippet.thumbnails.medium?.url||o.snippet.thumbnails.default?.url,videoUrl:`https://www.youtube.com/watch?v=${o.snippet.resourceId.videoId}`,description:o.snippet.description,publishedAt:o.snippet.publishedAt})).filter(o=>o.title!=="Private video"&&o.title!=="Deleted video"),nextPageToken:t.nextPageToken}:(console.error(`Failed to fetch items for playlist: ${i}`,t),{error:`YouTube API Error: ${t.error?.message||JSON.stringify(t)}`})}catch(n){return console.error(`Error fetching playlist items ${i}:`,n),{error:`Fetch Exception: ${n}`}}}async function I(i,a){if(!a)return console.warn("API Key is not set."),null;try{let n=await(await fetch(`${k}/playlists?part=snippet&id=${i}&key=${a}`)).json();if(!n.items||n.items.length===0)return console.error(`Playlist not found: ${i}`),null;let d=n.items[0].snippet,t=await u(i,a);return !t||t.error||!t.videos?null:{id:i,title:d.title,description:d.description,videos:t.videos,nextPageToken:t.nextPageToken}}catch(l){return console.error(`Error fetching playlist ${i}:`,l),null}}var C=i=>`repeat(auto-fill, minmax(${{2:280,3:220,4:180,5:150,6:130}[i]||180}px, 1fr))`,T=({playlists:i,apiKey:a,onLoadMore:l})=>{let[n,d]=useState(i),[t,f]=useState(i[0]?.id||""),[o,b]=useState(false),[w,x]=useState(false),r=n.find(e=>e.id===t);useEffect(()=>{!r||!a||r.videos||(async()=>{x(true);try{let c=await u(r.id,a);c.videos&&d(p=>p.map(g=>g.id===r.id?{...g,videos:c.videos||[],nextPageToken:c.nextPageToken}:g));}catch(c){console.error("Failed to fetch initial videos",c);}finally{x(false);}})();},[t,a]);let N=async()=>{if(!(!r||!r.nextPageToken)){b(true);try{let e=[],c;if(l){let p=await l(r.id,r.nextPageToken);e=p.videos,c=p.nextPageToken;}else if(a){let p=await u(r.id,a,r.nextPageToken);p.videos&&(e=p.videos,c=p.nextPageToken);}else {console.error("No apiKey or onLoadMore handler provided");return}e.length>0&&d(p=>p.map(g=>g.id===r.id?{...g,videos:[...g.videos||[],...e],nextPageToken:c}:g));}catch(e){console.error("Failed to load more videos",e);}finally{b(false);}}};return r?jsxs("div",{children:[jsx("div",{className:"rypg-flex rypg-flex-col rypg-sm-flex-row rypg-justify-between rypg-items-center rypg-mb-8 rypg-gap-4",children:jsx("div",{className:"rypg-flex rypg-space-x-2 rypg-overflow-x-auto rypg-pb-2 rypg-sm-pb-0 rypg-w-full rypg-sm-w-auto",children:n.map(e=>jsx("button",{onClick:()=>f(e.id),className:`rypg-btn-pill ${t===e.id?"rypg-btn-active":"rypg-btn-inactive"}`,children:e.config?.title||e.title},e.id))})}),jsxs("div",{className:"rypg-animate-fade-in",children:[jsxs("div",{className:"rypg-mb-6",children:[jsx("h2",{className:"rypg-title rypg-mb-2",children:r.title}),r.config?.showDescription&&r.description&&jsx("p",{className:"rypg-desc",children:r.description})]}),w?jsx("div",{className:"rypg-flex rypg-items-center rypg-justify-center rypg-mb-8",style:{minHeight:"200px"},children:jsxs("svg",{className:"rypg-spinner",style:{width:"2rem",height:"2rem",color:"white"},xmlns:"http://www.w3.org/2000/svg",fill:"none",viewBox:"0 0 24 24",children:[jsx("circle",{className:"opacity-25",cx:"12",cy:"12",r:"10",stroke:"currentColor",strokeWidth:"4"}),jsx("path",{className:"opacity-75",fill:"currentColor",d:"M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"})]})}):jsx("div",{className:"rypg-grid rypg-gap-6",style:{gridTemplateColumns:C(r.config?.gridColumns??4)},children:r.videos?.map(e=>jsx(P,{title:e.title,thumbnailUrl:e.thumbnailUrl,videoUrl:e.videoUrl,description:e.description,publishedAt:e.publishedAt,extraVideoFields:r.config?.extraVideoFields},e.id))}),!w&&r.nextPageToken&&jsx("div",{className:"mt-12 rypg-text-center",children:jsx("button",{onClick:N,disabled:o,className:"rypg-btn-load",children:o?jsxs(Fragment,{children:[jsxs("svg",{className:"rypg-spinner",xmlns:"http://www.w3.org/2000/svg",fill:"none",viewBox:"0 0 24 24",children:[jsx("circle",{className:"opacity-25",cx:"12",cy:"12",r:"10",stroke:"currentColor",strokeWidth:"4"}),jsx("path",{className:"opacity-75",fill:"currentColor",d:"M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"})]}),"Loading..."]}):"Load More Videos"})})]})]}):jsx("div",{children:"No playlists available."})};
2
+ export{T as PlaylistsExplorer,P as VideoCard,T as default,u as fetchPlaylistVideos,I as getPlaylist};//# sourceMappingURL=index.mjs.map
3
3
  //# sourceMappingURL=index.mjs.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/components/VideoCard.tsx","../src/utils/youtube.ts","../src/components/PlaylistsExplorer.tsx"],"names":["VideoCard","title","thumbnailUrl","videoUrl","description","publishedAt","columns","formatDate","dateString","jsxs","jsx","YOUTUBE_API_BASE","fetchPlaylistVideos","playlistId","apiKey","pageToken","url","itemsData","item","v","error","getPlaylist","playlistData","playlistSnippet","videosData","getGridColumns","n","PlaylistsExplorer","initialPlaylists","onLoadMore","playlists","setPlaylists","useState","activePlaylistId","setActivePlaylistId","loadingMore","setLoadingMore","initialLoading","setInitialLoading","activePlaylist","p","useEffect","result","prev","handleLoadMore","newVideos","newNextPageToken","prevPlaylists","playlist","video","Fragment"],"mappings":"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","file":"index.mjs","sourcesContent":["import React from 'react';\nimport type { VideoColumn } from '../types';\n\ninterface VideoCardProps {\n title: string;\n thumbnailUrl: string;\n videoUrl: string;\n description?: string;\n publishedAt?: string;\n}\n\ninterface VideoCardPropsInternal extends VideoCardProps {\n columns?: VideoColumn[];\n}\n\nexport const VideoCard: React.FC<VideoCardPropsInternal> = ({ title, thumbnailUrl, videoUrl, description, publishedAt, columns = ['thumbnail', 'title'] }) => {\n const formatDate = (dateString?: string) => {\n if (!dateString) return '';\n return new Date(dateString).toLocaleDateString('en-US', {\n year: 'numeric',\n month: 'short',\n day: 'numeric'\n });\n };\n\n return (\n <a\n href={videoUrl}\n target=\"_blank\"\n rel=\"noopener noreferrer\"\n className=\"rypg-card\"\n >\n {columns.includes('thumbnail') && (\n <div className=\"rypg-card-media\">\n <img\n src={thumbnailUrl}\n alt={title}\n className=\"rypg-card-img\"\n loading=\"lazy\"\n />\n <div className=\"rypg-card-overlay\">\n <svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 24 24\" fill=\"currentColor\" className=\"rypg-play-icon\">\n <path fillRule=\"evenodd\" d=\"M4.5 5.653c0-1.426 1.529-2.33 2.779-1.643l11.54 6.348c1.295.712 1.295 2.573 0 3.285L7.28 19.991c-1.25.687-2.779-.217-2.779-1.643V5.653z\" clipRule=\"evenodd\" />\n </svg>\n </div>\n </div>\n )}\n <div className=\"rypg-card-content\">\n {columns.includes('title') && (\n <h3 className=\"rypg-card-title\">\n {title}\n </h3>\n )}\n {columns.includes('description') && description && (\n <p className=\"rypg-card-description\" style={{ fontSize: '0.875rem', marginTop: '0.5rem', opacity: 0.8 }}>\n {description}\n </p>\n )}\n {columns.includes('publishedAt') && publishedAt && (\n <p className=\"rypg-card-date\" style={{ fontSize: '0.75rem', marginTop: '0.5rem', opacity: 0.6 }}>\n {formatDate(publishedAt)}\n </p>\n )}\n </div>\n </a>\n );\n};\n","import type { Playlist, Video } from \"../types\";\n\nconst YOUTUBE_API_BASE = \"https://www.googleapis.com/youtube/v3\";\n\ninterface FetchVideosResult {\n videos?: Video[];\n nextPageToken?: string;\n error?: string;\n}\n\nexport async function fetchPlaylistVideos(playlistId: string, apiKey: string, pageToken?: string): Promise<FetchVideosResult> {\n if (!apiKey) return { error: \"API Key is missing\" };\n\n try {\n let url = `${YOUTUBE_API_BASE}/playlistItems?part=snippet,contentDetails&playlistId=${playlistId}&maxResults=10&key=${apiKey}`;\n if (pageToken) {\n url += `&pageToken=${pageToken}`;\n }\n\n const itemsResponse = await fetch(url);\n const itemsData = await itemsResponse.json();\n\n if (!itemsData.items) {\n console.error(`Failed to fetch items for playlist: ${playlistId}`, itemsData);\n return { error: `YouTube API Error: ${itemsData.error?.message || JSON.stringify(itemsData)}` };\n }\n\n const videos: Video[] = itemsData.items.map((item: any) => ({\n id: item.snippet.resourceId.videoId,\n title: item.snippet.title,\n thumbnailUrl: item.snippet.thumbnails.medium?.url || item.snippet.thumbnails.default?.url,\n videoUrl: `https://www.youtube.com/watch?v=${item.snippet.resourceId.videoId}`,\n description: item.snippet.description,\n publishedAt: item.snippet.publishedAt,\n })).filter((v: Video) => v.title !== \"Private video\" && v.title !== \"Deleted video\");\n\n return {\n videos,\n nextPageToken: itemsData.nextPageToken\n };\n } catch (error) {\n console.error(`Error fetching playlist items ${playlistId}:`, error);\n return { error: `Fetch Exception: ${error}` };\n }\n}\n\nexport async function getPlaylist(playlistId: string, apiKey: string): Promise<Playlist | null> {\n if (!apiKey) {\n console.warn(\"API Key is not set.\");\n return null;\n }\n\n try {\n // 1. Get Playlist Details\n const playlistResponse = await fetch(\n `${YOUTUBE_API_BASE}/playlists?part=snippet&id=${playlistId}&key=${apiKey}`\n );\n const playlistData = await playlistResponse.json();\n\n if (!playlistData.items || playlistData.items.length === 0) {\n console.error(`Playlist not found: ${playlistId}`);\n return null;\n }\n\n const playlistSnippet = playlistData.items[0].snippet;\n\n // 2. Get First Batch of Videos\n const videosData = await fetchPlaylistVideos(playlistId, apiKey);\n\n if (!videosData || videosData.error || !videosData.videos) return null;\n\n return {\n id: playlistId,\n title: playlistSnippet.title,\n description: playlistSnippet.description,\n videos: videosData.videos,\n nextPageToken: videosData.nextPageToken\n };\n\n } catch (error) {\n console.error(`Error fetching playlist ${playlistId}:`, error);\n return null;\n }\n}\n","import React, { useState, useEffect } from 'react';\nimport type { Playlist, Video } from '../types';\nimport { VideoCard } from './VideoCard';\nimport { fetchPlaylistVideos } from '../utils/youtube';\n\nconst getGridColumns = (n: number): string => {\n const widthMap: { [key: number]: number } = {\n 2: 280,\n 3: 220,\n 4: 180,\n 5: 150,\n 6: 130,\n };\n const minWidth = widthMap[n] || 180;\n return `repeat(auto-fill, minmax(${minWidth}px, 1fr))`;\n};\n\ninterface PlaylistsExplorerProps {\n playlists: Playlist[];\n apiKey?: string;\n onLoadMore?: (playlistId: string, pageToken: string) => Promise<{ videos: Video[], nextPageToken?: string }>;\n}\n\nexport const PlaylistsExplorer: React.FC<PlaylistsExplorerProps> = ({ playlists: initialPlaylists, apiKey, onLoadMore }) => {\n // We maintain a local state of playlists to store fetched videos\n const [playlists, setPlaylists] = useState<Playlist[]>(initialPlaylists);\n const [activePlaylistId, setActivePlaylistId] = useState<string>(initialPlaylists[0]?.id || \"\");\n const [loadingMore, setLoadingMore] = useState<boolean>(false);\n const [initialLoading, setInitialLoading] = useState<boolean>(false);\n\n const activePlaylist = playlists.find(p => p.id === activePlaylistId);\n\n // Initial Fetch Effect: When active playlist changes, if it has no videos, fetch them\n useEffect(() => {\n if (!activePlaylist || !apiKey) return;\n\n // If videos are undefined or empty array (and we haven't fetched yet - we can check if nextPageToken is missing and videos empty implies not fetched if it's a fresh config)\n // A better check: if videos is undefined, we definitely need to fetch. \n // If videos is [], it might be empty playlist or not fetched. \n // Let's assume if it's undefined we fetch.\n if (!activePlaylist.videos) {\n const fetchInitial = async () => {\n setInitialLoading(true);\n try {\n const result = await fetchPlaylistVideos(activePlaylist.id, apiKey);\n if (result.videos) {\n setPlaylists(prev => prev.map(p => {\n if (p.id === activePlaylist.id) {\n return {\n ...p,\n videos: result.videos || [], // Ensure it's an array\n nextPageToken: result.nextPageToken,\n // Also fill in title/desc if missing from config? The API returns items, not playlist details here.\n // Ideally we would also fetch playlist details but for now let's just get videos.\n };\n }\n return p;\n }));\n }\n } catch (error) {\n console.error(\"Failed to fetch initial videos\", error);\n } finally {\n setInitialLoading(false);\n }\n };\n fetchInitial();\n }\n }, [activePlaylistId, apiKey]); // We purposefully don't include activePlaylist to avoid loops, rely on ID check or check content inside\n\n const handleLoadMore = async () => {\n if (!activePlaylist || !activePlaylist.nextPageToken) return;\n\n setLoadingMore(true);\n try {\n let newVideos: Video[] = [];\n let newNextPageToken: string | undefined;\n\n if (onLoadMore) {\n const result = await onLoadMore(activePlaylist.id, activePlaylist.nextPageToken);\n newVideos = result.videos;\n newNextPageToken = result.nextPageToken;\n } else if (apiKey) {\n const result = await fetchPlaylistVideos(activePlaylist.id, apiKey, activePlaylist.nextPageToken);\n if (result.videos) {\n newVideos = result.videos;\n newNextPageToken = result.nextPageToken;\n }\n } else {\n console.error(\"No apiKey or onLoadMore handler provided\");\n return;\n }\n\n if (newVideos.length > 0) {\n setPlaylists(prevPlaylists => prevPlaylists.map(p => {\n if (p.id === activePlaylist.id) {\n return {\n ...p,\n videos: [...(p.videos || []), ...newVideos],\n nextPageToken: newNextPageToken\n };\n }\n return p;\n }));\n }\n } catch (error) {\n console.error(\"Failed to load more videos\", error);\n } finally {\n setLoadingMore(false);\n }\n };\n\n if (!activePlaylist) return <div>No playlists available.</div>;\n\n return (\n <div>\n {/* Navigation & Tabs */}\n <div className=\"rypg-flex rypg-flex-col rypg-sm-flex-row rypg-justify-between rypg-items-center rypg-mb-8 rypg-gap-4\">\n <div className=\"rypg-flex rypg-space-x-2 rypg-overflow-x-auto rypg-pb-2 rypg-sm-pb-0 rypg-w-full rypg-sm-w-auto\">\n {playlists.map(playlist => (\n <button\n key={playlist.id}\n onClick={() => setActivePlaylistId(playlist.id)}\n className={`rypg-btn-pill ${activePlaylistId === playlist.id\n ? 'rypg-btn-active'\n : 'rypg-btn-inactive'\n }`}\n >\n {playlist.config?.title || playlist.title}\n </button>\n ))}\n </div>\n </div>\n\n {/* Playlist Content */}\n <div className=\"rypg-animate-fade-in\">\n {/* Always show title and description if enabled */}\n <div className=\"rypg-mb-6\">\n <h2 className=\"rypg-title rypg-mb-2\">{activePlaylist.title}</h2>\n {activePlaylist.config?.showDescription && activePlaylist.description && (\n <p className=\"rypg-desc\">{activePlaylist.description}</p>\n )}\n </div>\n\n {initialLoading ? (\n <div className=\"rypg-flex rypg-items-center rypg-justify-center rypg-mb-8\" style={{ minHeight: '200px' }}>\n <svg className=\"rypg-spinner\" style={{ width: '2rem', height: '2rem', color: 'white' }} xmlns=\"http://www.w3.org/2000/svg\" fill=\"none\" viewBox=\"0 0 24 24\">\n <circle className=\"opacity-25\" cx=\"12\" cy=\"12\" r=\"10\" stroke=\"currentColor\" strokeWidth=\"4\"></circle>\n <path className=\"opacity-75\" fill=\"currentColor\" d=\"M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z\"></path>\n </svg>\n </div>\n ) : (\n <div className=\"rypg-grid rypg-gap-6\" style={{ gridTemplateColumns: getGridColumns(activePlaylist.config?.gridColumns ?? 4) }}>\n {activePlaylist.videos?.map((video) => (\n <VideoCard\n key={video.id}\n title={video.title}\n thumbnailUrl={video.thumbnailUrl}\n videoUrl={video.videoUrl}\n description={video.description}\n publishedAt={video.publishedAt}\n columns={activePlaylist.config?.columns}\n />\n ))}\n </div>\n )}\n\n {/* Load More */}\n {!initialLoading && activePlaylist.nextPageToken && (\n <div className=\"mt-12 rypg-text-center\">\n <button\n onClick={handleLoadMore}\n disabled={loadingMore}\n className=\"rypg-btn-load\"\n >\n {loadingMore ? (\n <>\n <svg className=\"rypg-spinner\" xmlns=\"http://www.w3.org/2000/svg\" fill=\"none\" viewBox=\"0 0 24 24\">\n <circle className=\"opacity-25\" cx=\"12\" cy=\"12\" r=\"10\" stroke=\"currentColor\" strokeWidth=\"4\"></circle>\n <path className=\"opacity-75\" fill=\"currentColor\" d=\"M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z\"></path>\n </svg>\n Loading...\n </>\n ) : (\n 'Load More Videos'\n )}\n </button>\n </div>\n )}\n </div>\n </div>\n );\n};\n"]}
1
+ {"version":3,"sources":["../src/components/VideoCard.tsx","../src/utils/youtube.ts","../src/components/PlaylistsExplorer.tsx"],"names":["VideoCard","title","thumbnailUrl","videoUrl","description","publishedAt","extraVideoFields","formatDate","dateString","jsxs","jsx","YOUTUBE_API_BASE","fetchPlaylistVideos","playlistId","apiKey","pageToken","url","itemsData","item","v","error","getPlaylist","playlistData","playlistSnippet","videosData","getGridColumns","n","PlaylistsExplorer","initialPlaylists","onLoadMore","playlists","setPlaylists","useState","activePlaylistId","setActivePlaylistId","loadingMore","setLoadingMore","initialLoading","setInitialLoading","activePlaylist","p","useEffect","result","prev","handleLoadMore","newVideos","newNextPageToken","prevPlaylists","playlist","video","Fragment"],"mappings":"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","file":"index.mjs","sourcesContent":["import React from 'react';\nimport type { VideoField } from '../types';\n\ninterface VideoCardProps {\n title: string;\n thumbnailUrl: string;\n videoUrl: string;\n description?: string;\n publishedAt?: string;\n}\n\ninterface VideoCardPropsInternal extends VideoCardProps {\n extraVideoFields?: VideoField[];\n}\n\nexport const VideoCard: React.FC<VideoCardPropsInternal> = ({ title, thumbnailUrl, videoUrl, description, publishedAt, extraVideoFields = [] }) => {\n const formatDate = (dateString?: string) => {\n if (!dateString) return '';\n return new Date(dateString).toLocaleDateString('en-US', {\n year: 'numeric',\n month: 'short',\n day: 'numeric'\n });\n };\n\n return (\n <a\n href={videoUrl}\n target=\"_blank\"\n rel=\"noopener noreferrer\"\n className=\"rypg-card\"\n >\n <div className=\"rypg-card-media\">\n <img\n src={thumbnailUrl}\n alt={title}\n className=\"rypg-card-img\"\n loading=\"lazy\"\n />\n <div className=\"rypg-card-overlay\">\n <svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 24 24\" fill=\"currentColor\" className=\"rypg-play-icon\">\n <path fillRule=\"evenodd\" d=\"M4.5 5.653c0-1.426 1.529-2.33 2.779-1.643l11.54 6.348c1.295.712 1.295 2.573 0 3.285L7.28 19.991c-1.25.687-2.779-.217-2.779-1.643V5.653z\" clipRule=\"evenodd\" />\n </svg>\n </div>\n </div>\n <div className=\"rypg-card-content\">\n <h3 className=\"rypg-card-title\">\n {title}\n </h3>\n {extraVideoFields.includes('description') && description && (\n <p className=\"rypg-card-description\" style={{ fontSize: '0.875rem', marginTop: '0.5rem', opacity: 0.8 }}>\n {description}\n </p>\n )}\n {extraVideoFields.includes('publishedAt') && publishedAt && (\n <p className=\"rypg-card-date\" style={{ fontSize: '0.75rem', marginTop: '0.5rem', opacity: 0.6 }}>\n {formatDate(publishedAt)}\n </p>\n )}\n </div>\n </a>\n );\n};\n","import type { Playlist, Video } from \"../types\";\n\nconst YOUTUBE_API_BASE = \"https://www.googleapis.com/youtube/v3\";\n\ninterface FetchVideosResult {\n videos?: Video[];\n nextPageToken?: string;\n error?: string;\n}\n\nexport async function fetchPlaylistVideos(playlistId: string, apiKey: string, pageToken?: string): Promise<FetchVideosResult> {\n if (!apiKey) return { error: \"API Key is missing\" };\n\n try {\n let url = `${YOUTUBE_API_BASE}/playlistItems?part=snippet,contentDetails&playlistId=${playlistId}&maxResults=10&key=${apiKey}`;\n if (pageToken) {\n url += `&pageToken=${pageToken}`;\n }\n\n const itemsResponse = await fetch(url);\n const itemsData = await itemsResponse.json();\n\n if (!itemsData.items) {\n console.error(`Failed to fetch items for playlist: ${playlistId}`, itemsData);\n return { error: `YouTube API Error: ${itemsData.error?.message || JSON.stringify(itemsData)}` };\n }\n\n const videos: Video[] = itemsData.items.map((item: any) => ({\n id: item.snippet.resourceId.videoId,\n title: item.snippet.title,\n thumbnailUrl: item.snippet.thumbnails.medium?.url || item.snippet.thumbnails.default?.url,\n videoUrl: `https://www.youtube.com/watch?v=${item.snippet.resourceId.videoId}`,\n description: item.snippet.description,\n publishedAt: item.snippet.publishedAt,\n })).filter((v: Video) => v.title !== \"Private video\" && v.title !== \"Deleted video\");\n\n return {\n videos,\n nextPageToken: itemsData.nextPageToken\n };\n } catch (error) {\n console.error(`Error fetching playlist items ${playlistId}:`, error);\n return { error: `Fetch Exception: ${error}` };\n }\n}\n\nexport async function getPlaylist(playlistId: string, apiKey: string): Promise<Playlist | null> {\n if (!apiKey) {\n console.warn(\"API Key is not set.\");\n return null;\n }\n\n try {\n // 1. Get Playlist Details\n const playlistResponse = await fetch(\n `${YOUTUBE_API_BASE}/playlists?part=snippet&id=${playlistId}&key=${apiKey}`\n );\n const playlistData = await playlistResponse.json();\n\n if (!playlistData.items || playlistData.items.length === 0) {\n console.error(`Playlist not found: ${playlistId}`);\n return null;\n }\n\n const playlistSnippet = playlistData.items[0].snippet;\n\n // 2. Get First Batch of Videos\n const videosData = await fetchPlaylistVideos(playlistId, apiKey);\n\n if (!videosData || videosData.error || !videosData.videos) return null;\n\n return {\n id: playlistId,\n title: playlistSnippet.title,\n description: playlistSnippet.description,\n videos: videosData.videos,\n nextPageToken: videosData.nextPageToken\n };\n\n } catch (error) {\n console.error(`Error fetching playlist ${playlistId}:`, error);\n return null;\n }\n}\n","import React, { useState, useEffect } from 'react';\nimport type { Playlist, Video } from '../types';\nimport { VideoCard } from './VideoCard';\nimport { fetchPlaylistVideos } from '../utils/youtube';\n\nconst getGridColumns = (n: number): string => {\n const widthMap: { [key: number]: number } = {\n 2: 280,\n 3: 220,\n 4: 180,\n 5: 150,\n 6: 130,\n };\n const minWidth = widthMap[n] || 180;\n return `repeat(auto-fill, minmax(${minWidth}px, 1fr))`;\n};\n\ninterface PlaylistsExplorerProps {\n playlists: Playlist[];\n apiKey?: string;\n onLoadMore?: (playlistId: string, pageToken: string) => Promise<{ videos: Video[], nextPageToken?: string }>;\n}\n\nexport const PlaylistsExplorer: React.FC<PlaylistsExplorerProps> = ({ playlists: initialPlaylists, apiKey, onLoadMore }) => {\n // We maintain a local state of playlists to store fetched videos\n const [playlists, setPlaylists] = useState<Playlist[]>(initialPlaylists);\n const [activePlaylistId, setActivePlaylistId] = useState<string>(initialPlaylists[0]?.id || \"\");\n const [loadingMore, setLoadingMore] = useState<boolean>(false);\n const [initialLoading, setInitialLoading] = useState<boolean>(false);\n\n const activePlaylist = playlists.find(p => p.id === activePlaylistId);\n\n // Initial Fetch Effect: When active playlist changes, if it has no videos, fetch them\n useEffect(() => {\n if (!activePlaylist || !apiKey) return;\n\n // If videos are undefined or empty array (and we haven't fetched yet - we can check if nextPageToken is missing and videos empty implies not fetched if it's a fresh config)\n // A better check: if videos is undefined, we definitely need to fetch. \n // If videos is [], it might be empty playlist or not fetched. \n // Let's assume if it's undefined we fetch.\n if (!activePlaylist.videos) {\n const fetchInitial = async () => {\n setInitialLoading(true);\n try {\n const result = await fetchPlaylistVideos(activePlaylist.id, apiKey);\n if (result.videos) {\n setPlaylists(prev => prev.map(p => {\n if (p.id === activePlaylist.id) {\n return {\n ...p,\n videos: result.videos || [], // Ensure it's an array\n nextPageToken: result.nextPageToken,\n // Also fill in title/desc if missing from config? The API returns items, not playlist details here.\n // Ideally we would also fetch playlist details but for now let's just get videos.\n };\n }\n return p;\n }));\n }\n } catch (error) {\n console.error(\"Failed to fetch initial videos\", error);\n } finally {\n setInitialLoading(false);\n }\n };\n fetchInitial();\n }\n }, [activePlaylistId, apiKey]); // We purposefully don't include activePlaylist to avoid loops, rely on ID check or check content inside\n\n const handleLoadMore = async () => {\n if (!activePlaylist || !activePlaylist.nextPageToken) return;\n\n setLoadingMore(true);\n try {\n let newVideos: Video[] = [];\n let newNextPageToken: string | undefined;\n\n if (onLoadMore) {\n const result = await onLoadMore(activePlaylist.id, activePlaylist.nextPageToken);\n newVideos = result.videos;\n newNextPageToken = result.nextPageToken;\n } else if (apiKey) {\n const result = await fetchPlaylistVideos(activePlaylist.id, apiKey, activePlaylist.nextPageToken);\n if (result.videos) {\n newVideos = result.videos;\n newNextPageToken = result.nextPageToken;\n }\n } else {\n console.error(\"No apiKey or onLoadMore handler provided\");\n return;\n }\n\n if (newVideos.length > 0) {\n setPlaylists(prevPlaylists => prevPlaylists.map(p => {\n if (p.id === activePlaylist.id) {\n return {\n ...p,\n videos: [...(p.videos || []), ...newVideos],\n nextPageToken: newNextPageToken\n };\n }\n return p;\n }));\n }\n } catch (error) {\n console.error(\"Failed to load more videos\", error);\n } finally {\n setLoadingMore(false);\n }\n };\n\n if (!activePlaylist) return <div>No playlists available.</div>;\n\n return (\n <div>\n {/* Navigation & Tabs */}\n <div className=\"rypg-flex rypg-flex-col rypg-sm-flex-row rypg-justify-between rypg-items-center rypg-mb-8 rypg-gap-4\">\n <div className=\"rypg-flex rypg-space-x-2 rypg-overflow-x-auto rypg-pb-2 rypg-sm-pb-0 rypg-w-full rypg-sm-w-auto\">\n {playlists.map(playlist => (\n <button\n key={playlist.id}\n onClick={() => setActivePlaylistId(playlist.id)}\n className={`rypg-btn-pill ${activePlaylistId === playlist.id\n ? 'rypg-btn-active'\n : 'rypg-btn-inactive'\n }`}\n >\n {playlist.config?.title || playlist.title}\n </button>\n ))}\n </div>\n </div>\n\n {/* Playlist Content */}\n <div className=\"rypg-animate-fade-in\">\n {/* Always show title and description if enabled */}\n <div className=\"rypg-mb-6\">\n <h2 className=\"rypg-title rypg-mb-2\">{activePlaylist.title}</h2>\n {activePlaylist.config?.showDescription && activePlaylist.description && (\n <p className=\"rypg-desc\">{activePlaylist.description}</p>\n )}\n </div>\n\n {initialLoading ? (\n <div className=\"rypg-flex rypg-items-center rypg-justify-center rypg-mb-8\" style={{ minHeight: '200px' }}>\n <svg className=\"rypg-spinner\" style={{ width: '2rem', height: '2rem', color: 'white' }} xmlns=\"http://www.w3.org/2000/svg\" fill=\"none\" viewBox=\"0 0 24 24\">\n <circle className=\"opacity-25\" cx=\"12\" cy=\"12\" r=\"10\" stroke=\"currentColor\" strokeWidth=\"4\"></circle>\n <path className=\"opacity-75\" fill=\"currentColor\" d=\"M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z\"></path>\n </svg>\n </div>\n ) : (\n <div className=\"rypg-grid rypg-gap-6\" style={{ gridTemplateColumns: getGridColumns(activePlaylist.config?.gridColumns ?? 4) }}>\n {activePlaylist.videos?.map((video) => (\n <VideoCard\n key={video.id}\n title={video.title}\n thumbnailUrl={video.thumbnailUrl}\n videoUrl={video.videoUrl}\n description={video.description}\n publishedAt={video.publishedAt}\n extraVideoFields={activePlaylist.config?.extraVideoFields}\n />\n ))}\n </div>\n )}\n\n {/* Load More */}\n {!initialLoading && activePlaylist.nextPageToken && (\n <div className=\"mt-12 rypg-text-center\">\n <button\n onClick={handleLoadMore}\n disabled={loadingMore}\n className=\"rypg-btn-load\"\n >\n {loadingMore ? (\n <>\n <svg className=\"rypg-spinner\" xmlns=\"http://www.w3.org/2000/svg\" fill=\"none\" viewBox=\"0 0 24 24\">\n <circle className=\"opacity-25\" cx=\"12\" cy=\"12\" r=\"10\" stroke=\"currentColor\" strokeWidth=\"4\"></circle>\n <path className=\"opacity-75\" fill=\"currentColor\" d=\"M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z\"></path>\n </svg>\n Loading...\n </>\n ) : (\n 'Load More Videos'\n )}\n </button>\n </div>\n )}\n </div>\n </div>\n );\n};\n"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "react-youtube-playlist-grid",
3
- "version": "1.0.5",
3
+ "version": "1.0.6",
4
4
  "description": "A React component for displaying YouTube playlists in a grid with load more functionality.",
5
5
  "main": "./dist/index.js",
6
6
  "module": "./dist/index.mjs",
@@ -158,7 +158,7 @@ export const PlaylistsExplorer: React.FC<PlaylistsExplorerProps> = ({ playlists:
158
158
  videoUrl={video.videoUrl}
159
159
  description={video.description}
160
160
  publishedAt={video.publishedAt}
161
- columns={activePlaylist.config?.columns}
161
+ extraVideoFields={activePlaylist.config?.extraVideoFields}
162
162
  />
163
163
  ))}
164
164
  </div>
@@ -1,5 +1,5 @@
1
1
  import React from 'react';
2
- import type { VideoColumn } from '../types';
2
+ import type { VideoField } from '../types';
3
3
 
4
4
  interface VideoCardProps {
5
5
  title: string;
@@ -10,10 +10,10 @@ interface VideoCardProps {
10
10
  }
11
11
 
12
12
  interface VideoCardPropsInternal extends VideoCardProps {
13
- columns?: VideoColumn[];
13
+ extraVideoFields?: VideoField[];
14
14
  }
15
15
 
16
- export const VideoCard: React.FC<VideoCardPropsInternal> = ({ title, thumbnailUrl, videoUrl, description, publishedAt, columns = ['thumbnail', 'title'] }) => {
16
+ export const VideoCard: React.FC<VideoCardPropsInternal> = ({ title, thumbnailUrl, videoUrl, description, publishedAt, extraVideoFields = [] }) => {
17
17
  const formatDate = (dateString?: string) => {
18
18
  if (!dateString) return '';
19
19
  return new Date(dateString).toLocaleDateString('en-US', {
@@ -30,33 +30,29 @@ export const VideoCard: React.FC<VideoCardPropsInternal> = ({ title, thumbnailUr
30
30
  rel="noopener noreferrer"
31
31
  className="rypg-card"
32
32
  >
33
- {columns.includes('thumbnail') && (
34
- <div className="rypg-card-media">
35
- <img
36
- src={thumbnailUrl}
37
- alt={title}
38
- className="rypg-card-img"
39
- loading="lazy"
40
- />
41
- <div className="rypg-card-overlay">
42
- <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" className="rypg-play-icon">
43
- <path fillRule="evenodd" d="M4.5 5.653c0-1.426 1.529-2.33 2.779-1.643l11.54 6.348c1.295.712 1.295 2.573 0 3.285L7.28 19.991c-1.25.687-2.779-.217-2.779-1.643V5.653z" clipRule="evenodd" />
44
- </svg>
45
- </div>
33
+ <div className="rypg-card-media">
34
+ <img
35
+ src={thumbnailUrl}
36
+ alt={title}
37
+ className="rypg-card-img"
38
+ loading="lazy"
39
+ />
40
+ <div className="rypg-card-overlay">
41
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" className="rypg-play-icon">
42
+ <path fillRule="evenodd" d="M4.5 5.653c0-1.426 1.529-2.33 2.779-1.643l11.54 6.348c1.295.712 1.295 2.573 0 3.285L7.28 19.991c-1.25.687-2.779-.217-2.779-1.643V5.653z" clipRule="evenodd" />
43
+ </svg>
46
44
  </div>
47
- )}
45
+ </div>
48
46
  <div className="rypg-card-content">
49
- {columns.includes('title') && (
50
- <h3 className="rypg-card-title">
51
- {title}
52
- </h3>
53
- )}
54
- {columns.includes('description') && description && (
47
+ <h3 className="rypg-card-title">
48
+ {title}
49
+ </h3>
50
+ {extraVideoFields.includes('description') && description && (
55
51
  <p className="rypg-card-description" style={{ fontSize: '0.875rem', marginTop: '0.5rem', opacity: 0.8 }}>
56
52
  {description}
57
53
  </p>
58
54
  )}
59
- {columns.includes('publishedAt') && publishedAt && (
55
+ {extraVideoFields.includes('publishedAt') && publishedAt && (
60
56
  <p className="rypg-card-date" style={{ fontSize: '0.75rem', marginTop: '0.5rem', opacity: 0.6 }}>
61
57
  {formatDate(publishedAt)}
62
58
  </p>
@@ -1,10 +1,10 @@
1
- export type VideoColumn = 'thumbnail' | 'title' | 'description' | 'publishedAt';
1
+ export type VideoField = 'description' | 'publishedAt';
2
2
 
3
3
  export interface PlaylistConfig {
4
4
  id: string;
5
5
  showDescription: boolean;
6
6
  title: string;
7
- columns?: VideoColumn[];
7
+ extraVideoFields?: VideoField[];
8
8
  gridColumns?: number;
9
9
  }
10
10