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 +10 -1
- package/dist/index.css +2 -0
- package/dist/index.css.map +1 -0
- package/dist/index.d.mts +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +2 -2
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +2 -2
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
- package/src/components/PlaylistsExplorer.tsx +13 -13
- package/src/components/VideoCard.tsx +7 -7
- package/src/index.ts +2 -0
- package/src/styles.css +186 -0
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
|
-
|
|
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
|
|
2
|
-
exports.PlaylistsExplorer=
|
|
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
|
|
2
|
-
export{
|
|
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
|
package/dist/index.mjs.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.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":"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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 } 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
|
@@ -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
|
|
67
|
-
<div className="flex space-x-2 overflow-x-auto pb-2 sm
|
|
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={`
|
|
73
|
-
? '
|
|
74
|
-
: '
|
|
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="
|
|
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="
|
|
89
|
+
<p className="rypg-desc">{activePlaylist.description}</p>
|
|
90
90
|
)}
|
|
91
91
|
</div>
|
|
92
92
|
|
|
93
|
-
<div className="grid grid-cols-1 sm
|
|
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="
|
|
110
|
+
className="rypg-btn-load"
|
|
111
111
|
>
|
|
112
112
|
{loadingMore ? (
|
|
113
113
|
<>
|
|
114
|
-
<svg className="
|
|
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="
|
|
15
|
+
className="rypg-card"
|
|
16
16
|
>
|
|
17
|
-
<div className="
|
|
17
|
+
<div className="rypg-card-media">
|
|
18
18
|
<img
|
|
19
19
|
src={thumbnailUrl}
|
|
20
20
|
alt={title}
|
|
21
|
-
className="
|
|
21
|
+
className="rypg-card-img"
|
|
22
22
|
loading="lazy"
|
|
23
23
|
/>
|
|
24
|
-
<div className="
|
|
25
|
-
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" className="
|
|
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="
|
|
31
|
-
<h3 className="
|
|
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
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; }
|