diff options
| -rw-r--r-- | src/components/GitHubProjects.svelte | 3 | ||||
| -rw-r--r-- | src/components/Loader.svelte | 30 | ||||
| -rw-r--r-- | src/components/Navbar.svelte | 1 | ||||
| -rw-r--r-- | src/components/YouTubeVideos.svelte | 133 | ||||
| -rw-r--r-- | src/pages/videos.astro | 10 |
5 files changed, 176 insertions, 1 deletions
diff --git a/src/components/GitHubProjects.svelte b/src/components/GitHubProjects.svelte index 2753b89..9d9d569 100644 --- a/src/components/GitHubProjects.svelte +++ b/src/components/GitHubProjects.svelte @@ -1,5 +1,6 @@ <script> import { onMount } from 'svelte'; + import Loader from "./Loader.svelte"; export let username; export let isOrganization; let repos = []; @@ -54,7 +55,7 @@ <div> {#if isLoading} - <div>Loading...</div> + <Loader /> {:else} {#if error} <div class="error">{error}</div> diff --git a/src/components/Loader.svelte b/src/components/Loader.svelte new file mode 100644 index 0000000..55d1d40 --- /dev/null +++ b/src/components/Loader.svelte @@ -0,0 +1,30 @@ +<style> + .loader { + width: 48px; + height: 48px; + border: 5px solid #b3f69e; + border-bottom-color: #609460; + border-radius: 50%; + display: inline-block; + box-sizing: border-box; + animation: rotation 1s linear infinite; + } + + @keyframes rotation { + 0% { + transform: rotate(0deg); + } + 100% { + transform: rotate(360deg); + } + } + .centre { + display: flex; + justify-content: center; + } + +</style> + +<div class="centre"> + <span class="loader"></span> +</div> diff --git a/src/components/Navbar.svelte b/src/components/Navbar.svelte index 11f2ef3..3211799 100644 --- a/src/components/Navbar.svelte +++ b/src/components/Navbar.svelte @@ -109,6 +109,7 @@ <li><a href="/projects" class="nav-link" on:click={toggleNav}>Projects</a></li> <li><a href="/blog" class="nav-link" on:click={toggleNav}>Blog</a></li> <li><a href="/guestbook" class="nav-link" on:click={toggleNav}>Guestbook</a></li> + <li><a href="/videos" class="nav-link" on:click={toggleNav}>Videos</a></li> <!-- <li><a href="https://archive.alee14.me" class="nav-link" on:click={toggleNav}>Archive</a></li>--> <li><a href="/contacts" class="nav-link" on:click={toggleNav}>Contacts</a></li> </ul> diff --git a/src/components/YouTubeVideos.svelte b/src/components/YouTubeVideos.svelte new file mode 100644 index 0000000..677e502 --- /dev/null +++ b/src/components/YouTubeVideos.svelte @@ -0,0 +1,133 @@ +<script> + import { onMount, onDestroy } from 'svelte'; + import Loader from './Loader.svelte'; + import { formatDate } from "../util"; + + let error = null; + let isLoading = true; + let videos = []; + let nextPageToken = ''; + + const fetchVideos = async (pageToken = '') => { + isLoading = true; + const url = `${import.meta.env.PUBLIC_API_URL}/youtube?pageToken=${pageToken}`; + try { + const response = await fetch(url); + if (!response.ok) { + console.error('HTTP Error: ' + response.statusText); + error = 'HTTP Error: ' + response.statusText; + return; + } + const data = await response.json(); + videos = [...videos, ...data.items]; + nextPageToken = data.nextPageToken || ''; + } catch (e) { + console.error(e); + error = e.message; + } finally { + isLoading = false; + } + }; + + function onScroll() { + const nearBottom = window.innerHeight + window.scrollY >= document.body.offsetHeight - 2; + if (nearBottom && !isLoading && nextPageToken) { + fetchVideos(nextPageToken); + } + } + + onMount(() => { + if (typeof window !== 'undefined') { + window.addEventListener('scroll', onScroll); + fetchVideos().then(() => isLoading = false); + } + }); + + onDestroy(() => { + if (typeof window !== 'undefined') { + window.removeEventListener('scroll', onScroll); + } + }); +</script> + +<style> + .title { + margin-top: .2em; + margin-bottom: .2em; + } + + img { + width: 100%; + height: auto; + border-radius: 1em; + object-fit: cover; + } + + .card small { + display: block; + margin-bottom: 1em; + } + + .zoom { + transition: transform 0.2s ease; /* Smooth transition for the transform property */ + display: block; /* Ensures the image is block-level for proper scaling */ + margin: auto; /* Keeps the image centered */ + max-width: 100%; /* Ensures the image does not exceed its container's width */ + } + + .zoom:hover { + transform: scale(1.1); /* Scales the image to 110% of its original size on hover */ + } + + .container { + display: grid; + gap: 1em; /* Adjusts the gap between grid items */ + } + + @media (min-width: 992px) { + .container { + grid-template-columns: repeat(3, 1fr); /* Original setting for large screens */ + } + } + + @media (max-width: 991px) and (min-width: 768px) { + .container { + grid-template-columns: repeat(2, 1fr); /* Adjusts to 2 columns for tablets */ + } + } + + @media (max-width: 767px) { + .container { + grid-template-columns: 1fr; /* Adjusts to a single column for mobile */ + } + } + +</style> + +<div> + <div> + {#if isLoading && videos.length === 0} + <Loader /> + {:else} + {#if error} + <div class="error">{error}</div> + {:else} + <div class="container"> + {#each videos as video} + <div class="card"> + <a href={`https://youtu.be/${video.snippet.resourceId.videoId}`} target="_blank"> + <h3 class="title">{video.snippet.title}</h3> + <small>{formatDate(video.snippet.publishedAt)}</small> + <img src={video.snippet.thumbnails.medium.url} alt={video.snippet.title} class="zoom" /> + </a> + </div> + {/each} + </div> + <div class="load-more"></div> + {/if} + {/if} + {#if isLoading && videos.length > 0} + <Loader /> + {/if} + </div> +</div> diff --git a/src/pages/videos.astro b/src/pages/videos.astro new file mode 100644 index 0000000..c276fe7 --- /dev/null +++ b/src/pages/videos.astro @@ -0,0 +1,10 @@ +--- +import Page from "../layouts/Page.astro"; +import "../styles/cards.css"; +import YouTubeVideos from "../components/YouTubeVideos.svelte"; +--- +<Page title="Videos" description="All videos that I have made on YouTube"> + <main> + <YouTubeVideos client:load /> + </main> +</Page> |
