diff --git a/docs/videos/GL.md b/docs/videos/GL.md new file mode 100644 index 0000000..cb10f4a --- /dev/null +++ b/docs/videos/GL.md @@ -0,0 +1,13 @@ +--- +title: GL +description: A video playlist of GL +slug: /videos/gl +tags: ["SQL Account", "Video", "GL"] +hide_table_of_contents: true +--- + +import YouTubePlaylist from '@site/src/components/yt-playlist'; + +# GL + + \ No newline at end of file diff --git a/docs/videos/_category_.json b/docs/videos/_category_.json new file mode 100644 index 0000000..06d86ea --- /dev/null +++ b/docs/videos/_category_.json @@ -0,0 +1,9 @@ +{ + "label": "Videos", + "position": 11, + "link": { + "type": "generated-index", + "title": "Videos", + "description": "A comprehensive video playlist for SQL Account" + } +} \ No newline at end of file diff --git a/docs/videos/add-to-cart.md b/docs/videos/add-to-cart.md new file mode 100644 index 0000000..7d9b798 --- /dev/null +++ b/docs/videos/add-to-cart.md @@ -0,0 +1,13 @@ +--- +title: AddToCart App +description: A video playlist of AddToCart app +slug: /videos/add-to-cart +tags: ["SQL Account", "Video", "App", "AddToCart"] +hide_table_of_contents: true +--- + +import YouTubePlaylist from '@site/src/components/yt-playlist'; + +# AddToCart App + + \ No newline at end of file diff --git a/docs/videos/customer-sales.md b/docs/videos/customer-sales.md new file mode 100644 index 0000000..1c4ed3d --- /dev/null +++ b/docs/videos/customer-sales.md @@ -0,0 +1,13 @@ +--- +title: Customer / Sales +description: A video playlist of Customer and Sales +slug: /videos/customer-sales +tags: ["SQL Account", "Video", "Customer", "Sales"] +hide_table_of_contents: true +--- + +import YouTubePlaylist from '@site/src/components/yt-playlist'; + +# Customer / Sales + + \ No newline at end of file diff --git a/docs/videos/customization.md b/docs/videos/customization.md new file mode 100644 index 0000000..4ff2779 --- /dev/null +++ b/docs/videos/customization.md @@ -0,0 +1,13 @@ +--- +title: Customization +description: A video playlist of Customization features +slug: /videos/customisation +tags: ["SQL Account", "Video", "Customization"] +hide_table_of_contents: true +--- + +import YouTubePlaylist from '@src-sqlacc/components/yt-playlist'; + +# Customization + + \ No newline at end of file diff --git a/docs/videos/customize-report-diy.md b/docs/videos/customize-report-diy.md new file mode 100644 index 0000000..04bd706 --- /dev/null +++ b/docs/videos/customize-report-diy.md @@ -0,0 +1,13 @@ +--- +title: Customize Report / DIY +description: A video playlist of how-to customize report and use DIY script +slug: /videos/customize-report-diy +tags: ["SQL Account", "Video", "Customize", "Report", "DIY"] +hide_table_of_contents: true +--- + +import YouTubePlaylist from '@site/src/components/yt-playlist'; + +# Customize Report / DIY + + \ No newline at end of file diff --git a/docs/videos/data-import.md b/docs/videos/data-import.md new file mode 100644 index 0000000..f7b3241 --- /dev/null +++ b/docs/videos/data-import.md @@ -0,0 +1,13 @@ +--- +title: Data Import +description: A video playlist of Data Import features +slug: /videos/data-import +tags: ["SQL Account", "Video", "Data Import"] +hide_table_of_contents: true +--- + +import YouTubePlaylist from '@site/src/components/yt-playlist'; + +# Data Import + + \ No newline at end of file diff --git a/docs/videos/eCommerce.md b/docs/videos/eCommerce.md new file mode 100644 index 0000000..57ba9af --- /dev/null +++ b/docs/videos/eCommerce.md @@ -0,0 +1,13 @@ +--- +title: E-Commerce +description: A video playlist of E-Commerce feature +slug: /videos/eCommerce +tags: ["SQL Account", "Video", "E-Commerce"] +hide_table_of_contents: true +--- + +import YouTubePlaylist from '@site/src/components/yt-playlist'; + +# E-Commerce + + \ No newline at end of file diff --git a/docs/videos/gst.md b/docs/videos/gst.md new file mode 100644 index 0000000..a4de8f4 --- /dev/null +++ b/docs/videos/gst.md @@ -0,0 +1,13 @@ +--- +title: GST +description: A video playlist of GST +slug: /videos/gst +tags: ["SQL Account", "Video", "Tax", "GST"] +hide_table_of_contents: true +--- + +import YouTubePlaylist from '@site/src/components/yt-playlist'; + +# GST + + \ No newline at end of file diff --git a/docs/videos/interbank-giro.md b/docs/videos/interbank-giro.md new file mode 100644 index 0000000..f1bb036 --- /dev/null +++ b/docs/videos/interbank-giro.md @@ -0,0 +1,13 @@ +--- +title: Interbank GIRO +description: A video playlist of Interbank GIRO +slug: /videos/interbank-giro +tags: ["SQL Account", "Video", "Bank", "GIRO"] +hide_table_of_contents: true +--- + +import YouTubePlaylist from '@site/src/components/yt-playlist'; + +# Interbank GIRO + + \ No newline at end of file diff --git a/docs/videos/production.md b/docs/videos/production.md new file mode 100644 index 0000000..0637815 --- /dev/null +++ b/docs/videos/production.md @@ -0,0 +1,13 @@ +--- +title: Production +description: A video playlist of Production Job +slug: /videos/production +tags: ["SQL Account", "Video", "Production"] +hide_table_of_contents: true +--- + +import YouTubePlaylist from '@site/src/components/yt-playlist'; + +# Production + + \ No newline at end of file diff --git a/docs/videos/sst.md b/docs/videos/sst.md new file mode 100644 index 0000000..909aa9c --- /dev/null +++ b/docs/videos/sst.md @@ -0,0 +1,13 @@ +--- +title: SST +description: A video playlist of SST app +slug: /videos/sst +tags: ["SQL Account", "Video", "Tax", "SST"] +hide_table_of_contents: true +--- + +import YouTubePlaylist from '@site/src/components/yt-playlist'; + +# SST + + \ No newline at end of file diff --git a/docs/videos/stock-take-app.md b/docs/videos/stock-take-app.md new file mode 100644 index 0000000..fb988db --- /dev/null +++ b/docs/videos/stock-take-app.md @@ -0,0 +1,13 @@ +--- +title: Stock Take App +description: A video playlist of Stock Take +slug: /videos/stock-take-app +tags: ["SQL Account", "Video", "App", "Stock Take"] +hide_table_of_contents: true +--- + +import YouTubePlaylist from '@site/src/components/yt-playlist'; + +# Stock Take App + + \ No newline at end of file diff --git a/docs/videos/stock.md b/docs/videos/stock.md new file mode 100644 index 0000000..6f86d17 --- /dev/null +++ b/docs/videos/stock.md @@ -0,0 +1,13 @@ +--- +title: Stock +description: A video playlist of Stock +slug: /videos/stock +tags: ["SQL Account", "Video", "Stock"] +hide_table_of_contents: true +--- + +import YouTubePlaylist from '@site/src/components/yt-playlist'; + +# Stock + + \ No newline at end of file diff --git a/docs/videos/supplier-purchase.md b/docs/videos/supplier-purchase.md new file mode 100644 index 0000000..1343f35 --- /dev/null +++ b/docs/videos/supplier-purchase.md @@ -0,0 +1,13 @@ +--- +title: Supplier / Purchase +description: A video playlist of Supplier and Purchase +slug: /videos/supplier-purchase +tags: ["SQL Account", "Video", "Supplier", "Purchase"] +hide_table_of_contents: true +--- + +import YouTubePlaylist from '@site/src/components/yt-playlist'; + +# Supplier / Purchase + + \ No newline at end of file diff --git a/docs/videos/tool-settings-backup.md b/docs/videos/tool-settings-backup.md new file mode 100644 index 0000000..19d57fe --- /dev/null +++ b/docs/videos/tool-settings-backup.md @@ -0,0 +1,13 @@ +--- +title: Tool & Settings/Backup +description: A video playlist of Tools, Settings and Backup +slug: /videos/tool-settings-backup +tags: ["SQL Account", "Video", "Settings", "Tools", "Backup"] +hide_table_of_contents: true +--- + +import YouTubePlaylist from '@site/src/components/yt-playlist'; + +# Tool & Settings/Backup + + \ No newline at end of file diff --git a/docusaurus.config.js b/docusaurus.config.js index 6807119..279b21a 100644 --- a/docusaurus.config.js +++ b/docusaurus.config.js @@ -6,6 +6,10 @@ import { themes as prismThemes } from "prism-react-renderer"; import path from "path"; +import dotenv from 'dotenv'; + +// Load environment variables from .env file +dotenv.config(); /** @type {import('@docusaurus/types').Config} */ const config = { @@ -46,6 +50,11 @@ const config = { }; } ], + + customFields: { + youtubeApiKey: process.env.YOUTUBE_API_KEY, + }, + presets: [ [ "classic", diff --git a/package-lock.json b/package-lock.json index 38a911e..6f73cf0 100644 --- a/package-lock.json +++ b/package-lock.json @@ -14,6 +14,7 @@ "@easyops-cn/docusaurus-search-local": "^0.40.1", "@mdx-js/react": "^3.0.0", "clsx": "^2.0.0", + "dotenv": "^16.4.7", "plugin-image-zoom": "github:flexanalytics/plugin-image-zoom", "prism-react-renderer": "^2.3.0", "qrcode.react": "^4.2.0", @@ -7483,6 +7484,18 @@ "node": ">=8" } }, + "node_modules/dotenv": { + "version": "16.4.7", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.7.tgz", + "integrity": "sha512-47qPchRCykZC03FhkYAhrvwU4xDBFIj1QPqaarj6mdM/hgUzfPHcpkHJOn3mJAufFeeAxAzeGsr5X0M4k6fLZQ==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, "node_modules/dunder-proto": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", diff --git a/package.json b/package.json index 5e88eac..88b2dd4 100644 --- a/package.json +++ b/package.json @@ -20,6 +20,7 @@ "@easyops-cn/docusaurus-search-local": "^0.40.1", "@mdx-js/react": "^3.0.0", "clsx": "^2.0.0", + "dotenv": "^16.4.7", "plugin-image-zoom": "github:flexanalytics/plugin-image-zoom", "prism-react-renderer": "^2.3.0", "qrcode.react": "^4.2.0", diff --git a/src/components/yt-playlist.js b/src/components/yt-playlist.js new file mode 100644 index 0000000..e145161 --- /dev/null +++ b/src/components/yt-playlist.js @@ -0,0 +1,191 @@ +import React, { useState, useEffect, useRef } from 'react'; +import styles from "@site/src/css/yt-playlist.module.css"; +import useDocusaurusContext from '@docusaurus/useDocusaurusContext'; + +export default function YouTubePlaylist({ playlistId }) { + const { siteConfig } = useDocusaurusContext(); + const apiKey = siteConfig.customFields?.youtubeApiKey; + const [playlistItems, setPlaylistItems] = useState([]); + const [selectedVideo, setSelectedVideo] = useState(null); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + const [nextPageToken, setNextPageToken] = useState(null); + const [loadingMore, setLoadingMore] = useState(false); + + const videoListRef = useRef(null); + const loadingRef = useRef(null); + + useEffect(() => { + const fetchPlaylistData = async () => { + try { + setLoading(true); + const response = await fetch( + `https://www.googleapis.com/youtube/v3/playlistItems?part=snippet&maxResults=50&playlistId=${playlistId}&key=${apiKey}` + ); + + if (!response.ok) { + throw new Error('Failed to fetch playlist data'); + } + + const data = await response.json(); + setPlaylistItems(data.items || []); + + if (data.items && data.items.length > 0) { + setSelectedVideo(data.items[0].snippet.resourceId.videoId); + } + + setNextPageToken(data.nextPageToken || null); + + } catch (err) { + setError(err.message); + } finally { + setLoading(false); + setLoadingMore(false); + } + }; + + if (playlistId && apiKey) { + fetchPlaylistData(); + } else { + setError('Playlist ID and API Key are required'); + setLoading(false); + } + }, [playlistId, apiKey]); + + const loadMoreVideos = () => { + if (nextPageToken && !loadingMore) { + setLoadingMore(true); + const fetchMoreVideos = async () => { + try { + const response = await fetch( + `https://www.googleapis.com/youtube/v3/playlistItems?part=snippet&maxResults=50&playlistId=${playlistId}&key=${apiKey}&pageToken=${nextPageToken}` + ); + + if (!response.ok) { + throw new Error('Failed to fetch additional videos'); + } + + const data = await response.json(); + setPlaylistItems(prevItems => [...prevItems, ...(data.items || [])]); + setNextPageToken(data.nextPageToken || null); + } catch (err) { + setError(err.message); + } finally { + setLoadingMore(false); + } + }; + + fetchMoreVideos(); + } + }; + + useEffect(() => { + const handleScroll = () => { + if (!videoListRef.current || !loadingRef.current || loading || loadingMore || !nextPageToken) return; + + const videoListElement = videoListRef.current; + const loadingElement = loadingRef.current; + + // Check if the user has scrolled to the bottom (with a small threshold) + const threshold = 50; // pixels from bottom to trigger loading + const isAtBottom = + videoListElement.scrollTop + videoListElement.clientHeight >= + videoListElement.scrollHeight - threshold; + + if (isAtBottom) { + loadMoreVideos(); + } + }; + + const listElement = videoListRef.current; + if (listElement) { + listElement.addEventListener('scroll', handleScroll); + } + + return () => { + if (listElement) { + listElement.removeEventListener('scroll', handleScroll); + } + }; + }, [loading, loadingMore, nextPageToken]); + + const selectVideo = (videoId) => { + setSelectedVideo(videoId); + }; + + // Helper function to safely get thumbnail URL + const getThumbnailUrl = (thumbnails) => { + if (!thumbnails) return null; + + // Try to get thumbnails in order of preference + if (thumbnails.default?.url) return thumbnails.default.url; + if (thumbnails.medium?.url) return thumbnails.medium.url; + if (thumbnails.high?.url) return thumbnails.high.url; + if (thumbnails.standard?.url) return thumbnails.standard.url; + if (thumbnails.maxres?.url) return thumbnails.maxres.url; + + // If we have thumbnails object but none of the expected formats, + // find the first URL property + const firstThumbnail = Object.values(thumbnails).find(thumb => thumb?.url); + if (firstThumbnail?.url) return firstThumbnail.url; + + return 'https://placehold.co/120x68?text=Unavailable'; + }; + + if (loading && playlistItems.length === 0) return
Loading playlist...
; + if (error) return
Error: {error}
; + + return ( +
+
+ {selectedVideo && ( + + )} +
+ +
+

Playlist Videos

+
    + {playlistItems.map((item) => ( +
  • selectVideo(item.snippet.resourceId.videoId)} + > +
    + {item.snippet.title { + e.target.onerror = null; + e.target.src = 'https://placehold.co/120x68?text=Unavailable'; + }} + /> +
    +
    +
    {item.snippet.title || 'Untitled video'}
    +
    +
  • + ))} + + {nextPageToken && ( +
  • + {loadingMore ? ( +
    + ) : ( +
    Scroll for more videos
    + )} +
  • + )} +
