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)}
+ >
+
+
})
{
+ 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