react-youtube-playlist-grid 1.0.1 → 1.0.3

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/README.md CHANGED
@@ -25,6 +25,11 @@ npm install react-youtube-playlist-grid
25
25
 
26
26
  If you have a public YouTube Data API Key, you can use it directly.
27
27
 
28
+ **Import the styles:**
29
+ ```tsx
30
+ import 'react-youtube-playlist-grid/dist/index.css';
31
+ ```
32
+
28
33
  ```tsx
29
34
  import { PlaylistsExplorer } from 'react-youtube-playlist-grid';
30
35
 
@@ -86,9 +91,13 @@ function App() {
86
91
  | `apiKey` | `string` | Optional. Your YouTube Data API Key for client-side fetching. |
87
92
  | `onLoadMore` | `function` | Optional. Custom async function to fetch more videos. |
88
93
 
94
+
89
95
  ## Styling
90
96
 
91
- This component uses **Tailwind CSS** utility classes internally for layout and styling. For it to look correct, your project should have Tailwind CSS configured.
97
+ The component comes with bundled CSS. You must import the CSS file for it to look correct.
98
+ ```tsx
99
+ import 'react-youtube-playlist-grid/dist/index.css';
100
+ ```
92
101
 
93
102
  ## License
94
103
 
package/dist/index.css ADDED
@@ -0,0 +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}
2
+ /*# sourceMappingURL=index.css.map */
@@ -0,0 +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":[]}
package/dist/index.d.mts CHANGED
@@ -45,4 +45,4 @@ interface FetchVideosResult {
45
45
  declare function fetchPlaylistVideos(playlistId: string, apiKey: string, pageToken?: string): Promise<FetchVideosResult>;
46
46
  declare function getPlaylist(playlistId: string, apiKey: string): Promise<Playlist | null>;
47
47
 
48
- export { type Playlist, type PlaylistConfig, PlaylistsExplorer, type Video, VideoCard, fetchPlaylistVideos, getPlaylist };
48
+ export { type Playlist, type PlaylistConfig, PlaylistsExplorer, type Video, VideoCard, PlaylistsExplorer as default, fetchPlaylistVideos, getPlaylist };
package/dist/index.d.ts CHANGED
@@ -45,4 +45,4 @@ interface FetchVideosResult {
45
45
  declare function fetchPlaylistVideos(playlistId: string, apiKey: string, pageToken?: string): Promise<FetchVideosResult>;
46
46
  declare function getPlaylist(playlistId: string, apiKey: string): Promise<Playlist | null>;
47
47
 
48
- export { type Playlist, type PlaylistConfig, PlaylistsExplorer, type Video, VideoCard, fetchPlaylistVideos, getPlaylist };
48
+ export { type Playlist, type PlaylistConfig, PlaylistsExplorer, type Video, VideoCard, PlaylistsExplorer as default, fetchPlaylistVideos, getPlaylist };
package/dist/index.js CHANGED
@@ -1,3 +1,3 @@
1
- 'use strict';var react=require('react'),jsxRuntime=require('react/jsx-runtime');var y=({title:t,thumbnailUrl:n,videoUrl:l})=>jsxRuntime.jsxs("a",{href:l,target:"_blank",rel:"noopener noreferrer",className:"group block relative overflow-hidden rounded-lg bg-slate-800 ring-1 ring-white/10 hover:ring-brand-500/50 transition-all duration-300 hover:shadow-[0_0_20px_rgba(139,92,246,0.15)] transform hover:-translate-y-1",children:[jsxRuntime.jsxs("div",{className:"aspect-video w-full overflow-hidden relative",children:[jsxRuntime.jsx("img",{src:n,alt:t,className:"w-full h-full object-cover transition-transform duration-500 group-hover:scale-105",loading:"lazy"}),jsxRuntime.jsx("div",{className:"absolute inset-0 bg-black/40 opacity-0 group-hover:opacity-100 transition-opacity duration-300 flex items-center justify-center",children:jsxRuntime.jsx("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 24 24",fill:"currentColor",className:"w-12 h-12 text-white drop-shadow-lg",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:"p-4",children:jsxRuntime.jsx("h3",{className:"text-sm font-semibold text-white group-hover:text-brand-300 transition-colors line-clamp-2 leading-snug",children:t})})]});var b="https://www.googleapis.com/youtube/v3";async function f(t,n,l){if(!n)return {error:"API Key is missing"};try{let r=`${b}/playlistItems?part=snippet,contentDetails&playlistId=${t}&maxResults=10&key=${n}`;l&&(r+=`&pageToken=${l}`);let o=await(await fetch(r)).json();return o.items?{videos:o.items.map(a=>({id:a.snippet.resourceId.videoId,title:a.snippet.title,thumbnailUrl:a.snippet.thumbnails.medium?.url||a.snippet.thumbnails.default?.url,videoUrl:`https://www.youtube.com/watch?v=${a.snippet.resourceId.videoId}`})).filter(a=>a.title!=="Private video"&&a.title!=="Deleted video"),nextPageToken:o.nextPageToken}:(console.error(`Failed to fetch items for playlist: ${t}`,o),{error:`YouTube API Error: ${o.error?.message||JSON.stringify(o)}`})}catch(r){return console.error(`Error fetching playlist items ${t}:`,r),{error:`Fetch Exception: ${r}`}}}async function T(t,n){if(!n)return console.warn("API Key is not set."),null;try{let r=await(await fetch(`${b}/playlists?part=snippet&id=${t}&key=${n}`)).json();if(!r.items||r.items.length===0)return console.error(`Playlist not found: ${t}`),null;let p=r.items[0].snippet,o=await f(t,n);return !o||o.error||!o.videos?null:{id:t,title:p.title,description:p.description,videos:o.videos,nextPageToken:o.nextPageToken}}catch(l){return console.error(`Error fetching playlist ${t}:`,l),null}}var A=({initialPlaylists:t,apiKey:n,onLoadMore:l})=>{let[r,p]=react.useState(t[0]?.id||""),[o,h]=react.useState(t),[a,x]=react.useState(false),i=o.find(e=>e.id===r),P=async()=>{if(!(!i||!i.nextPageToken)){x(true);try{let e=[],u;if(l){let d=await l(i.id,i.nextPageToken);e=d.videos,u=d.nextPageToken;}else if(n){let d=await f(i.id,n,i.nextPageToken);d.videos&&(e=d.videos,u=d.nextPageToken);}else {console.error("No apiKey or onLoadMore handler provided");return}e.length>0&&h(d=>d.map(g=>g.id===i.id?{...g,videos:[...g.videos,...e],nextPageToken:u}:g));}catch(e){console.error("Failed to load more videos",e);}finally{x(false);}}};return i?jsxRuntime.jsxs("div",{children:[jsxRuntime.jsx("div",{className:"flex flex-col sm:flex-row justify-between items-center mb-8 gap-4",children:jsxRuntime.jsx("div",{className:"flex space-x-2 overflow-x-auto pb-2 sm:pb-0 w-full sm:w-auto",children:o.map(e=>jsxRuntime.jsx("button",{onClick:()=>p(e.id),className:`px-4 py-2 rounded-full text-sm font-medium whitespace-nowrap transition-colors ${r===e.id?"bg-brand-600 text-white shadow-lg shadow-brand-500/30":"bg-slate-800 text-slate-400 hover:text-white hover:bg-slate-700"}`,children:e.config?.title||e.title},e.id))})}),jsxRuntime.jsxs("div",{className:"animate-fade-in",children:[jsxRuntime.jsxs("div",{className:"mb-6",children:[jsxRuntime.jsx("h2",{className:"text-xl font-bold text-white mb-2",children:i.title}),i.config?.showDescription&&jsxRuntime.jsx("p",{className:"text-slate-400 max-w-3xl",children:i.description})]}),jsxRuntime.jsx("div",{className:"grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-6",children:i.videos.map(e=>jsxRuntime.jsx(y,{title:e.title,thumbnailUrl:e.thumbnailUrl,videoUrl:e.videoUrl},e.id))}),i.nextPageToken&&jsxRuntime.jsx("div",{className:"mt-12 text-center",children:jsxRuntime.jsx("button",{onClick:P,disabled:a,className:"inline-flex items-center px-6 py-3 border border-transparent text-base font-medium rounded-md shadow-sm text-white bg-slate-700 hover:bg-slate-600 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-brand-500 disabled:opacity-50 disabled:cursor-not-allowed transition-colors",children:a?jsxRuntime.jsxs(jsxRuntime.Fragment,{children:[jsxRuntime.jsxs("svg",{className:"animate-spin -ml-1 mr-3 h-5 w-5 text-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"})]}),"Loading..."]}):"Load More Videos"})})]})]}):jsxRuntime.jsx("div",{children:"No playlists available."})};
2
- exports.PlaylistsExplorer=A;exports.VideoCard=y;exports.fetchPlaylistVideos=f;exports.getPlaylist=T;//# sourceMappingURL=index.js.map
1
+ 'use strict';Object.defineProperty(exports,'__esModule',{value:true});var react=require('react'),jsxRuntime=require('react/jsx-runtime');var x=({title:e,thumbnailUrl:n,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:n,alt:e,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:e})})]});var w="https://www.googleapis.com/youtube/v3";async function f(e,n,l){if(!n)return {error:"API Key is missing"};try{let i=`${w}/playlistItems?part=snippet,contentDetails&playlistId=${e}&maxResults=10&key=${n}`;l&&(i+=`&pageToken=${l}`);let t=await(await fetch(i)).json();return t.items?{videos:t.items.map(s=>({id:s.snippet.resourceId.videoId,title:s.snippet.title,thumbnailUrl:s.snippet.thumbnails.medium?.url||s.snippet.thumbnails.default?.url,videoUrl:`https://www.youtube.com/watch?v=${s.snippet.resourceId.videoId}`})).filter(s=>s.title!=="Private video"&&s.title!=="Deleted video"),nextPageToken:t.nextPageToken}:(console.error(`Failed to fetch items for playlist: ${e}`,t),{error:`YouTube API Error: ${t.error?.message||JSON.stringify(t)}`})}catch(i){return console.error(`Error fetching playlist items ${e}:`,i),{error:`Fetch Exception: ${i}`}}}async function R(e,n){if(!n)return console.warn("API Key is not set."),null;try{let i=await(await fetch(`${w}/playlists?part=snippet&id=${e}&key=${n}`)).json();if(!i.items||i.items.length===0)return console.error(`Playlist not found: ${e}`),null;let d=i.items[0].snippet,t=await f(e,n);return !t||t.error||!t.videos?null:{id:e,title:d.title,description:d.description,videos:t.videos,nextPageToken:t.nextPageToken}}catch(l){return console.error(`Error fetching playlist ${e}:`,l),null}}var k=({initialPlaylists:e,apiKey:n,onLoadMore:l})=>{let[i,d]=react.useState(e[0]?.id||""),[t,u]=react.useState(e),[s,b]=react.useState(false),o=t.find(r=>r.id===i),P=async()=>{if(!(!o||!o.nextPageToken)){b(true);try{let r=[],m;if(l){let p=await l(o.id,o.nextPageToken);r=p.videos,m=p.nextPageToken;}else if(n){let p=await f(o.id,n,o.nextPageToken);p.videos&&(r=p.videos,m=p.nextPageToken);}else {console.error("No apiKey or onLoadMore handler provided");return}r.length>0&&u(p=>p.map(y=>y.id===o.id?{...y,videos:[...y.videos,...r],nextPageToken:m}:y));}catch(r){console.error("Failed to load more videos",r);}finally{b(false);}}};return o?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:t.map(r=>jsxRuntime.jsx("button",{onClick:()=>d(r.id),className:`rypg-btn-pill ${i===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:o.title}),o.config?.showDescription&&jsxRuntime.jsx("p",{className:"rypg-desc",children:o.description})]}),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:o.videos.map(r=>jsxRuntime.jsx(x,{title:r.title,thumbnailUrl:r.thumbnailUrl,videoUrl:r.videoUrl},r.id))}),o.nextPageToken&&jsxRuntime.jsx("div",{className:"mt-12 rypg-text-center",children:jsxRuntime.jsx("button",{onClick:P,disabled:s,className:"rypg-btn-load",children:s?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=k;exports.VideoCard=x;exports.default=k;exports.fetchPlaylistVideos=f;exports.getPlaylist=R;//# 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","activePlaylistId","setActivePlaylistId","useState","playlists","setPlaylists","loadingMore","setLoadingMore","activePlaylist","p","handleLoadMore","newVideos","newNextPageToken","result","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=\"group block relative overflow-hidden rounded-lg bg-slate-800 ring-1 ring-white/10 hover:ring-brand-500/50 transition-all duration-300 hover:shadow-[0_0_20px_rgba(139,92,246,0.15)] transform hover:-translate-y-1\"\n >\n <div className=\"aspect-video w-full overflow-hidden relative\">\n <img\n src={thumbnailUrl}\n alt={title}\n className=\"w-full h-full object-cover transition-transform duration-500 group-hover:scale-105\"\n loading=\"lazy\"\n />\n <div className=\"absolute inset-0 bg-black/40 opacity-0 group-hover:opacity-100 transition-opacity duration-300 flex items-center justify-center\">\n <svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 24 24\" fill=\"currentColor\" className=\"w-12 h-12 text-white drop-shadow-lg\">\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=\"p-4\">\n <h3 className=\"text-sm font-semibold text-white group-hover:text-brand-300 transition-colors line-clamp-2 leading-snug\">\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 } from 'react';\nimport type { Playlist, Video } from '../types';\nimport { VideoCard } from './VideoCard';\nimport { fetchPlaylistVideos } from '../utils/youtube';\n\ninterface PlaylistsExplorerProps {\n initialPlaylists: Playlist[];\n apiKey?: string;\n onLoadMore?: (playlistId: string, pageToken: string) => Promise<{ videos: Video[], nextPageToken?: string }>;\n}\n\nexport const PlaylistsExplorer: React.FC<PlaylistsExplorerProps> = ({ initialPlaylists, apiKey, onLoadMore }) => {\n const [activePlaylistId, setActivePlaylistId] = useState<string>(initialPlaylists[0]?.id || \"\");\n const [playlists, setPlaylists] = useState<Playlist[]>(initialPlaylists);\n const [loadingMore, setLoadingMore] = useState<boolean>(false);\n\n const activePlaylist = playlists.find(p => p.id === activePlaylistId);\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=\"flex flex-col sm:flex-row justify-between items-center mb-8 gap-4\">\n <div className=\"flex space-x-2 overflow-x-auto pb-2 sm:pb-0 w-full sm:w-auto\">\n {playlists.map(playlist => (\n <button\n key={playlist.id}\n onClick={() => setActivePlaylistId(playlist.id)}\n className={`px-4 py-2 rounded-full text-sm font-medium whitespace-nowrap transition-colors ${activePlaylistId === playlist.id\n ? 'bg-brand-600 text-white shadow-lg shadow-brand-500/30'\n : 'bg-slate-800 text-slate-400 hover:text-white hover:bg-slate-700'\n }`}\n >\n {playlist.config?.title || playlist.title}\n </button>\n ))}\n </div>\n </div>\n\n {/* Playlist Content */}\n <div className=\"animate-fade-in\">\n {/* Always show title and description if enabled */}\n <div className=\"mb-6\">\n <h2 className=\"text-xl font-bold text-white mb-2\">{activePlaylist.title}</h2>\n {activePlaylist.config?.showDescription && (\n <p className=\"text-slate-400 max-w-3xl\">{activePlaylist.description}</p>\n )}\n </div>\n\n <div className=\"grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 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 {/* Load More */}\n {activePlaylist.nextPageToken && (\n <div className=\"mt-12 text-center\">\n <button\n onClick={handleLoadMore}\n disabled={loadingMore}\n className=\"inline-flex items-center px-6 py-3 border border-transparent text-base font-medium rounded-md shadow-sm text-white bg-slate-700 hover:bg-slate-600 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-brand-500 disabled:opacity-50 disabled:cursor-not-allowed transition-colors\"\n >\n {loadingMore ? (\n <>\n <svg className=\"animate-spin -ml-1 mr-3 h-5 w-5 text-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 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","jsxs","jsx","YOUTUBE_API_BASE","fetchPlaylistVideos","playlistId","apiKey","pageToken","url","itemsData","item","v","error","getPlaylist","playlistData","playlistSnippet","videosData","PlaylistsExplorer","initialPlaylists","onLoadMore","activePlaylistId","setActivePlaylistId","useState","playlists","setPlaylists","loadingMore","setLoadingMore","activePlaylist","p","handleLoadMore","newVideos","newNextPageToken","result","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 } from 'react';\nimport type { Playlist, Video } from '../types';\nimport { VideoCard } from './VideoCard';\nimport { fetchPlaylistVideos } from '../utils/youtube';\n\ninterface PlaylistsExplorerProps {\n initialPlaylists: Playlist[];\n apiKey?: string;\n onLoadMore?: (playlistId: string, pageToken: string) => Promise<{ videos: Video[], nextPageToken?: string }>;\n}\n\nexport const PlaylistsExplorer: React.FC<PlaylistsExplorerProps> = ({ initialPlaylists, apiKey, onLoadMore }) => {\n const [activePlaylistId, setActivePlaylistId] = useState<string>(initialPlaylists[0]?.id || \"\");\n const [playlists, setPlaylists] = useState<Playlist[]>(initialPlaylists);\n const [loadingMore, setLoadingMore] = useState<boolean>(false);\n\n const activePlaylist = playlists.find(p => p.id === activePlaylistId);\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 && (\n <p className=\"rypg-desc\">{activePlaylist.description}</p>\n )}\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 {/* Load More */}\n {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}from'react';import {jsxs,jsx,Fragment}from'react/jsx-runtime';var y=({title:t,thumbnailUrl:n,videoUrl:l})=>jsxs("a",{href:l,target:"_blank",rel:"noopener noreferrer",className:"group block relative overflow-hidden rounded-lg bg-slate-800 ring-1 ring-white/10 hover:ring-brand-500/50 transition-all duration-300 hover:shadow-[0_0_20px_rgba(139,92,246,0.15)] transform hover:-translate-y-1",children:[jsxs("div",{className:"aspect-video w-full overflow-hidden relative",children:[jsx("img",{src:n,alt:t,className:"w-full h-full object-cover transition-transform duration-500 group-hover:scale-105",loading:"lazy"}),jsx("div",{className:"absolute inset-0 bg-black/40 opacity-0 group-hover:opacity-100 transition-opacity duration-300 flex items-center justify-center",children:jsx("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 24 24",fill:"currentColor",className:"w-12 h-12 text-white drop-shadow-lg",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:"p-4",children:jsx("h3",{className:"text-sm font-semibold text-white group-hover:text-brand-300 transition-colors line-clamp-2 leading-snug",children:t})})]});var b="https://www.googleapis.com/youtube/v3";async function f(t,n,l){if(!n)return {error:"API Key is missing"};try{let r=`${b}/playlistItems?part=snippet,contentDetails&playlistId=${t}&maxResults=10&key=${n}`;l&&(r+=`&pageToken=${l}`);let o=await(await fetch(r)).json();return o.items?{videos:o.items.map(a=>({id:a.snippet.resourceId.videoId,title:a.snippet.title,thumbnailUrl:a.snippet.thumbnails.medium?.url||a.snippet.thumbnails.default?.url,videoUrl:`https://www.youtube.com/watch?v=${a.snippet.resourceId.videoId}`})).filter(a=>a.title!=="Private video"&&a.title!=="Deleted video"),nextPageToken:o.nextPageToken}:(console.error(`Failed to fetch items for playlist: ${t}`,o),{error:`YouTube API Error: ${o.error?.message||JSON.stringify(o)}`})}catch(r){return console.error(`Error fetching playlist items ${t}:`,r),{error:`Fetch Exception: ${r}`}}}async function T(t,n){if(!n)return console.warn("API Key is not set."),null;try{let r=await(await fetch(`${b}/playlists?part=snippet&id=${t}&key=${n}`)).json();if(!r.items||r.items.length===0)return console.error(`Playlist not found: ${t}`),null;let p=r.items[0].snippet,o=await f(t,n);return !o||o.error||!o.videos?null:{id:t,title:p.title,description:p.description,videos:o.videos,nextPageToken:o.nextPageToken}}catch(l){return console.error(`Error fetching playlist ${t}:`,l),null}}var A=({initialPlaylists:t,apiKey:n,onLoadMore:l})=>{let[r,p]=useState(t[0]?.id||""),[o,h]=useState(t),[a,x]=useState(false),i=o.find(e=>e.id===r),P=async()=>{if(!(!i||!i.nextPageToken)){x(true);try{let e=[],u;if(l){let d=await l(i.id,i.nextPageToken);e=d.videos,u=d.nextPageToken;}else if(n){let d=await f(i.id,n,i.nextPageToken);d.videos&&(e=d.videos,u=d.nextPageToken);}else {console.error("No apiKey or onLoadMore handler provided");return}e.length>0&&h(d=>d.map(g=>g.id===i.id?{...g,videos:[...g.videos,...e],nextPageToken:u}:g));}catch(e){console.error("Failed to load more videos",e);}finally{x(false);}}};return i?jsxs("div",{children:[jsx("div",{className:"flex flex-col sm:flex-row justify-between items-center mb-8 gap-4",children:jsx("div",{className:"flex space-x-2 overflow-x-auto pb-2 sm:pb-0 w-full sm:w-auto",children:o.map(e=>jsx("button",{onClick:()=>p(e.id),className:`px-4 py-2 rounded-full text-sm font-medium whitespace-nowrap transition-colors ${r===e.id?"bg-brand-600 text-white shadow-lg shadow-brand-500/30":"bg-slate-800 text-slate-400 hover:text-white hover:bg-slate-700"}`,children:e.config?.title||e.title},e.id))})}),jsxs("div",{className:"animate-fade-in",children:[jsxs("div",{className:"mb-6",children:[jsx("h2",{className:"text-xl font-bold text-white mb-2",children:i.title}),i.config?.showDescription&&jsx("p",{className:"text-slate-400 max-w-3xl",children:i.description})]}),jsx("div",{className:"grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-6",children:i.videos.map(e=>jsx(y,{title:e.title,thumbnailUrl:e.thumbnailUrl,videoUrl:e.videoUrl},e.id))}),i.nextPageToken&&jsx("div",{className:"mt-12 text-center",children:jsx("button",{onClick:P,disabled:a,className:"inline-flex items-center px-6 py-3 border border-transparent text-base font-medium rounded-md shadow-sm text-white bg-slate-700 hover:bg-slate-600 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-brand-500 disabled:opacity-50 disabled:cursor-not-allowed transition-colors",children:a?jsxs(Fragment,{children:[jsxs("svg",{className:"animate-spin -ml-1 mr-3 h-5 w-5 text-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"})]}),"Loading..."]}):"Load More Videos"})})]})]}):jsx("div",{children:"No playlists available."})};
2
- export{A as PlaylistsExplorer,y as VideoCard,f as fetchPlaylistVideos,T as getPlaylist};//# sourceMappingURL=index.mjs.map
1
+ import {useState}from'react';import {jsxs,jsx,Fragment}from'react/jsx-runtime';var x=({title:e,thumbnailUrl:n,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:n,alt:e,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:e})})]});var w="https://www.googleapis.com/youtube/v3";async function f(e,n,l){if(!n)return {error:"API Key is missing"};try{let i=`${w}/playlistItems?part=snippet,contentDetails&playlistId=${e}&maxResults=10&key=${n}`;l&&(i+=`&pageToken=${l}`);let t=await(await fetch(i)).json();return t.items?{videos:t.items.map(s=>({id:s.snippet.resourceId.videoId,title:s.snippet.title,thumbnailUrl:s.snippet.thumbnails.medium?.url||s.snippet.thumbnails.default?.url,videoUrl:`https://www.youtube.com/watch?v=${s.snippet.resourceId.videoId}`})).filter(s=>s.title!=="Private video"&&s.title!=="Deleted video"),nextPageToken:t.nextPageToken}:(console.error(`Failed to fetch items for playlist: ${e}`,t),{error:`YouTube API Error: ${t.error?.message||JSON.stringify(t)}`})}catch(i){return console.error(`Error fetching playlist items ${e}:`,i),{error:`Fetch Exception: ${i}`}}}async function R(e,n){if(!n)return console.warn("API Key is not set."),null;try{let i=await(await fetch(`${w}/playlists?part=snippet&id=${e}&key=${n}`)).json();if(!i.items||i.items.length===0)return console.error(`Playlist not found: ${e}`),null;let d=i.items[0].snippet,t=await f(e,n);return !t||t.error||!t.videos?null:{id:e,title:d.title,description:d.description,videos:t.videos,nextPageToken:t.nextPageToken}}catch(l){return console.error(`Error fetching playlist ${e}:`,l),null}}var k=({initialPlaylists:e,apiKey:n,onLoadMore:l})=>{let[i,d]=useState(e[0]?.id||""),[t,u]=useState(e),[s,b]=useState(false),o=t.find(r=>r.id===i),P=async()=>{if(!(!o||!o.nextPageToken)){b(true);try{let r=[],m;if(l){let p=await l(o.id,o.nextPageToken);r=p.videos,m=p.nextPageToken;}else if(n){let p=await f(o.id,n,o.nextPageToken);p.videos&&(r=p.videos,m=p.nextPageToken);}else {console.error("No apiKey or onLoadMore handler provided");return}r.length>0&&u(p=>p.map(y=>y.id===o.id?{...y,videos:[...y.videos,...r],nextPageToken:m}:y));}catch(r){console.error("Failed to load more videos",r);}finally{b(false);}}};return o?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:t.map(r=>jsx("button",{onClick:()=>d(r.id),className:`rypg-btn-pill ${i===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:o.title}),o.config?.showDescription&&jsx("p",{className:"rypg-desc",children:o.description})]}),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:o.videos.map(r=>jsx(x,{title:r.title,thumbnailUrl:r.thumbnailUrl,videoUrl:r.videoUrl},r.id))}),o.nextPageToken&&jsx("div",{className:"mt-12 rypg-text-center",children:jsx("button",{onClick:P,disabled:s,className:"rypg-btn-load",children:s?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{k as PlaylistsExplorer,x as VideoCard,k as default,f as fetchPlaylistVideos,R 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","activePlaylistId","setActivePlaylistId","useState","playlists","setPlaylists","loadingMore","setLoadingMore","activePlaylist","p","handleLoadMore","newVideos","newNextPageToken","result","prevPlaylists","playlist","video","Fragment"],"mappings":"+EAQO,IAAMA,EAAsC,CAAC,CAAE,KAAA,CAAAC,CAAAA,CAAO,aAAAC,CAAAA,CAAc,QAAA,CAAAC,CAAS,CAAA,GAE5EC,KAAC,GAAA,CAAA,CACG,IAAA,CAAMD,CAAAA,CACN,MAAA,CAAO,SACP,GAAA,CAAI,qBAAA,CACJ,SAAA,CAAU,oNAAA,CAEV,UAAAC,IAAAA,CAAC,KAAA,CAAA,CAAI,SAAA,CAAU,8CAAA,CACX,UAAAC,GAAAA,CAAC,KAAA,CAAA,CACG,GAAA,CAAKH,CAAAA,CACL,IAAKD,CAAAA,CACL,SAAA,CAAU,oFAAA,CACV,OAAA,CAAQ,OACZ,CAAA,CACAI,GAAAA,CAAC,KAAA,CAAA,CAAI,SAAA,CAAU,kIACX,QAAA,CAAAA,GAAAA,CAAC,KAAA,CAAA,CAAI,KAAA,CAAM,6BAA6B,OAAA,CAAQ,WAAA,CAAY,IAAA,CAAK,cAAA,CAAe,UAAU,qCAAA,CACtF,QAAA,CAAAA,GAAAA,CAAC,MAAA,CAAA,CAAK,SAAS,SAAA,CAAU,CAAA,CAAE,yIAAA,CAA0I,QAAA,CAAS,UAAU,CAAA,CAC5L,CAAA,CACJ,GACJ,CAAA,CACAA,GAAAA,CAAC,OAAI,SAAA,CAAU,KAAA,CACX,QAAA,CAAAA,GAAAA,CAAC,MAAG,SAAA,CAAU,yGAAA,CACT,QAAA,CAAAJ,CAAAA,CACL,EACJ,CAAA,CAAA,CACJ,EChCR,IAAMK,CAAAA,CAAmB,wCAQzB,eAAsBC,CAAAA,CAAoBC,CAAAA,CAAoBC,CAAAA,CAAgBC,EAAgD,CAC1H,GAAI,CAACD,CAAAA,CAAQ,OAAO,CAAE,KAAA,CAAO,oBAAqB,CAAA,CAElD,GAAI,CACA,IAAIE,CAAAA,CAAM,CAAA,EAAGL,CAAgB,CAAA,sDAAA,EAAyDE,CAAU,sBAAsBC,CAAM,CAAA,CAAA,CACxHC,IACAC,CAAAA,EAAO,CAAA,WAAA,EAAcD,CAAS,CAAA,CAAA,CAAA,CAIlC,IAAME,CAAAA,CAAY,KAAA,CADI,MAAM,KAAA,CAAMD,CAAG,CAAA,EACC,IAAA,EAAK,CAE3C,OAAKC,EAAU,KAAA,CAYR,CACH,MAAA,CARoBA,CAAAA,CAAU,MAAM,GAAA,CAAKC,CAAAA,GAAe,CACxD,EAAA,CAAIA,EAAK,OAAA,CAAQ,UAAA,CAAW,OAAA,CAC5B,KAAA,CAAOA,EAAK,OAAA,CAAQ,KAAA,CACpB,YAAA,CAAcA,CAAAA,CAAK,QAAQ,UAAA,CAAW,MAAA,EAAQ,KAAOA,CAAAA,CAAK,OAAA,CAAQ,WAAW,OAAA,EAAS,GAAA,CACtF,QAAA,CAAU,CAAA,gCAAA,EAAmCA,EAAK,OAAA,CAAQ,UAAA,CAAW,OAAO,CAAA,CAChF,EAAE,CAAA,CAAE,MAAA,CAAQC,CAAAA,EAAaA,CAAAA,CAAE,QAAU,eAAA,EAAmBA,CAAAA,CAAE,KAAA,GAAU,eAAe,EAI/E,aAAA,CAAeF,CAAAA,CAAU,aAC7B,CAAA,EAdI,QAAQ,KAAA,CAAM,CAAA,oCAAA,EAAuCJ,CAAU,CAAA,CAAA,CAAII,CAAS,CAAA,CACrE,CAAE,KAAA,CAAO,CAAA,mBAAA,EAAsBA,EAAU,KAAA,EAAO,OAAA,EAAW,KAAK,SAAA,CAAUA,CAAS,CAAC,CAAA,CAAG,CAAA,CActG,CAAA,MAASG,CAAAA,CAAO,CACZ,OAAA,OAAA,CAAQ,KAAA,CAAM,CAAA,8BAAA,EAAiCP,CAAU,IAAKO,CAAK,CAAA,CAC5D,CAAE,KAAA,CAAO,oBAAoBA,CAAK,CAAA,CAAG,CAChD,CACJ,CAEA,eAAsBC,CAAAA,CAAYR,CAAAA,CAAoBC,CAAAA,CAA0C,CAC5F,GAAI,CAACA,CAAAA,CACD,OAAA,OAAA,CAAQ,KAAK,qBAAqB,CAAA,CAC3B,IAAA,CAGX,GAAI,CAKA,IAAMQ,CAAAA,CAAe,MAHI,MAAM,KAAA,CAC3B,GAAGX,CAAgB,CAAA,2BAAA,EAA8BE,CAAU,CAAA,KAAA,EAAQC,CAAM,CAAA,CAC7E,CAAA,EAC4C,IAAA,EAAK,CAEjD,GAAI,CAACQ,CAAAA,CAAa,KAAA,EAASA,CAAAA,CAAa,MAAM,MAAA,GAAW,CAAA,CACrD,OAAA,OAAA,CAAQ,KAAA,CAAM,uBAAuBT,CAAU,CAAA,CAAE,CAAA,CAC1C,IAAA,CAGX,IAAMU,CAAAA,CAAkBD,CAAAA,CAAa,KAAA,CAAM,CAAC,EAAE,OAAA,CAGxCE,CAAAA,CAAa,MAAMZ,CAAAA,CAAoBC,EAAYC,CAAM,CAAA,CAE/D,OAAI,CAACU,CAAAA,EAAcA,EAAW,KAAA,EAAS,CAACA,CAAAA,CAAW,MAAA,CAAe,KAE3D,CACH,EAAA,CAAIX,CAAAA,CACJ,KAAA,CAAOU,EAAgB,KAAA,CACvB,WAAA,CAAaA,CAAAA,CAAgB,WAAA,CAC7B,OAAQC,CAAAA,CAAW,MAAA,CACnB,aAAA,CAAeA,CAAAA,CAAW,aAC9B,CAEJ,CAAA,MAASJ,CAAAA,CAAO,CACZ,eAAQ,KAAA,CAAM,CAAA,wBAAA,EAA2BP,CAAU,CAAA,CAAA,CAAA,CAAKO,CAAK,CAAA,CACtD,IACX,CACJ,CCtEO,IAAMK,CAAAA,CAAsD,CAAC,CAAE,gBAAA,CAAAC,CAAAA,CAAkB,MAAA,CAAAZ,CAAAA,CAAQ,WAAAa,CAAW,CAAA,GAAM,CAC7G,GAAM,CAACC,CAAAA,CAAkBC,CAAmB,CAAA,CAAIC,QAAAA,CAAiBJ,EAAiB,CAAC,CAAA,EAAG,EAAA,EAAM,EAAE,EACxF,CAACK,CAAAA,CAAWC,CAAY,CAAA,CAAIF,SAAqBJ,CAAgB,CAAA,CACjE,CAACO,CAAAA,CAAaC,CAAc,CAAA,CAAIJ,QAAAA,CAAkB,KAAK,CAAA,CAEvDK,EAAiBJ,CAAAA,CAAU,IAAA,CAAKK,CAAAA,EAAKA,CAAAA,CAAE,KAAOR,CAAgB,CAAA,CAE9DS,CAAAA,CAAiB,SAAY,CAC/B,GAAI,EAAA,CAACF,CAAAA,EAAkB,CAACA,EAAe,aAAA,CAAA,CAEvC,CAAAD,CAAAA,CAAe,IAAI,EACnB,GAAI,CACA,IAAII,CAAAA,CAAqB,EAAC,CACtBC,CAAAA,CAEJ,GAAIZ,CAAAA,CAAY,CACZ,IAAMa,CAAAA,CAAS,MAAMb,CAAAA,CAAWQ,EAAe,EAAA,CAAIA,CAAAA,CAAe,aAAa,CAAA,CAC/EG,CAAAA,CAAYE,EAAO,MAAA,CACnBD,CAAAA,CAAmBC,CAAAA,CAAO,cAC9B,SAAW1B,CAAAA,CAAQ,CACf,IAAM0B,CAAAA,CAAS,MAAM5B,CAAAA,CAAoBuB,CAAAA,CAAe,EAAA,CAAIrB,CAAAA,CAAQqB,EAAe,aAAa,CAAA,CAC5FK,CAAAA,CAAO,MAAA,GACPF,EAAYE,CAAAA,CAAO,MAAA,CACnBD,CAAAA,CAAmBC,CAAAA,CAAO,eAElC,CAAA,KAAO,CACH,OAAA,CAAQ,KAAA,CAAM,0CAA0C,CAAA,CACxD,MACJ,CAEIF,CAAAA,CAAU,OAAS,CAAA,EACnBN,CAAAA,CAAaS,GAAiBA,CAAAA,CAAc,GAAA,CAAIL,GACxCA,CAAAA,CAAE,EAAA,GAAOD,CAAAA,CAAe,EAAA,CACjB,CACH,GAAGC,CAAAA,CACH,MAAA,CAAQ,CAAC,GAAGA,CAAAA,CAAE,MAAA,CAAQ,GAAGE,CAAS,EAClC,aAAA,CAAeC,CACnB,CAAA,CAEGH,CACV,CAAC,EAEV,CAAA,MAAShB,CAAAA,CAAO,CACZ,QAAQ,KAAA,CAAM,4BAAA,CAA8BA,CAAK,EACrD,QAAE,CACEc,CAAAA,CAAe,KAAK,EACxB,EACJ,CAAA,CAEA,OAAKC,EAGD1B,IAAAA,CAAC,KAAA,CAAA,CAEG,UAAAC,GAAAA,CAAC,KAAA,CAAA,CAAI,SAAA,CAAU,mEAAA,CACX,SAAAA,GAAAA,CAAC,KAAA,CAAA,CAAI,SAAA,CAAU,8DAAA,CACV,SAAAqB,CAAAA,CAAU,GAAA,CAAIW,CAAAA,EACXhC,GAAAA,CAAC,UAEG,OAAA,CAAS,IAAMmB,CAAAA,CAAoBa,CAAAA,CAAS,EAAE,CAAA,CAC9C,SAAA,CAAW,CAAA,+EAAA,EAAkFd,CAAAA,GAAqBc,EAAS,EAAA,CACrH,uDAAA,CACA,iEACF,CAAA,CAAA,CAEH,SAAAA,CAAAA,CAAS,MAAA,EAAQ,KAAA,EAASA,CAAAA,CAAS,OAP/BA,CAAAA,CAAS,EAQlB,CACH,CAAA,CACL,CAAA,CACJ,EAGAjC,IAAAA,CAAC,KAAA,CAAA,CAAI,SAAA,CAAU,iBAAA,CAEX,UAAAA,IAAAA,CAAC,KAAA,CAAA,CAAI,SAAA,CAAU,MAAA,CACX,UAAAC,GAAAA,CAAC,IAAA,CAAA,CAAG,SAAA,CAAU,mCAAA,CAAqC,SAAAyB,CAAAA,CAAe,KAAA,CAAM,CAAA,CACvEA,CAAAA,CAAe,QAAQ,eAAA,EACpBzB,GAAAA,CAAC,GAAA,CAAA,CAAE,SAAA,CAAU,2BAA4B,QAAA,CAAAyB,CAAAA,CAAe,WAAA,CAAY,CAAA,CAAA,CAE5E,EAEAzB,GAAAA,CAAC,KAAA,CAAA,CAAI,SAAA,CAAU,qEAAA,CACV,SAAAyB,CAAAA,CAAe,MAAA,CAAO,IAAKQ,CAAAA,EACxBjC,GAAAA,CAACL,EAAA,CAEG,KAAA,CAAOsC,CAAAA,CAAM,KAAA,CACb,aAAcA,CAAAA,CAAM,YAAA,CACpB,QAAA,CAAUA,CAAAA,CAAM,UAHXA,CAAAA,CAAM,EAIf,CACH,CAAA,CACL,EAGCR,CAAAA,CAAe,aAAA,EACZzB,GAAAA,CAAC,KAAA,CAAA,CAAI,UAAU,mBAAA,CACX,QAAA,CAAAA,GAAAA,CAAC,QAAA,CAAA,CACG,QAAS2B,CAAAA,CACT,QAAA,CAAUJ,CAAAA,CACV,SAAA,CAAU,gSAET,QAAA,CAAAA,CAAAA,CACGxB,IAAAA,CAAAmC,QAAAA,CAAA,CACI,QAAA,CAAA,CAAAnC,IAAAA,CAAC,OAAI,SAAA,CAAU,4CAAA,CAA6C,MAAM,4BAAA,CAA6B,IAAA,CAAK,MAAA,CAAO,OAAA,CAAQ,YAC/G,QAAA,CAAA,CAAAC,GAAAA,CAAC,QAAA,CAAA,CAAO,SAAA,CAAU,aAAa,EAAA,CAAG,IAAA,CAAK,EAAA,CAAG,IAAA,CAAK,EAAE,IAAA,CAAK,MAAA,CAAO,cAAA,CAAe,WAAA,CAAY,IAAI,CAAA,CAC5FA,GAAAA,CAAC,MAAA,CAAA,CAAK,SAAA,CAAU,aAAa,IAAA,CAAK,cAAA,CAAe,CAAA,CAAE,iHAAA,CAAkH,GACzK,CAAA,CAAM,YAAA,CAAA,CAEV,CAAA,CAEA,kBAAA,CAER,EACJ,CAAA,CAAA,CAER,CAAA,CAAA,CACJ,EAlEwBA,GAAAA,CAAC,KAAA,CAAA,CAAI,mCAAuB,CAoE5D","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=\"group block relative overflow-hidden rounded-lg bg-slate-800 ring-1 ring-white/10 hover:ring-brand-500/50 transition-all duration-300 hover:shadow-[0_0_20px_rgba(139,92,246,0.15)] transform hover:-translate-y-1\"\n >\n <div className=\"aspect-video w-full overflow-hidden relative\">\n <img\n src={thumbnailUrl}\n alt={title}\n className=\"w-full h-full object-cover transition-transform duration-500 group-hover:scale-105\"\n loading=\"lazy\"\n />\n <div className=\"absolute inset-0 bg-black/40 opacity-0 group-hover:opacity-100 transition-opacity duration-300 flex items-center justify-center\">\n <svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 24 24\" fill=\"currentColor\" className=\"w-12 h-12 text-white drop-shadow-lg\">\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=\"p-4\">\n <h3 className=\"text-sm font-semibold text-white group-hover:text-brand-300 transition-colors line-clamp-2 leading-snug\">\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 } from 'react';\nimport type { Playlist, Video } from '../types';\nimport { VideoCard } from './VideoCard';\nimport { fetchPlaylistVideos } from '../utils/youtube';\n\ninterface PlaylistsExplorerProps {\n initialPlaylists: Playlist[];\n apiKey?: string;\n onLoadMore?: (playlistId: string, pageToken: string) => Promise<{ videos: Video[], nextPageToken?: string }>;\n}\n\nexport const PlaylistsExplorer: React.FC<PlaylistsExplorerProps> = ({ initialPlaylists, apiKey, onLoadMore }) => {\n const [activePlaylistId, setActivePlaylistId] = useState<string>(initialPlaylists[0]?.id || \"\");\n const [playlists, setPlaylists] = useState<Playlist[]>(initialPlaylists);\n const [loadingMore, setLoadingMore] = useState<boolean>(false);\n\n const activePlaylist = playlists.find(p => p.id === activePlaylistId);\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=\"flex flex-col sm:flex-row justify-between items-center mb-8 gap-4\">\n <div className=\"flex space-x-2 overflow-x-auto pb-2 sm:pb-0 w-full sm:w-auto\">\n {playlists.map(playlist => (\n <button\n key={playlist.id}\n onClick={() => setActivePlaylistId(playlist.id)}\n className={`px-4 py-2 rounded-full text-sm font-medium whitespace-nowrap transition-colors ${activePlaylistId === playlist.id\n ? 'bg-brand-600 text-white shadow-lg shadow-brand-500/30'\n : 'bg-slate-800 text-slate-400 hover:text-white hover:bg-slate-700'\n }`}\n >\n {playlist.config?.title || playlist.title}\n </button>\n ))}\n </div>\n </div>\n\n {/* Playlist Content */}\n <div className=\"animate-fade-in\">\n {/* Always show title and description if enabled */}\n <div className=\"mb-6\">\n <h2 className=\"text-xl font-bold text-white mb-2\">{activePlaylist.title}</h2>\n {activePlaylist.config?.showDescription && (\n <p className=\"text-slate-400 max-w-3xl\">{activePlaylist.description}</p>\n )}\n </div>\n\n <div className=\"grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 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 {/* Load More */}\n {activePlaylist.nextPageToken && (\n <div className=\"mt-12 text-center\">\n <button\n onClick={handleLoadMore}\n disabled={loadingMore}\n className=\"inline-flex items-center px-6 py-3 border border-transparent text-base font-medium rounded-md shadow-sm text-white bg-slate-700 hover:bg-slate-600 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-brand-500 disabled:opacity-50 disabled:cursor-not-allowed transition-colors\"\n >\n {loadingMore ? (\n <>\n <svg className=\"animate-spin -ml-1 mr-3 h-5 w-5 text-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 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","jsxs","jsx","YOUTUBE_API_BASE","fetchPlaylistVideos","playlistId","apiKey","pageToken","url","itemsData","item","v","error","getPlaylist","playlistData","playlistSnippet","videosData","PlaylistsExplorer","initialPlaylists","onLoadMore","activePlaylistId","setActivePlaylistId","useState","playlists","setPlaylists","loadingMore","setLoadingMore","activePlaylist","p","handleLoadMore","newVideos","newNextPageToken","result","prevPlaylists","playlist","video","Fragment"],"mappings":"+EAQO,IAAMA,EAAsC,CAAC,CAAE,KAAA,CAAAC,CAAAA,CAAO,aAAAC,CAAAA,CAAc,QAAA,CAAAC,CAAS,CAAA,GAE5EC,KAAC,GAAA,CAAA,CACG,IAAA,CAAMD,CAAAA,CACN,MAAA,CAAO,SACP,GAAA,CAAI,qBAAA,CACJ,SAAA,CAAU,WAAA,CAEV,UAAAC,IAAAA,CAAC,KAAA,CAAA,CAAI,SAAA,CAAU,iBAAA,CACX,UAAAC,GAAAA,CAAC,KAAA,CAAA,CACG,GAAA,CAAKH,CAAAA,CACL,IAAKD,CAAAA,CACL,SAAA,CAAU,eAAA,CACV,OAAA,CAAQ,OACZ,CAAA,CACAI,GAAAA,CAAC,KAAA,CAAA,CAAI,SAAA,CAAU,oBACX,QAAA,CAAAA,GAAAA,CAAC,KAAA,CAAA,CAAI,KAAA,CAAM,6BAA6B,OAAA,CAAQ,WAAA,CAAY,IAAA,CAAK,cAAA,CAAe,UAAU,gBAAA,CACtF,QAAA,CAAAA,GAAAA,CAAC,MAAA,CAAA,CAAK,SAAS,SAAA,CAAU,CAAA,CAAE,yIAAA,CAA0I,QAAA,CAAS,UAAU,CAAA,CAC5L,CAAA,CACJ,GACJ,CAAA,CACAA,GAAAA,CAAC,OAAI,SAAA,CAAU,mBAAA,CACX,QAAA,CAAAA,GAAAA,CAAC,MAAG,SAAA,CAAU,iBAAA,CACT,QAAA,CAAAJ,CAAAA,CACL,EACJ,CAAA,CAAA,CACJ,EChCR,IAAMK,CAAAA,CAAmB,wCAQzB,eAAsBC,CAAAA,CAAoBC,CAAAA,CAAoBC,CAAAA,CAAgBC,EAAgD,CAC1H,GAAI,CAACD,CAAAA,CAAQ,OAAO,CAAE,KAAA,CAAO,oBAAqB,CAAA,CAElD,GAAI,CACA,IAAIE,CAAAA,CAAM,CAAA,EAAGL,CAAgB,CAAA,sDAAA,EAAyDE,CAAU,sBAAsBC,CAAM,CAAA,CAAA,CACxHC,IACAC,CAAAA,EAAO,CAAA,WAAA,EAAcD,CAAS,CAAA,CAAA,CAAA,CAIlC,IAAME,CAAAA,CAAY,KAAA,CADI,MAAM,KAAA,CAAMD,CAAG,CAAA,EACC,IAAA,EAAK,CAE3C,OAAKC,EAAU,KAAA,CAYR,CACH,MAAA,CARoBA,CAAAA,CAAU,MAAM,GAAA,CAAKC,CAAAA,GAAe,CACxD,EAAA,CAAIA,EAAK,OAAA,CAAQ,UAAA,CAAW,OAAA,CAC5B,KAAA,CAAOA,EAAK,OAAA,CAAQ,KAAA,CACpB,YAAA,CAAcA,CAAAA,CAAK,QAAQ,UAAA,CAAW,MAAA,EAAQ,KAAOA,CAAAA,CAAK,OAAA,CAAQ,WAAW,OAAA,EAAS,GAAA,CACtF,QAAA,CAAU,CAAA,gCAAA,EAAmCA,EAAK,OAAA,CAAQ,UAAA,CAAW,OAAO,CAAA,CAChF,EAAE,CAAA,CAAE,MAAA,CAAQC,CAAAA,EAAaA,CAAAA,CAAE,QAAU,eAAA,EAAmBA,CAAAA,CAAE,KAAA,GAAU,eAAe,EAI/E,aAAA,CAAeF,CAAAA,CAAU,aAC7B,CAAA,EAdI,QAAQ,KAAA,CAAM,CAAA,oCAAA,EAAuCJ,CAAU,CAAA,CAAA,CAAII,CAAS,CAAA,CACrE,CAAE,KAAA,CAAO,CAAA,mBAAA,EAAsBA,EAAU,KAAA,EAAO,OAAA,EAAW,KAAK,SAAA,CAAUA,CAAS,CAAC,CAAA,CAAG,CAAA,CActG,CAAA,MAASG,CAAAA,CAAO,CACZ,OAAA,OAAA,CAAQ,KAAA,CAAM,CAAA,8BAAA,EAAiCP,CAAU,IAAKO,CAAK,CAAA,CAC5D,CAAE,KAAA,CAAO,oBAAoBA,CAAK,CAAA,CAAG,CAChD,CACJ,CAEA,eAAsBC,CAAAA,CAAYR,CAAAA,CAAoBC,CAAAA,CAA0C,CAC5F,GAAI,CAACA,CAAAA,CACD,OAAA,OAAA,CAAQ,KAAK,qBAAqB,CAAA,CAC3B,IAAA,CAGX,GAAI,CAKA,IAAMQ,CAAAA,CAAe,MAHI,MAAM,KAAA,CAC3B,GAAGX,CAAgB,CAAA,2BAAA,EAA8BE,CAAU,CAAA,KAAA,EAAQC,CAAM,CAAA,CAC7E,CAAA,EAC4C,IAAA,EAAK,CAEjD,GAAI,CAACQ,CAAAA,CAAa,KAAA,EAASA,CAAAA,CAAa,MAAM,MAAA,GAAW,CAAA,CACrD,OAAA,OAAA,CAAQ,KAAA,CAAM,uBAAuBT,CAAU,CAAA,CAAE,CAAA,CAC1C,IAAA,CAGX,IAAMU,CAAAA,CAAkBD,CAAAA,CAAa,KAAA,CAAM,CAAC,EAAE,OAAA,CAGxCE,CAAAA,CAAa,MAAMZ,CAAAA,CAAoBC,EAAYC,CAAM,CAAA,CAE/D,OAAI,CAACU,CAAAA,EAAcA,EAAW,KAAA,EAAS,CAACA,CAAAA,CAAW,MAAA,CAAe,KAE3D,CACH,EAAA,CAAIX,CAAAA,CACJ,KAAA,CAAOU,EAAgB,KAAA,CACvB,WAAA,CAAaA,CAAAA,CAAgB,WAAA,CAC7B,OAAQC,CAAAA,CAAW,MAAA,CACnB,aAAA,CAAeA,CAAAA,CAAW,aAC9B,CAEJ,CAAA,MAASJ,CAAAA,CAAO,CACZ,eAAQ,KAAA,CAAM,CAAA,wBAAA,EAA2BP,CAAU,CAAA,CAAA,CAAA,CAAKO,CAAK,CAAA,CACtD,IACX,CACJ,CCtEO,IAAMK,CAAAA,CAAsD,CAAC,CAAE,gBAAA,CAAAC,CAAAA,CAAkB,MAAA,CAAAZ,CAAAA,CAAQ,WAAAa,CAAW,CAAA,GAAM,CAC7G,GAAM,CAACC,CAAAA,CAAkBC,CAAmB,CAAA,CAAIC,QAAAA,CAAiBJ,EAAiB,CAAC,CAAA,EAAG,EAAA,EAAM,EAAE,EACxF,CAACK,CAAAA,CAAWC,CAAY,CAAA,CAAIF,SAAqBJ,CAAgB,CAAA,CACjE,CAACO,CAAAA,CAAaC,CAAc,CAAA,CAAIJ,QAAAA,CAAkB,KAAK,CAAA,CAEvDK,EAAiBJ,CAAAA,CAAU,IAAA,CAAKK,CAAAA,EAAKA,CAAAA,CAAE,KAAOR,CAAgB,CAAA,CAE9DS,CAAAA,CAAiB,SAAY,CAC/B,GAAI,EAAA,CAACF,CAAAA,EAAkB,CAACA,EAAe,aAAA,CAAA,CAEvC,CAAAD,CAAAA,CAAe,IAAI,EACnB,GAAI,CACA,IAAII,CAAAA,CAAqB,EAAC,CACtBC,CAAAA,CAEJ,GAAIZ,CAAAA,CAAY,CACZ,IAAMa,CAAAA,CAAS,MAAMb,CAAAA,CAAWQ,EAAe,EAAA,CAAIA,CAAAA,CAAe,aAAa,CAAA,CAC/EG,CAAAA,CAAYE,EAAO,MAAA,CACnBD,CAAAA,CAAmBC,CAAAA,CAAO,cAC9B,SAAW1B,CAAAA,CAAQ,CACf,IAAM0B,CAAAA,CAAS,MAAM5B,CAAAA,CAAoBuB,CAAAA,CAAe,EAAA,CAAIrB,CAAAA,CAAQqB,EAAe,aAAa,CAAA,CAC5FK,CAAAA,CAAO,MAAA,GACPF,EAAYE,CAAAA,CAAO,MAAA,CACnBD,CAAAA,CAAmBC,CAAAA,CAAO,eAElC,CAAA,KAAO,CACH,OAAA,CAAQ,KAAA,CAAM,0CAA0C,CAAA,CACxD,MACJ,CAEIF,CAAAA,CAAU,OAAS,CAAA,EACnBN,CAAAA,CAAaS,GAAiBA,CAAAA,CAAc,GAAA,CAAIL,GACxCA,CAAAA,CAAE,EAAA,GAAOD,CAAAA,CAAe,EAAA,CACjB,CACH,GAAGC,CAAAA,CACH,MAAA,CAAQ,CAAC,GAAGA,CAAAA,CAAE,MAAA,CAAQ,GAAGE,CAAS,EAClC,aAAA,CAAeC,CACnB,CAAA,CAEGH,CACV,CAAC,EAEV,CAAA,MAAShB,CAAAA,CAAO,CACZ,QAAQ,KAAA,CAAM,4BAAA,CAA8BA,CAAK,EACrD,QAAE,CACEc,CAAAA,CAAe,KAAK,EACxB,EACJ,CAAA,CAEA,OAAKC,EAGD1B,IAAAA,CAAC,KAAA,CAAA,CAEG,UAAAC,GAAAA,CAAC,KAAA,CAAA,CAAI,SAAA,CAAU,sGAAA,CACX,SAAAA,GAAAA,CAAC,KAAA,CAAA,CAAI,SAAA,CAAU,iGAAA,CACV,SAAAqB,CAAAA,CAAU,GAAA,CAAIW,CAAAA,EACXhC,GAAAA,CAAC,UAEG,OAAA,CAAS,IAAMmB,CAAAA,CAAoBa,CAAAA,CAAS,EAAE,CAAA,CAC9C,SAAA,CAAW,CAAA,cAAA,EAAiBd,CAAAA,GAAqBc,EAAS,EAAA,CACpD,iBAAA,CACA,mBACF,CAAA,CAAA,CAEH,SAAAA,CAAAA,CAAS,MAAA,EAAQ,KAAA,EAASA,CAAAA,CAAS,OAP/BA,CAAAA,CAAS,EAQlB,CACH,CAAA,CACL,CAAA,CACJ,EAGAjC,IAAAA,CAAC,KAAA,CAAA,CAAI,SAAA,CAAU,sBAAA,CAEX,UAAAA,IAAAA,CAAC,KAAA,CAAA,CAAI,SAAA,CAAU,WAAA,CACX,UAAAC,GAAAA,CAAC,IAAA,CAAA,CAAG,SAAA,CAAU,sBAAA,CAAwB,SAAAyB,CAAAA,CAAe,KAAA,CAAM,CAAA,CAC1DA,CAAAA,CAAe,QAAQ,eAAA,EACpBzB,GAAAA,CAAC,GAAA,CAAA,CAAE,SAAA,CAAU,YAAa,QAAA,CAAAyB,CAAAA,CAAe,WAAA,CAAY,CAAA,CAAA,CAE7D,EAEAzB,GAAAA,CAAC,KAAA,CAAA,CAAI,SAAA,CAAU,mGAAA,CACV,SAAAyB,CAAAA,CAAe,MAAA,CAAO,IAAKQ,CAAAA,EACxBjC,GAAAA,CAACL,EAAA,CAEG,KAAA,CAAOsC,CAAAA,CAAM,KAAA,CACb,aAAcA,CAAAA,CAAM,YAAA,CACpB,QAAA,CAAUA,CAAAA,CAAM,UAHXA,CAAAA,CAAM,EAIf,CACH,CAAA,CACL,EAGCR,CAAAA,CAAe,aAAA,EACZzB,GAAAA,CAAC,KAAA,CAAA,CAAI,UAAU,wBAAA,CACX,QAAA,CAAAA,GAAAA,CAAC,QAAA,CAAA,CACG,QAAS2B,CAAAA,CACT,QAAA,CAAUJ,CAAAA,CACV,SAAA,CAAU,gBAET,QAAA,CAAAA,CAAAA,CACGxB,IAAAA,CAAAmC,QAAAA,CAAA,CACI,QAAA,CAAA,CAAAnC,IAAAA,CAAC,OAAI,SAAA,CAAU,cAAA,CAAe,MAAM,4BAAA,CAA6B,IAAA,CAAK,MAAA,CAAO,OAAA,CAAQ,YACjF,QAAA,CAAA,CAAAC,GAAAA,CAAC,QAAA,CAAA,CAAO,SAAA,CAAU,aAAa,EAAA,CAAG,IAAA,CAAK,EAAA,CAAG,IAAA,CAAK,EAAE,IAAA,CAAK,MAAA,CAAO,cAAA,CAAe,WAAA,CAAY,IAAI,CAAA,CAC5FA,GAAAA,CAAC,MAAA,CAAA,CAAK,SAAA,CAAU,aAAa,IAAA,CAAK,cAAA,CAAe,CAAA,CAAE,iHAAA,CAAkH,GACzK,CAAA,CAAM,YAAA,CAAA,CAEV,CAAA,CAEA,kBAAA,CAER,EACJ,CAAA,CAAA,CAER,CAAA,CAAA,CACJ,EAlEwBA,GAAAA,CAAC,KAAA,CAAA,CAAI,mCAAuB,CAoE5D","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 } from 'react';\nimport type { Playlist, Video } from '../types';\nimport { VideoCard } from './VideoCard';\nimport { fetchPlaylistVideos } from '../utils/youtube';\n\ninterface PlaylistsExplorerProps {\n initialPlaylists: Playlist[];\n apiKey?: string;\n onLoadMore?: (playlistId: string, pageToken: string) => Promise<{ videos: Video[], nextPageToken?: string }>;\n}\n\nexport const PlaylistsExplorer: React.FC<PlaylistsExplorerProps> = ({ initialPlaylists, apiKey, onLoadMore }) => {\n const [activePlaylistId, setActivePlaylistId] = useState<string>(initialPlaylists[0]?.id || \"\");\n const [playlists, setPlaylists] = useState<Playlist[]>(initialPlaylists);\n const [loadingMore, setLoadingMore] = useState<boolean>(false);\n\n const activePlaylist = playlists.find(p => p.id === activePlaylistId);\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 && (\n <p className=\"rypg-desc\">{activePlaylist.description}</p>\n )}\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 {/* Load More */}\n {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.1",
3
+ "version": "1.0.3",
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",
@@ -63,15 +63,15 @@ export const PlaylistsExplorer: React.FC<PlaylistsExplorerProps> = ({ initialPla
63
63
  return (
64
64
  <div>
65
65
  {/* Navigation & Tabs */}
66
- <div className="flex flex-col sm:flex-row justify-between items-center mb-8 gap-4">
67
- <div className="flex space-x-2 overflow-x-auto pb-2 sm:pb-0 w-full sm:w-auto">
66
+ <div className="rypg-flex rypg-flex-col rypg-sm-flex-row rypg-justify-between rypg-items-center rypg-mb-8 rypg-gap-4">
67
+ <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">
68
68
  {playlists.map(playlist => (
69
69
  <button
70
70
  key={playlist.id}
71
71
  onClick={() => setActivePlaylistId(playlist.id)}
72
- className={`px-4 py-2 rounded-full text-sm font-medium whitespace-nowrap transition-colors ${activePlaylistId === playlist.id
73
- ? 'bg-brand-600 text-white shadow-lg shadow-brand-500/30'
74
- : 'bg-slate-800 text-slate-400 hover:text-white hover:bg-slate-700'
72
+ className={`rypg-btn-pill ${activePlaylistId === playlist.id
73
+ ? 'rypg-btn-active'
74
+ : 'rypg-btn-inactive'
75
75
  }`}
76
76
  >
77
77
  {playlist.config?.title || playlist.title}
@@ -81,16 +81,16 @@ export const PlaylistsExplorer: React.FC<PlaylistsExplorerProps> = ({ initialPla
81
81
  </div>
82
82
 
83
83
  {/* Playlist Content */}
84
- <div className="animate-fade-in">
84
+ <div className="rypg-animate-fade-in">
85
85
  {/* Always show title and description if enabled */}
86
- <div className="mb-6">
87
- <h2 className="text-xl font-bold text-white mb-2">{activePlaylist.title}</h2>
86
+ <div className="rypg-mb-6">
87
+ <h2 className="rypg-title rypg-mb-2">{activePlaylist.title}</h2>
88
88
  {activePlaylist.config?.showDescription && (
89
- <p className="text-slate-400 max-w-3xl">{activePlaylist.description}</p>
89
+ <p className="rypg-desc">{activePlaylist.description}</p>
90
90
  )}
91
91
  </div>
92
92
 
93
- <div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-6">
93
+ <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">
94
94
  {activePlaylist.videos.map((video) => (
95
95
  <VideoCard
96
96
  key={video.id}
@@ -103,15 +103,15 @@ export const PlaylistsExplorer: React.FC<PlaylistsExplorerProps> = ({ initialPla
103
103
 
104
104
  {/* Load More */}
105
105
  {activePlaylist.nextPageToken && (
106
- <div className="mt-12 text-center">
106
+ <div className="mt-12 rypg-text-center">
107
107
  <button
108
108
  onClick={handleLoadMore}
109
109
  disabled={loadingMore}
110
- className="inline-flex items-center px-6 py-3 border border-transparent text-base font-medium rounded-md shadow-sm text-white bg-slate-700 hover:bg-slate-600 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-brand-500 disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
110
+ className="rypg-btn-load"
111
111
  >
112
112
  {loadingMore ? (
113
113
  <>
114
- <svg className="animate-spin -ml-1 mr-3 h-5 w-5 text-white" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
114
+ <svg className="rypg-spinner" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
115
115
  <circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4"></circle>
116
116
  <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>
117
117
  </svg>
@@ -12,23 +12,23 @@ export const VideoCard: React.FC<VideoCardProps> = ({ title, thumbnailUrl, video
12
12
  href={videoUrl}
13
13
  target="_blank"
14
14
  rel="noopener noreferrer"
15
- className="group block relative overflow-hidden rounded-lg bg-slate-800 ring-1 ring-white/10 hover:ring-brand-500/50 transition-all duration-300 hover:shadow-[0_0_20px_rgba(139,92,246,0.15)] transform hover:-translate-y-1"
15
+ className="rypg-card"
16
16
  >
17
- <div className="aspect-video w-full overflow-hidden relative">
17
+ <div className="rypg-card-media">
18
18
  <img
19
19
  src={thumbnailUrl}
20
20
  alt={title}
21
- className="w-full h-full object-cover transition-transform duration-500 group-hover:scale-105"
21
+ className="rypg-card-img"
22
22
  loading="lazy"
23
23
  />
24
- <div className="absolute inset-0 bg-black/40 opacity-0 group-hover:opacity-100 transition-opacity duration-300 flex items-center justify-center">
25
- <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" className="w-12 h-12 text-white drop-shadow-lg">
24
+ <div className="rypg-card-overlay">
25
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" className="rypg-play-icon">
26
26
  <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" />
27
27
  </svg>
28
28
  </div>
29
29
  </div>
30
- <div className="p-4">
31
- <h3 className="text-sm font-semibold text-white group-hover:text-brand-300 transition-colors line-clamp-2 leading-snug">
30
+ <div className="rypg-card-content">
31
+ <h3 className="rypg-card-title">
32
32
  {title}
33
33
  </h3>
34
34
  </div>
package/src/index.ts CHANGED
@@ -1,4 +1,6 @@
1
+ import './styles.css';
1
2
  export * from './components/PlaylistsExplorer';
3
+ export { PlaylistsExplorer as default } from './components/PlaylistsExplorer';
2
4
  export * from './components/VideoCard';
3
5
  export * from './types';
4
6
  export * from './utils/youtube';
package/src/styles.css ADDED
@@ -0,0 +1,186 @@
1
+ /* Base Colors (can be overridden by CSS variables) */
2
+ :root {
3
+ --rypg-brand-500: #8b5cf6;
4
+ --rypg-brand-600: #7c3aed;
5
+ --rypg-brand-300: #c4b5fd;
6
+ --rypg-slate-800: #1e293b;
7
+ --rypg-slate-700: #334155;
8
+ --rypg-slate-600: #475569;
9
+ --rypg-slate-400: #94a3b8;
10
+ --rypg-white: #ffffff;
11
+ --rypg-black-overlay: rgba(0, 0, 0, 0.4);
12
+ }
13
+
14
+ /* Animations */
15
+ @keyframes rypg-spin {
16
+ from { transform: rotate(0deg); }
17
+ to { transform: rotate(360deg); }
18
+ }
19
+
20
+ @keyframes rypg-fade-in {
21
+ from { opacity: 0; transform: translateY(10px); }
22
+ to { opacity: 1; transform: translateY(0); }
23
+ }
24
+
25
+ /* Utility / Layout */
26
+ .rypg-flex { display: flex; }
27
+ .rypg-flex-col { flex-direction: column; }
28
+ .rypg-items-center { align-items: center; }
29
+ .rypg-justify-between { justify-content: space-between; }
30
+ .rypg-gap-4 { gap: 1rem; }
31
+ .rypg-mb-8 { margin-bottom: 2rem; }
32
+ .rypg-mb-6 { margin-bottom: 1.5rem; }
33
+ .rypg-mb-2 { margin-bottom: 0.5rem; }
34
+ .rypg-pb-2 { padding-bottom: 0.5rem; }
35
+ .rypg-w-full { width: 100%; }
36
+ .rypg-overflow-x-auto { overflow-x: auto; }
37
+ .rypg-space-x-2 > * + * { margin-left: 0.5rem; }
38
+
39
+ @media (min-width: 640px) {
40
+ .rypg-sm-flex-row { flex-direction: row; }
41
+ .rypg-sm-pb-0 { padding-bottom: 0; }
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
+ }
51
+
52
+ .rypg-grid { display: grid; }
53
+ .rypg-grid-cols-1 { grid-template-columns: repeat(1, minmax(0, 1fr)); }
54
+ .rypg-gap-6 { gap: 1.5rem; }
55
+
56
+ /* Components */
57
+ .rypg-btn-pill {
58
+ padding: 0.5rem 1rem;
59
+ border-radius: 9999px;
60
+ font-size: 0.875rem;
61
+ font-weight: 500;
62
+ white-space: nowrap;
63
+ transition: all 0.2s;
64
+ border: none;
65
+ cursor: pointer;
66
+ }
67
+
68
+ .rypg-btn-active {
69
+ background-color: var(--rypg-brand-600);
70
+ color: var(--rypg-white);
71
+ box-shadow: 0 10px 15px -3px rgba(139, 92, 246, 0.3);
72
+ }
73
+
74
+ .rypg-btn-inactive {
75
+ background-color: var(--rypg-slate-800);
76
+ color: var(--rypg-slate-400);
77
+ }
78
+ .rypg-btn-inactive:hover {
79
+ color: var(--rypg-white);
80
+ background-color: var(--rypg-slate-700);
81
+ }
82
+
83
+ .rypg-title {
84
+ font-size: 1.25rem;
85
+ font-weight: 700;
86
+ color: var(--rypg-white);
87
+ }
88
+
89
+ .rypg-desc {
90
+ color: var(--rypg-slate-400);
91
+ max-width: 48rem;
92
+ margin: 0;
93
+ }
94
+
95
+ .rypg-btn-load {
96
+ display: inline-flex;
97
+ align-items: center;
98
+ padding: 0.75rem 1.5rem;
99
+ border: 1px solid transparent;
100
+ font-size: 1rem;
101
+ font-weight: 500;
102
+ border-radius: 0.375rem;
103
+ color: var(--rypg-white);
104
+ background-color: var(--rypg-slate-700);
105
+ cursor: pointer;
106
+ transition: background-color 0.2s;
107
+ }
108
+ .rypg-btn-load:hover { background-color: var(--rypg-slate-600); }
109
+ .rypg-btn-load:disabled { opacity: 0.5; cursor: not-allowed; }
110
+
111
+ .rypg-spinner {
112
+ animation: rypg-spin 1s linear infinite;
113
+ margin-right: 0.75rem;
114
+ height: 1.25rem;
115
+ width: 1.25rem;
116
+ }
117
+
118
+ /* Video Card */
119
+ .rypg-card {
120
+ display: block;
121
+ position: relative;
122
+ overflow: hidden;
123
+ border-radius: 0.5rem;
124
+ background-color: var(--rypg-slate-800);
125
+ text-decoration: none;
126
+ transition: all 0.3s;
127
+ transform: translateZ(0); /* Hardware accel */
128
+ box-shadow: 0 0 0 1px rgba(255, 255, 255, 0.1);
129
+ }
130
+ .rypg-card:hover {
131
+ transform: translateY(-0.25rem);
132
+ box-shadow: 0 0 20px rgba(139, 92, 246, 0.15);
133
+ border-color: rgba(139, 92, 246, 0.5);
134
+ }
135
+
136
+ .rypg-card-media {
137
+ aspect-ratio: 16 / 9;
138
+ width: 100%;
139
+ overflow: hidden;
140
+ position: relative;
141
+ }
142
+
143
+ .rypg-card-img {
144
+ width: 100%;
145
+ height: 100%;
146
+ object-fit: cover;
147
+ transition: transform 0.5s;
148
+ }
149
+ .rypg-card:hover .rypg-card-img { transform: scale(1.05); }
150
+
151
+ .rypg-card-overlay {
152
+ position: absolute;
153
+ inset: 0;
154
+ background-color: var(--rypg-black-overlay);
155
+ opacity: 0;
156
+ display: flex;
157
+ align-items: center;
158
+ justify-content: center;
159
+ transition: opacity 0.3s;
160
+ }
161
+ .rypg-card:hover .rypg-card-overlay { opacity: 1; }
162
+
163
+ .rypg-play-icon {
164
+ width: 3rem;
165
+ height: 3rem;
166
+ color: var(--rypg-white);
167
+ filter: drop-shadow(0 4px 6px rgba(0,0,0,0.5));
168
+ }
169
+
170
+ .rypg-card-content { padding: 1rem; }
171
+ .rypg-card-title {
172
+ font-size: 0.875rem;
173
+ font-weight: 600;
174
+ color: var(--rypg-white);
175
+ margin: 0;
176
+ display: -webkit-box;
177
+ -webkit-line-clamp: 2;
178
+ -webkit-box-orient: vertical;
179
+ overflow: hidden;
180
+ line-height: 1.375;
181
+ transition: color 0.2s;
182
+ }
183
+ .rypg-card:hover .rypg-card-title { color: var(--rypg-brand-300); }
184
+
185
+ .rypg-animate-fade-in { animation: rypg-fade-in 0.5s ease-out; }
186
+ .rypg-text-center { text-align: center; }