+
+
+ ); +} \ No newline at end of file diff --git a/src/css/yt-playlist.module.css b/src/css/yt-playlist.module.css new file mode 100644 index 0000000..a093f07 --- /dev/null +++ b/src/css/yt-playlist.module.css @@ -0,0 +1,150 @@ +.playlistContainer { + display: flex; + flex-direction: column; + gap: 20px; + margin-bottom: 30px; +} + +.videoPlayer { + width: 100%; + aspect-ratio: 16/9; +} + +.videoIframe { + width: 100%; + height: 100%; + border-radius: 8px; +} + +.playlistItems { + width: 100%; + border: 1px solid var(--ifm-color-emphasis-300); + border-radius: 8px; + padding: 15px; + background-color: var(--ifm-background-surface-color); +} + +.videoList { + list-style: none; + padding: 0; + margin: 0; + max-height: 400px; + overflow-y: auto; +} + +.videoItem { + display: flex; + padding: 10px; + cursor: pointer; + border-radius: 4px; + transition: background-color 0.2s; + margin-bottom: 8px; +} + +.videoItem:hover { + background-color: var(--ifm-color-emphasis-200); +} + +.selectedVideo { + background-color: var(--ifm-color-emphasis-300); +} + +.thumbnailContainer { + flex: 0 0 120px; + margin-right: 12px; +} + +.thumbnail { + width: 120px; + height: 68px; + object-fit: cover; + border-radius: 4px; +} + +.videoInfo { + flex: 1; + display: flex; + flex-direction: column; + justify-content: center; +} + +.videoTitle { + font-size: 0.9rem; + line-height: 1.3; + overflow: hidden; + text-overflow: ellipsis; + display: -webkit-box; + line-clamp: 2; + -webkit-line-clamp: 2; + -webkit-box-orient: vertical; +} + +.loading, .error { + padding: 20px; + text-align: center; + border-radius: 8px; + background-color: var(--ifm-color-emphasis-100); +} + +.error { + color: var(--ifm-color-danger); +} + +.loadingMoreItem { + padding: 15px; + text-align: center; + display: flex; + justify-content: center; + align-items: center; + height: 60px; +} + +.spinner { + border: 3px solid rgba(0, 0, 0, 0.1); + border-top: 3px solid var(--ifm-color-primary); + border-radius: 50%; + width: 24px; + height: 24px; + animation: spin 1s linear infinite; +} + +@keyframes spin { + 0% { transform: rotate(0deg); } + 100% { transform: rotate(360deg); } +} + +.scrollIndicator { + font-size: 0.9rem; + color: var(--ifm-color-emphasis-600); + font-style: italic; +} + +/* Ensure the videoList has scrolling behavior */ +.videoList { + list-style: none; + padding: 0; + margin: 0; + max-height: 400px; + overflow-y: auto; + -webkit-overflow-scrolling: touch; /* For smoother scrolling on iOS */ +} + + +@media (min-width: 768px) { + .playlistContainer { + flex-direction: row; + } + + .videoPlayer { + flex: 0 0 65%; + } + + .playlistItems { + flex: 0 0 30%; + max-height: none; + } + + .videoList { + max-height: 500px; + } +} \ No newline at end of file