react-youtube-playlist-grid 1.0.4 → 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.css CHANGED
@@ -1,2 +1,2 @@
1
- :root{--rypg-brand-500: #8b5cf6;--rypg-brand-600: #7c3aed;--rypg-brand-300: #c4b5fd;--rypg-slate-800: #1e293b;--rypg-slate-700: #334155;--rypg-slate-600: #475569;--rypg-slate-400: #94a3b8;--rypg-white: #ffffff;--rypg-black-overlay: rgba(0, 0, 0, .4)}@keyframes rypg-spin{0%{transform:rotate(0)}to{transform:rotate(360deg)}}@keyframes rypg-fade-in{0%{opacity:0;transform:translateY(10px)}to{opacity:1;transform:translateY(0)}}.rypg-flex{display:flex}.rypg-flex-col{flex-direction:column}.rypg-items-center{align-items:center}.rypg-justify-between{justify-content:space-between}.rypg-gap-4{gap:1rem}.rypg-mb-8{margin-bottom:2rem}.rypg-mb-6{margin-bottom:1.5rem}.rypg-mb-2{margin-bottom:.5rem}.rypg-pb-2{padding-bottom:.5rem}.rypg-w-full{width:100%}.rypg-overflow-x-auto{overflow-x:auto}.rypg-space-x-2>*+*{margin-left:.5rem}@media(min-width:640px){.rypg-sm-flex-row{flex-direction:row}.rypg-sm-pb-0{padding-bottom:0}.rypg-sm-w-auto{width:auto}.rypg-sm-grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}}@media(min-width:1024px){.rypg-lg-grid-cols-3{grid-template-columns:repeat(3,minmax(0,1fr))}}@media(min-width:1280px){.rypg-xl-grid-cols-4{grid-template-columns:repeat(4,minmax(0,1fr))}}.rypg-grid{display:grid}.rypg-grid-cols-1{grid-template-columns:repeat(1,minmax(0,1fr))}.rypg-gap-6{gap:1.5rem}.rypg-btn-pill{padding:.5rem 1rem;border-radius:9999px;font-size:.875rem;font-weight:500;white-space:nowrap;transition:all .2s;border:none;cursor:pointer}.rypg-btn-active{background-color:var(--rypg-brand-600);color:var(--rypg-white);box-shadow:0 10px 15px -3px #8b5cf64d}.rypg-btn-inactive{background-color:var(--rypg-slate-800);color:var(--rypg-slate-400)}.rypg-btn-inactive:hover{color:var(--rypg-white);background-color:var(--rypg-slate-700)}.rypg-title{font-size:1.25rem;font-weight:700;color:var(--rypg-white)}.rypg-desc{color:var(--rypg-slate-400);max-width:48rem;margin:0}.rypg-btn-load{display:inline-flex;align-items:center;padding:.75rem 1.5rem;border:1px solid transparent;font-size:1rem;font-weight:500;border-radius:.375rem;color:var(--rypg-white);background-color:var(--rypg-slate-700);cursor:pointer;transition:background-color .2s}.rypg-btn-load:hover{background-color:var(--rypg-slate-600)}.rypg-btn-load:disabled{opacity:.5;cursor:not-allowed}.rypg-spinner{animation:rypg-spin 1s linear infinite;margin-right:.75rem;height:1.25rem;width:1.25rem}.rypg-card{display:block;position:relative;overflow:hidden;border-radius:.5rem;background-color:var(--rypg-slate-800);text-decoration:none;transition:all .3s;transform:translateZ(0);box-shadow:0 0 0 1px #ffffff1a}.rypg-card:hover{transform:translateY(-.25rem);box-shadow:0 0 20px #8b5cf626;border-color:#8b5cf680}.rypg-card-media{aspect-ratio:16 / 9;width:100%;overflow:hidden;position:relative}.rypg-card-img{width:100%;height:100%;object-fit:cover;transition:transform .5s}.rypg-card:hover .rypg-card-img{transform:scale(1.05)}.rypg-card-overlay{position:absolute;inset:0;background-color:var(--rypg-black-overlay);opacity:0;display:flex;align-items:center;justify-content:center;transition:opacity .3s}.rypg-card:hover .rypg-card-overlay{opacity:1}.rypg-play-icon{width:3rem;height:3rem;color:var(--rypg-white);filter:drop-shadow(0 4px 6px rgba(0,0,0,.5))}.rypg-card-content{padding:1rem}.rypg-card-title{font-size:.875rem;font-weight:600;color:var(--rypg-white);margin:0;display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden;line-height:1.375;transition:color .2s}.rypg-card:hover .rypg-card-title{color:var(--rypg-brand-300)}.rypg-animate-fade-in{animation:rypg-fade-in .5s ease-out}.rypg-text-center{text-align:center}
1
+ :root{--rypg-brand-500: #8b5cf6;--rypg-brand-600: #7c3aed;--rypg-brand-300: #c4b5fd;--rypg-slate-800: #1e293b;--rypg-slate-700: #334155;--rypg-slate-600: #475569;--rypg-slate-400: #94a3b8;--rypg-white: #ffffff;--rypg-black-overlay: rgba(0, 0, 0, .4)}@keyframes rypg-spin{0%{transform:rotate(0)}to{transform:rotate(360deg)}}@keyframes rypg-fade-in{0%{opacity:0;transform:translateY(10px)}to{opacity:1;transform:translateY(0)}}.rypg-flex{display:flex}.rypg-flex-col{flex-direction:column}.rypg-items-center{align-items:center}.rypg-justify-between{justify-content:space-between}.rypg-gap-4{gap:1rem}.rypg-mb-8{margin-bottom:2rem}.rypg-mb-6{margin-bottom:1.5rem}.rypg-mb-2{margin-bottom:.5rem}.rypg-pb-2{padding-bottom:.5rem}.rypg-w-full{width:100%}.rypg-overflow-x-auto{overflow-x:auto}.rypg-space-x-2>*+*{margin-left:.5rem}@media(min-width:640px){.rypg-sm-flex-row{flex-direction:row}.rypg-sm-pb-0{padding-bottom:0}.rypg-sm-w-auto{width:auto}}.rypg-grid{display:grid}.rypg-gap-6{gap:1.5rem}.rypg-btn-pill{padding:.5rem 1rem;border-radius:9999px;font-size:.875rem;font-weight:500;white-space:nowrap;transition:all .2s;border:none;cursor:pointer}.rypg-btn-active{background-color:var(--rypg-brand-600);color:var(--rypg-white);box-shadow:0 10px 15px -3px #8b5cf64d}.rypg-btn-inactive{background-color:var(--rypg-slate-800);color:var(--rypg-slate-400)}.rypg-btn-inactive:hover{color:var(--rypg-white);background-color:var(--rypg-slate-700)}.rypg-title{font-size:1.25rem;font-weight:700;color:var(--rypg-white)}.rypg-desc{color:var(--rypg-slate-400);max-width:48rem;margin:0}.rypg-btn-load{display:inline-flex;align-items:center;padding:.75rem 1.5rem;border:1px solid transparent;font-size:1rem;font-weight:500;border-radius:.375rem;color:var(--rypg-white);background-color:var(--rypg-slate-700);cursor:pointer;transition:background-color .2s}.rypg-btn-load:hover{background-color:var(--rypg-slate-600)}.rypg-btn-load:disabled{opacity:.5;cursor:not-allowed}.rypg-spinner{animation:rypg-spin 1s linear infinite;margin-right:.75rem;height:1.25rem;width:1.25rem}.rypg-card{display:block;position:relative;overflow:hidden;border-radius:.5rem;background-color:var(--rypg-slate-800);text-decoration:none;transition:all .3s;transform:translateZ(0);box-shadow:0 0 0 1px #ffffff1a}.rypg-card:hover{transform:translateY(-.25rem);box-shadow:0 0 20px #8b5cf626;border-color:#8b5cf680}.rypg-card-media{aspect-ratio:16 / 9;width:100%;overflow:hidden;position:relative}.rypg-card-img{width:100%;height:100%;object-fit:cover;transition:transform .5s}.rypg-card:hover .rypg-card-img{transform:scale(1.05)}.rypg-card-overlay{position:absolute;inset:0;background-color:var(--rypg-black-overlay);opacity:0;display:flex;align-items:center;justify-content:center;transition:opacity .3s}.rypg-card:hover .rypg-card-overlay{opacity:1}.rypg-play-icon{width:3rem;height:3rem;color:var(--rypg-white);filter:drop-shadow(0 4px 6px rgba(0,0,0,.5))}.rypg-card-content{padding:1rem}.rypg-card-title{font-size:.875rem;font-weight:600;color:var(--rypg-white);margin:0;display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden;line-height:1.375;transition:color .2s}.rypg-card:hover .rypg-card-title{color:var(--rypg-brand-300)}.rypg-animate-fade-in{animation:rypg-fade-in .5s ease-out}.rypg-text-center{text-align:center}
2
2
  /*# sourceMappingURL=index.css.map */
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/styles.css"],"sourcesContent":["/* Base Colors (can be overridden by CSS variables) */\n:root {\n --rypg-brand-500: #8b5cf6;\n --rypg-brand-600: #7c3aed;\n --rypg-brand-300: #c4b5fd;\n --rypg-slate-800: #1e293b;\n --rypg-slate-700: #334155;\n --rypg-slate-600: #475569;\n --rypg-slate-400: #94a3b8;\n --rypg-white: #ffffff;\n --rypg-black-overlay: rgba(0, 0, 0, 0.4);\n}\n\n/* Animations */\n@keyframes rypg-spin {\n from { transform: rotate(0deg); }\n to { transform: rotate(360deg); }\n}\n\n@keyframes rypg-fade-in {\n from { opacity: 0; transform: translateY(10px); }\n to { opacity: 1; transform: translateY(0); }\n}\n\n/* Utility / Layout */\n.rypg-flex { display: flex; }\n.rypg-flex-col { flex-direction: column; }\n.rypg-items-center { align-items: center; }\n.rypg-justify-between { justify-content: space-between; }\n.rypg-gap-4 { gap: 1rem; }\n.rypg-mb-8 { margin-bottom: 2rem; }\n.rypg-mb-6 { margin-bottom: 1.5rem; }\n.rypg-mb-2 { margin-bottom: 0.5rem; }\n.rypg-pb-2 { padding-bottom: 0.5rem; }\n.rypg-w-full { width: 100%; }\n.rypg-overflow-x-auto { overflow-x: auto; }\n.rypg-space-x-2 > * + * { margin-left: 0.5rem; }\n\n@media (min-width: 640px) {\n .rypg-sm-flex-row { flex-direction: row; }\n .rypg-sm-pb-0 { padding-bottom: 0; }\n .rypg-sm-w-auto { width: auto; }\n .rypg-sm-grid-cols-2 { grid-template-columns: repeat(2, minmax(0, 1fr)); }\n}\n@media (min-width: 1024px) {\n .rypg-lg-grid-cols-3 { grid-template-columns: repeat(3, minmax(0, 1fr)); }\n}\n@media (min-width: 1280px) {\n .rypg-xl-grid-cols-4 { grid-template-columns: repeat(4, minmax(0, 1fr)); }\n}\n\n.rypg-grid { display: grid; }\n.rypg-grid-cols-1 { grid-template-columns: repeat(1, minmax(0, 1fr)); }\n.rypg-gap-6 { gap: 1.5rem; }\n\n/* Components */\n.rypg-btn-pill {\n padding: 0.5rem 1rem;\n border-radius: 9999px;\n font-size: 0.875rem;\n font-weight: 500;\n white-space: nowrap;\n transition: all 0.2s;\n border: none;\n cursor: pointer;\n}\n\n.rypg-btn-active {\n background-color: var(--rypg-brand-600);\n color: var(--rypg-white);\n box-shadow: 0 10px 15px -3px rgba(139, 92, 246, 0.3);\n}\n\n.rypg-btn-inactive {\n background-color: var(--rypg-slate-800);\n color: var(--rypg-slate-400);\n}\n.rypg-btn-inactive:hover {\n color: var(--rypg-white);\n background-color: var(--rypg-slate-700);\n}\n\n.rypg-title {\n font-size: 1.25rem;\n font-weight: 700;\n color: var(--rypg-white);\n}\n\n.rypg-desc {\n color: var(--rypg-slate-400);\n max-width: 48rem;\n margin: 0;\n}\n\n.rypg-btn-load {\n display: inline-flex;\n align-items: center;\n padding: 0.75rem 1.5rem;\n border: 1px solid transparent;\n font-size: 1rem;\n font-weight: 500;\n border-radius: 0.375rem;\n color: var(--rypg-white);\n background-color: var(--rypg-slate-700);\n cursor: pointer;\n transition: background-color 0.2s;\n}\n.rypg-btn-load:hover { background-color: var(--rypg-slate-600); }\n.rypg-btn-load:disabled { opacity: 0.5; cursor: not-allowed; }\n\n.rypg-spinner {\n animation: rypg-spin 1s linear infinite;\n margin-right: 0.75rem;\n height: 1.25rem;\n width: 1.25rem;\n}\n\n/* Video Card */\n.rypg-card {\n display: block;\n position: relative;\n overflow: hidden;\n border-radius: 0.5rem;\n background-color: var(--rypg-slate-800);\n text-decoration: none;\n transition: all 0.3s;\n transform: translateZ(0); /* Hardware accel */\n box-shadow: 0 0 0 1px rgba(255, 255, 255, 0.1);\n}\n.rypg-card:hover {\n transform: translateY(-0.25rem);\n box-shadow: 0 0 20px rgba(139, 92, 246, 0.15);\n border-color: rgba(139, 92, 246, 0.5);\n}\n\n.rypg-card-media {\n aspect-ratio: 16 / 9;\n width: 100%;\n overflow: hidden;\n position: relative;\n}\n\n.rypg-card-img {\n width: 100%;\n height: 100%;\n object-fit: cover;\n transition: transform 0.5s;\n}\n.rypg-card:hover .rypg-card-img { transform: scale(1.05); }\n\n.rypg-card-overlay {\n position: absolute;\n inset: 0;\n background-color: var(--rypg-black-overlay);\n opacity: 0;\n display: flex;\n align-items: center;\n justify-content: center;\n transition: opacity 0.3s;\n}\n.rypg-card:hover .rypg-card-overlay { opacity: 1; }\n\n.rypg-play-icon {\n width: 3rem;\n height: 3rem;\n color: var(--rypg-white);\n filter: drop-shadow(0 4px 6px rgba(0,0,0,0.5));\n}\n\n.rypg-card-content { padding: 1rem; }\n.rypg-card-title {\n font-size: 0.875rem;\n font-weight: 600;\n color: var(--rypg-white);\n margin: 0;\n display: -webkit-box;\n -webkit-line-clamp: 2;\n -webkit-box-orient: vertical;\n overflow: hidden;\n line-height: 1.375;\n transition: color 0.2s;\n}\n.rypg-card:hover .rypg-card-title { color: var(--rypg-brand-300); }\n\n.rypg-animate-fade-in { animation: rypg-fade-in 0.5s ease-out; }\n.rypg-text-center { text-align: center; }\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../src/styles.css"],"sourcesContent":["/* Base Colors (can be overridden by CSS variables) */\n:root {\n --rypg-brand-500: #8b5cf6;\n --rypg-brand-600: #7c3aed;\n --rypg-brand-300: #c4b5fd;\n --rypg-slate-800: #1e293b;\n --rypg-slate-700: #334155;\n --rypg-slate-600: #475569;\n --rypg-slate-400: #94a3b8;\n --rypg-white: #ffffff;\n --rypg-black-overlay: rgba(0, 0, 0, 0.4);\n}\n\n/* Animations */\n@keyframes rypg-spin {\n from { transform: rotate(0deg); }\n to { transform: rotate(360deg); }\n}\n\n@keyframes rypg-fade-in {\n from { opacity: 0; transform: translateY(10px); }\n to { opacity: 1; transform: translateY(0); }\n}\n\n/* Utility / Layout */\n.rypg-flex { display: flex; }\n.rypg-flex-col { flex-direction: column; }\n.rypg-items-center { align-items: center; }\n.rypg-justify-between { justify-content: space-between; }\n.rypg-gap-4 { gap: 1rem; }\n.rypg-mb-8 { margin-bottom: 2rem; }\n.rypg-mb-6 { margin-bottom: 1.5rem; }\n.rypg-mb-2 { margin-bottom: 0.5rem; }\n.rypg-pb-2 { padding-bottom: 0.5rem; }\n.rypg-w-full { width: 100%; }\n.rypg-overflow-x-auto { overflow-x: auto; }\n.rypg-space-x-2 > * + * { margin-left: 0.5rem; }\n\n@media (min-width: 640px) {\n .rypg-sm-flex-row { flex-direction: row; }\n .rypg-sm-pb-0 { padding-bottom: 0; }\n .rypg-sm-w-auto { width: auto; }\n}\n\n.rypg-grid { display: grid; }\n.rypg-gap-6 { gap: 1.5rem; }\n\n/* Components */\n.rypg-btn-pill {\n padding: 0.5rem 1rem;\n border-radius: 9999px;\n font-size: 0.875rem;\n font-weight: 500;\n white-space: nowrap;\n transition: all 0.2s;\n border: none;\n cursor: pointer;\n}\n\n.rypg-btn-active {\n background-color: var(--rypg-brand-600);\n color: var(--rypg-white);\n box-shadow: 0 10px 15px -3px rgba(139, 92, 246, 0.3);\n}\n\n.rypg-btn-inactive {\n background-color: var(--rypg-slate-800);\n color: var(--rypg-slate-400);\n}\n.rypg-btn-inactive:hover {\n color: var(--rypg-white);\n background-color: var(--rypg-slate-700);\n}\n\n.rypg-title {\n font-size: 1.25rem;\n font-weight: 700;\n color: var(--rypg-white);\n}\n\n.rypg-desc {\n color: var(--rypg-slate-400);\n max-width: 48rem;\n margin: 0;\n}\n\n.rypg-btn-load {\n display: inline-flex;\n align-items: center;\n padding: 0.75rem 1.5rem;\n border: 1px solid transparent;\n font-size: 1rem;\n font-weight: 500;\n border-radius: 0.375rem;\n color: var(--rypg-white);\n background-color: var(--rypg-slate-700);\n cursor: pointer;\n transition: background-color 0.2s;\n}\n.rypg-btn-load:hover { background-color: var(--rypg-slate-600); }\n.rypg-btn-load:disabled { opacity: 0.5; cursor: not-allowed; }\n\n.rypg-spinner {\n animation: rypg-spin 1s linear infinite;\n margin-right: 0.75rem;\n height: 1.25rem;\n width: 1.25rem;\n}\n\n/* Video Card */\n.rypg-card {\n display: block;\n position: relative;\n overflow: hidden;\n border-radius: 0.5rem;\n background-color: var(--rypg-slate-800);\n text-decoration: none;\n transition: all 0.3s;\n transform: translateZ(0); /* Hardware accel */\n box-shadow: 0 0 0 1px rgba(255, 255, 255, 0.1);\n}\n.rypg-card:hover {\n transform: translateY(-0.25rem);\n box-shadow: 0 0 20px rgba(139, 92, 246, 0.15);\n border-color: rgba(139, 92, 246, 0.5);\n}\n\n.rypg-card-media {\n aspect-ratio: 16 / 9;\n width: 100%;\n overflow: hidden;\n position: relative;\n}\n\n.rypg-card-img {\n width: 100%;\n height: 100%;\n object-fit: cover;\n transition: transform 0.5s;\n}\n.rypg-card:hover .rypg-card-img { transform: scale(1.05); }\n\n.rypg-card-overlay {\n position: absolute;\n inset: 0;\n background-color: var(--rypg-black-overlay);\n opacity: 0;\n display: flex;\n align-items: center;\n justify-content: center;\n transition: opacity 0.3s;\n}\n.rypg-card:hover .rypg-card-overlay { opacity: 1; }\n\n.rypg-play-icon {\n width: 3rem;\n height: 3rem;\n color: var(--rypg-white);\n filter: drop-shadow(0 4px 6px rgba(0,0,0,0.5));\n}\n\n.rypg-card-content { padding: 1rem; }\n.rypg-card-title {\n font-size: 0.875rem;\n font-weight: 600;\n color: var(--rypg-white);\n margin: 0;\n display: -webkit-box;\n -webkit-line-clamp: 2;\n -webkit-box-orient: vertical;\n overflow: hidden;\n line-height: 1.375;\n transition: color 0.2s;\n}\n.rypg-card:hover .rypg-card-title { color: var(--rypg-brand-300); }\n\n.rypg-animate-fade-in { animation: rypg-fade-in 0.5s ease-out; }\n.rypg-text-center { text-align: center; }\n"],"mappings":"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","names":[]}
package/dist/index.d.mts CHANGED
@@ -1,15 +1,20 @@
1
1
  import React from 'react';
2
2
 
3
+ type VideoField = 'description' | 'publishedAt';
3
4
  interface PlaylistConfig {
4
5
  id: string;
5
6
  showDescription: boolean;
6
7
  title: string;
8
+ extraVideoFields?: VideoField[];
9
+ gridColumns?: number;
7
10
  }
8
11
  interface Video {
9
12
  id: string;
10
13
  title: string;
11
14
  thumbnailUrl: string;
12
15
  videoUrl: string;
16
+ description?: string;
17
+ publishedAt?: string;
13
18
  }
14
19
  interface Playlist {
15
20
  id: string;
@@ -34,8 +39,13 @@ interface VideoCardProps {
34
39
  title: string;
35
40
  thumbnailUrl: string;
36
41
  videoUrl: string;
42
+ description?: string;
43
+ publishedAt?: string;
44
+ }
45
+ interface VideoCardPropsInternal extends VideoCardProps {
46
+ extraVideoFields?: VideoField[];
37
47
  }
38
- declare const VideoCard: React.FC<VideoCardProps>;
48
+ declare const VideoCard: React.FC<VideoCardPropsInternal>;
39
49
 
40
50
  interface FetchVideosResult {
41
51
  videos?: Video[];
@@ -45,4 +55,4 @@ interface FetchVideosResult {
45
55
  declare function fetchPlaylistVideos(playlistId: string, apiKey: string, pageToken?: string): Promise<FetchVideosResult>;
46
56
  declare function getPlaylist(playlistId: string, apiKey: string): Promise<Playlist | null>;
47
57
 
48
- export { type Playlist, type PlaylistConfig, PlaylistsExplorer, type Video, VideoCard, 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,15 +1,20 @@
1
1
  import React from 'react';
2
2
 
3
+ type VideoField = 'description' | 'publishedAt';
3
4
  interface PlaylistConfig {
4
5
  id: string;
5
6
  showDescription: boolean;
6
7
  title: string;
8
+ extraVideoFields?: VideoField[];
9
+ gridColumns?: number;
7
10
  }
8
11
  interface Video {
9
12
  id: string;
10
13
  title: string;
11
14
  thumbnailUrl: string;
12
15
  videoUrl: string;
16
+ description?: string;
17
+ publishedAt?: string;
13
18
  }
14
19
  interface Playlist {
15
20
  id: string;
@@ -34,8 +39,13 @@ interface VideoCardProps {
34
39
  title: string;
35
40
  thumbnailUrl: string;
36
41
  videoUrl: string;
42
+ description?: string;
43
+ publishedAt?: string;
44
+ }
45
+ interface VideoCardPropsInternal extends VideoCardProps {
46
+ extraVideoFields?: VideoField[];
37
47
  }
38
- declare const VideoCard: React.FC<VideoCardProps>;
48
+ declare const VideoCard: React.FC<VideoCardPropsInternal>;
39
49
 
40
50
  interface FetchVideosResult {
41
51
  videos?: Video[];
@@ -45,4 +55,4 @@ interface FetchVideosResult {
45
55
  declare function fetchPlaylistVideos(playlistId: string, apiKey: string, pageToken?: string): Promise<FetchVideosResult>;
46
56
  declare function getPlaylist(playlistId: string, apiKey: string): Promise<Playlist | null>;
47
57
 
48
- export { type Playlist, type PlaylistConfig, PlaylistsExplorer, type Video, VideoCard, 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 k=({title:i,thumbnailUrl:a,videoUrl:l})=>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.jsx("div",{className:"rypg-card-content",children:jsxRuntime.jsx("h3",{className:"rypg-card-title",children:i})})]});var P="https://www.googleapis.com/youtube/v3";async function f(i,a,l){if(!a)return {error:"API Key is missing"};try{let s=`${P}/playlistItems?part=snippet,contentDetails&playlistId=${i}&maxResults=10&key=${a}`;l&&(s+=`&pageToken=${l}`);let t=await(await fetch(s)).json();return t.items?{videos:t.items.map(n=>({id:n.snippet.resourceId.videoId,title:n.snippet.title,thumbnailUrl:n.snippet.thumbnails.medium?.url||n.snippet.thumbnails.default?.url,videoUrl:`https://www.youtube.com/watch?v=${n.snippet.resourceId.videoId}`})).filter(n=>n.title!=="Private video"&&n.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(s){return console.error(`Error fetching playlist items ${i}:`,s),{error:`Fetch Exception: ${s}`}}}async function z(i,a){if(!a)return console.warn("API Key is not set."),null;try{let s=await(await fetch(`${P}/playlists?part=snippet&id=${i}&key=${a}`)).json();if(!s.items||s.items.length===0)return console.error(`Playlist not found: ${i}`),null;let c=s.items[0].snippet,t=await f(i,a);return !t||t.error||!t.videos?null:{id:i,title:c.title,description:c.description,videos:t.videos,nextPageToken:t.nextPageToken}}catch(l){return console.error(`Error fetching playlist ${i}:`,l),null}}var C=({playlists:i,apiKey:a,onLoadMore:l})=>{let[s,c]=react.useState(i),[t,u]=react.useState(i[0]?.id||""),[n,h]=react.useState(false),[b,x]=react.useState(false),e=s.find(r=>r.id===t);react.useEffect(()=>{!e||!a||e.videos||(async()=>{x(true);try{let d=await f(e.id,a);d.videos&&c(p=>p.map(g=>g.id===e.id?{...g,videos:d.videos||[],nextPageToken:d.nextPageToken}:g));}catch(d){console.error("Failed to fetch initial videos",d);}finally{x(false);}})();},[t,a]);let N=async()=>{if(!(!e||!e.nextPageToken)){h(true);try{let r=[],d;if(l){let p=await l(e.id,e.nextPageToken);r=p.videos,d=p.nextPageToken;}else if(a){let p=await f(e.id,a,e.nextPageToken);p.videos&&(r=p.videos,d=p.nextPageToken);}else {console.error("No apiKey or onLoadMore handler provided");return}r.length>0&&c(p=>p.map(g=>g.id===e.id?{...g,videos:[...g.videos||[],...r],nextPageToken:d}:g));}catch(r){console.error("Failed to load more videos",r);}finally{h(false);}}};return e?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:s.map(r=>jsxRuntime.jsx("button",{onClick:()=>u(r.id),className:`rypg-btn-pill ${t===r.id?"rypg-btn-active":"rypg-btn-inactive"}`,children:r.config?.title||r.title},r.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:e.title}),e.config?.showDescription&&e.description&&jsxRuntime.jsx("p",{className:"rypg-desc",children:e.description})]}),b?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-grid-cols-1 rypg-sm-grid-cols-2 rypg-lg-grid-cols-3 rypg-xl-grid-cols-4 rypg-gap-6",children:e.videos?.map(r=>jsxRuntime.jsx(k,{title:r.title,thumbnailUrl:r.thumbnailUrl,videoUrl:r.videoUrl},r.id))}),!b&&e.nextPageToken&&jsxRuntime.jsx("div",{className:"mt-12 rypg-text-center",children:jsxRuntime.jsx("button",{onClick:N,disabled:n,className:"rypg-btn-load",children:n?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=C;exports.VideoCard=k;exports.default=C;exports.fetchPlaylistVideos=f;exports.getPlaylist=z;//# 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","jsxs","jsx","YOUTUBE_API_BASE","fetchPlaylistVideos","playlistId","apiKey","pageToken","url","itemsData","item","v","error","getPlaylist","playlistData","playlistSnippet","videosData","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';\n\ninterface VideoCardProps {\n title: string;\n thumbnailUrl: string;\n videoUrl: string;\n}\n\nexport const VideoCard: React.FC<VideoCardProps> = ({ title, thumbnailUrl, videoUrl }) => {\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 </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 })).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\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-grid-cols-1 rypg-sm-grid-cols-2 rypg-lg-grid-cols-3 rypg-xl-grid-cols-4 rypg-gap-6\">\n {activePlaylist.videos?.map((video) => (\n <VideoCard\n key={video.id}\n title={video.title}\n thumbnailUrl={video.thumbnailUrl}\n videoUrl={video.videoUrl}\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 k=({title:i,thumbnailUrl:a,videoUrl:l})=>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"})})})]}),jsx("div",{className:"rypg-card-content",children:jsx("h3",{className:"rypg-card-title",children:i})})]});var P="https://www.googleapis.com/youtube/v3";async function f(i,a,l){if(!a)return {error:"API Key is missing"};try{let s=`${P}/playlistItems?part=snippet,contentDetails&playlistId=${i}&maxResults=10&key=${a}`;l&&(s+=`&pageToken=${l}`);let t=await(await fetch(s)).json();return t.items?{videos:t.items.map(n=>({id:n.snippet.resourceId.videoId,title:n.snippet.title,thumbnailUrl:n.snippet.thumbnails.medium?.url||n.snippet.thumbnails.default?.url,videoUrl:`https://www.youtube.com/watch?v=${n.snippet.resourceId.videoId}`})).filter(n=>n.title!=="Private video"&&n.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(s){return console.error(`Error fetching playlist items ${i}:`,s),{error:`Fetch Exception: ${s}`}}}async function z(i,a){if(!a)return console.warn("API Key is not set."),null;try{let s=await(await fetch(`${P}/playlists?part=snippet&id=${i}&key=${a}`)).json();if(!s.items||s.items.length===0)return console.error(`Playlist not found: ${i}`),null;let c=s.items[0].snippet,t=await f(i,a);return !t||t.error||!t.videos?null:{id:i,title:c.title,description:c.description,videos:t.videos,nextPageToken:t.nextPageToken}}catch(l){return console.error(`Error fetching playlist ${i}:`,l),null}}var C=({playlists:i,apiKey:a,onLoadMore:l})=>{let[s,c]=useState(i),[t,u]=useState(i[0]?.id||""),[n,h]=useState(false),[b,x]=useState(false),e=s.find(r=>r.id===t);useEffect(()=>{!e||!a||e.videos||(async()=>{x(true);try{let d=await f(e.id,a);d.videos&&c(p=>p.map(g=>g.id===e.id?{...g,videos:d.videos||[],nextPageToken:d.nextPageToken}:g));}catch(d){console.error("Failed to fetch initial videos",d);}finally{x(false);}})();},[t,a]);let N=async()=>{if(!(!e||!e.nextPageToken)){h(true);try{let r=[],d;if(l){let p=await l(e.id,e.nextPageToken);r=p.videos,d=p.nextPageToken;}else if(a){let p=await f(e.id,a,e.nextPageToken);p.videos&&(r=p.videos,d=p.nextPageToken);}else {console.error("No apiKey or onLoadMore handler provided");return}r.length>0&&c(p=>p.map(g=>g.id===e.id?{...g,videos:[...g.videos||[],...r],nextPageToken:d}:g));}catch(r){console.error("Failed to load more videos",r);}finally{h(false);}}};return e?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:s.map(r=>jsx("button",{onClick:()=>u(r.id),className:`rypg-btn-pill ${t===r.id?"rypg-btn-active":"rypg-btn-inactive"}`,children:r.config?.title||r.title},r.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:e.title}),e.config?.showDescription&&e.description&&jsx("p",{className:"rypg-desc",children:e.description})]}),b?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-grid-cols-1 rypg-sm-grid-cols-2 rypg-lg-grid-cols-3 rypg-xl-grid-cols-4 rypg-gap-6",children:e.videos?.map(r=>jsx(k,{title:r.title,thumbnailUrl:r.thumbnailUrl,videoUrl:r.videoUrl},r.id))}),!b&&e.nextPageToken&&jsx("div",{className:"mt-12 rypg-text-center",children:jsx("button",{onClick:N,disabled:n,className:"rypg-btn-load",children:n?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{C as PlaylistsExplorer,k as VideoCard,C as default,f as fetchPlaylistVideos,z 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","jsxs","jsx","YOUTUBE_API_BASE","fetchPlaylistVideos","playlistId","apiKey","pageToken","url","itemsData","item","v","error","getPlaylist","playlistData","playlistSnippet","videosData","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';\n\ninterface VideoCardProps {\n title: string;\n thumbnailUrl: string;\n videoUrl: string;\n}\n\nexport const VideoCard: React.FC<VideoCardProps> = ({ title, thumbnailUrl, videoUrl }) => {\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 </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 })).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\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-grid-cols-1 rypg-sm-grid-cols-2 rypg-lg-grid-cols-3 rypg-xl-grid-cols-4 rypg-gap-6\">\n {activePlaylist.videos?.map((video) => (\n <VideoCard\n key={video.id}\n title={video.title}\n thumbnailUrl={video.thumbnailUrl}\n videoUrl={video.videoUrl}\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.4",
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",
@@ -3,6 +3,18 @@ import type { Playlist, Video } from '../types';
3
3
  import { VideoCard } from './VideoCard';
4
4
  import { fetchPlaylistVideos } from '../utils/youtube';
5
5
 
6
+ const getGridColumns = (n: number): string => {
7
+ const widthMap: { [key: number]: number } = {
8
+ 2: 280,
9
+ 3: 220,
10
+ 4: 180,
11
+ 5: 150,
12
+ 6: 130,
13
+ };
14
+ const minWidth = widthMap[n] || 180;
15
+ return `repeat(auto-fill, minmax(${minWidth}px, 1fr))`;
16
+ };
17
+
6
18
  interface PlaylistsExplorerProps {
7
19
  playlists: Playlist[];
8
20
  apiKey?: string;
@@ -137,13 +149,16 @@ export const PlaylistsExplorer: React.FC<PlaylistsExplorerProps> = ({ playlists:
137
149
  </svg>
138
150
  </div>
139
151
  ) : (
140
- <div className="rypg-grid rypg-grid-cols-1 rypg-sm-grid-cols-2 rypg-lg-grid-cols-3 rypg-xl-grid-cols-4 rypg-gap-6">
152
+ <div className="rypg-grid rypg-gap-6" style={{ gridTemplateColumns: getGridColumns(activePlaylist.config?.gridColumns ?? 4) }}>
141
153
  {activePlaylist.videos?.map((video) => (
142
154
  <VideoCard
143
155
  key={video.id}
144
156
  title={video.title}
145
157
  thumbnailUrl={video.thumbnailUrl}
146
158
  videoUrl={video.videoUrl}
159
+ description={video.description}
160
+ publishedAt={video.publishedAt}
161
+ extraVideoFields={activePlaylist.config?.extraVideoFields}
147
162
  />
148
163
  ))}
149
164
  </div>
@@ -1,12 +1,28 @@
1
1
  import React from 'react';
2
+ import type { VideoField } from '../types';
2
3
 
3
4
  interface VideoCardProps {
4
5
  title: string;
5
6
  thumbnailUrl: string;
6
7
  videoUrl: string;
8
+ description?: string;
9
+ publishedAt?: string;
7
10
  }
8
11
 
9
- export const VideoCard: React.FC<VideoCardProps> = ({ title, thumbnailUrl, videoUrl }) => {
12
+ interface VideoCardPropsInternal extends VideoCardProps {
13
+ extraVideoFields?: VideoField[];
14
+ }
15
+
16
+ export const VideoCard: React.FC<VideoCardPropsInternal> = ({ title, thumbnailUrl, videoUrl, description, publishedAt, extraVideoFields = [] }) => {
17
+ const formatDate = (dateString?: string) => {
18
+ if (!dateString) return '';
19
+ return new Date(dateString).toLocaleDateString('en-US', {
20
+ year: 'numeric',
21
+ month: 'short',
22
+ day: 'numeric'
23
+ });
24
+ };
25
+
10
26
  return (
11
27
  <a
12
28
  href={videoUrl}
@@ -31,6 +47,16 @@ export const VideoCard: React.FC<VideoCardProps> = ({ title, thumbnailUrl, video
31
47
  <h3 className="rypg-card-title">
32
48
  {title}
33
49
  </h3>
50
+ {extraVideoFields.includes('description') && description && (
51
+ <p className="rypg-card-description" style={{ fontSize: '0.875rem', marginTop: '0.5rem', opacity: 0.8 }}>
52
+ {description}
53
+ </p>
54
+ )}
55
+ {extraVideoFields.includes('publishedAt') && publishedAt && (
56
+ <p className="rypg-card-date" style={{ fontSize: '0.75rem', marginTop: '0.5rem', opacity: 0.6 }}>
57
+ {formatDate(publishedAt)}
58
+ </p>
59
+ )}
34
60
  </div>
35
61
  </a>
36
62
  );
package/src/styles.css CHANGED
@@ -40,17 +40,9 @@
40
40
  .rypg-sm-flex-row { flex-direction: row; }
41
41
  .rypg-sm-pb-0 { padding-bottom: 0; }
42
42
  .rypg-sm-w-auto { width: auto; }
43
- .rypg-sm-grid-cols-2 { grid-template-columns: repeat(2, minmax(0, 1fr)); }
44
- }
45
- @media (min-width: 1024px) {
46
- .rypg-lg-grid-cols-3 { grid-template-columns: repeat(3, minmax(0, 1fr)); }
47
- }
48
- @media (min-width: 1280px) {
49
- .rypg-xl-grid-cols-4 { grid-template-columns: repeat(4, minmax(0, 1fr)); }
50
43
  }
51
44
 
52
45
  .rypg-grid { display: grid; }
53
- .rypg-grid-cols-1 { grid-template-columns: repeat(1, minmax(0, 1fr)); }
54
46
  .rypg-gap-6 { gap: 1.5rem; }
55
47
 
56
48
  /* Components */
@@ -1,7 +1,11 @@
1
+ export type VideoField = 'description' | 'publishedAt';
2
+
1
3
  export interface PlaylistConfig {
2
4
  id: string;
3
5
  showDescription: boolean;
4
6
  title: string;
7
+ extraVideoFields?: VideoField[];
8
+ gridColumns?: number;
5
9
  }
6
10
 
7
11
 
@@ -11,6 +15,8 @@ export interface Video {
11
15
  title: string;
12
16
  thumbnailUrl: string;
13
17
  videoUrl: string;
18
+ description?: string;
19
+ publishedAt?: string;
14
20
  }
15
21
 
16
22
  export interface Playlist {
@@ -30,6 +30,8 @@ export async function fetchPlaylistVideos(playlistId: string, apiKey: string, pa
30
30
  title: item.snippet.title,
31
31
  thumbnailUrl: item.snippet.thumbnails.medium?.url || item.snippet.thumbnails.default?.url,
32
32
  videoUrl: `https://www.youtube.com/watch?v=${item.snippet.resourceId.videoId}`,
33
+ description: item.snippet.description,
34
+ publishedAt: item.snippet.publishedAt,
33
35
  })).filter((v: Video) => v.title !== "Private video" && v.title !== "Deleted video");
34
36
 
35
37
  return {