Imagine an app where users can swipe up and down to browse a variety of stylish photos and videos of girls, delivering a TikTok-like immersive experience. Today, I’ll walk you through building an addictive random girl content browser from scratch. This app features smooth swipe interactions, auto-play, smart error handling, and multiple playback modes, ensuring users enjoy high-quality content effortlessly.
Live Demo: This app is deployed at https://immersiview.utities.online/ as part of the utility toolkit www.utities.online. You can check it out right now!
Before diving into the code, let’s outline the core technologies we’ll use:
First, we need to create the entry page for our app. This page is minimal and primarily renders our core video swipe component.
tsx// app/random-girl-videos/page.tsx import VideoSlider from "../components/VideoSlider"; // Client-side renderer for random girl videos export default function RandomVideoRenderer() { // Simple and clean: render the video slider component return; }
This entry point delegates all core logic to the VideoSlider
component, keeping the codebase clean and modular.
To fully immerse users in the video content, we need to disable page scrolling. We’ll create a custom hook for this:
tsx// hooks/useBodyScrollLock.ts import { useEffect } from 'react'; /** * Custom hook to control body scroll behavior. * Disables body scroll on mount, restores it on unmount. */ export function useBodyScrollLock() { useEffect(() => { // Save the original overflow value const originalOverflow = document.body.style.overflow; // Disable body scroll document.body.style.overflow = 'hidden'; // Cleanup: Restore the original overflow value return () => { document.body.style.overflow = originalOverflow; }; }, []); }
This simple hook is crucial for creating an immersive experience by temporarily modifying the document.body.style.overflow
property to prevent page scrolling, keeping the user focused on the current video.
The VideoSlider
component is the heart of the app, managing video loading, display, and interaction logic. Here’s how to implement it:
tsx// app/components/VideoSlider.tsx "use client"; import { useState, useCallback, useEffect } from "react"; import { RefreshCw } from "lucide-react"; import { useBodyScrollLock } from "../../hooks/useBodyScrollLock"; import { Swiper, SwiperSlide } from "swiper/react"; import { Keyboard } from "swiper/modules"; import type { Swiper as SwiperType } from "swiper"; import { useGalleryStore } from "../../lib/store"; import { VideoWithLoading } from "./VideoWithLoading"; import { getApiRandomVideos, getDomain } from "../utils/imageUtils"; import type { VideoData } from "../utils/imageUtils"; import "swiper/css"; export default function VideoSlider() { const [currentIndex, setCurrentIndex] = useState(0); const [swiperInstance, setSwiperInstance] = useState(null); const [displayVideos, setDisplayVideos] = useState (getApiRandomVideos(5)); const [videoErrors, setVideoErrors] = useState
The VideoWithLoading
component handles individual video playback, loading states, and user interactions:
tsx// app/components/VideoWithLoading.tsx import { useState, useRef, useEffect } from "react"; import { AlertCircle, Play } from "lucide-react"; import type { VideoData } from "../utils/imageUtils"; interface VideoWithLoadingProps { video: VideoData; onLoad: () => void; onError: () => void; isActive: boolean; onVideoEnded?: () => void; } export const VideoWithLoading = ({ video, onLoad, onError, isActive, onVideoEnded }: VideoWithLoadingProps) => { const [hasError, setHasError] = useState(false); const [status, setStatus] = useState<"idle" | "loading" | "error" | "playing" | "finished">("idle"); const videoRef = useRef(null); // Handle video load completion const handleLoadedData = () => { setStatus("playing"); onLoad(); }; // Handle video load errors const handleError = () => { setHasError(true); setStatus("error"); onError(); }; // Handle video click for play/pause const handleVideoClick = () => { if (!videoRef.current) return; if (videoRef.current.paused) { videoRef.current.play().then(() => { setStatus("playing"); }).catch((err) => { console.log("Playback failed on click:", err); if (err.name === 'NotAllowedError') { setStatus("error"); } else { setHasError(true); onError(); } }); } else { videoRef.current.pause(); setStatus("idle"); } }; // Handle video end const handleEnded = () => { setStatus("finished"); if (onVideoEnded) { onVideoEnded(); } }; // Auto-play or pause based on active state useEffect(() => { const videoElement = videoRef.current; if (!videoElement) return; videoElement.muted = false; videoElement.playsInline = true; // Inline playback on mobile if (isActive) { videoElement.play().then(() => { setStatus("playing"); }).catch((err) => { console.log("Auto-play prevented, user interaction required"); setStatus("idle"); }); } else { videoElement.pause(); setStatus("finished"); } return () => { if (videoElement) { videoElement.pause(); } }; }, [video.url, isActive]); // Fallback UI for error state if (hasError) { return ( ); } return (Video failed to load
{/* Video element */} {/* Loading indicator */} {status === "loading" && (); };)} {/* User interaction prompt */} {status === "idle" && ()}Click to play video
status
variable to clearly manage video states (idle, loading, error, playing, finished).To remember user playback preferences, we use Zustand for state management:
tsx// lib/store.ts import { create } from 'zustand'; import { persist } from 'zustand/middleware'; export type VideoPlayMode = 'random' | 'loop'; interface GalleryState { videoPlayMode: VideoPlayMode; setVideoPlayMode: (mode: VideoPlayMode) => void; } export const useGalleryStore = create()( persist( (set) => ({ videoPlayMode: 'random', // Default to random mode setVideoPlayMode: (mode) => set({ videoPlayMode: mode }), }), { name: 'gallery-storage' } // Local storage key ) );
We also implement utility functions for fetching video data:
tsx// app/utils/imageUtils.ts export interface VideoData { id: number; url: string; title: string; } // Fetch random girl videos from API export const getApiRandomVideos = (count: number = 4) => { const apiUrl = "http://api.mmp.cc/api/ksvideo?type=mp4&id=jk"; return Array.from({ length: count }, (_, index) => ({ id: index, url: `${apiUrl}&t=${Date.now()}-${index}`, // Add timestamp to avoid caching title: `Girl Video ${index + 1}`, })); }; // Extract domain for error handling and blocking export const getDomain = (url: string): string => { try { return new URL(url).hostname; } catch { return "unknown"; } };
These utilities handle data fetching, cache control, and domain extraction, supporting smart error handling.
This app is deployed at https://immersiview.utities.online/ as part of the utility toolkit www.utities.online. You can experience the smooth video browsing app right away!
Deployment is straightforward. Use Vercel, Netlify, or any platform that supports Next.js. Simply connect your code repository, and the platform will automatically build and deploy your app.
In this tutorial, we built an immersive random girl video browser with the following core features:
The app’s core appeal lies in its ability to provide a relaxed and enjoyable browsing experience, with each swipe revealing new and exciting content. As a developer, you can further expand this app by adding social features like likes, favorites, and sharing, or by optimizing the content recommendation algorithm to help users discover their preferred types of content more accurately.
Now that you’ve mastered the key techniques for building immersive video apps, you can apply these skills to more creative projects. Happy coding!