Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions landing/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>GIFit! - Create GIFs from any video</title>
<meta name="description" content="GIFit! is a browser extension that lets you create GIFs from any YouTube video with precision controls. Try the live demo right in your browser." />
<meta name="theme-color" content="#0F0F0F" />
<link rel="icon" type="image/svg+xml" href="/GIFit/favicon.svg" />
</head>
<body>
<div id="root"></div>
<script type="module" src="./src/main.tsx"></script>
</body>
</html>
112 changes: 112 additions & 0 deletions landing/src/DemoSection.module.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
.demoContainer {
display: flex;
flex-direction: column;
align-items: center;
gap: 1.5rem;
}

/* --- Video Selector --- */

.videoSelector {
display: flex;
align-items: center;
gap: 0.75rem;
flex-wrap: wrap;
justify-content: center;
}

.selectorLabel {
font-size: 0.875rem;
color: var(--landing_text_muted);
font-weight: 500;
}

.selectorButtons {
display: flex;
gap: 0.5rem;
}

.selectorButton {
font-family: inherit;
font-size: 0.8125rem;
font-weight: 500;
padding: 0.375rem 0.875rem;
border-radius: 6px;
border: 1px solid var(--landing_border);
background: transparent;
color: var(--landing_text_muted);
cursor: pointer;
transition:
border-color 200ms,
color 200ms,
background 200ms;
}

.selectorButton:hover {
border-color: rgba(255, 255, 255, 0.25);
color: white;
}

.selectorButtonActive {
border-color: var(--landing_accent);
color: white;
background: rgba(235, 10, 30, 0.1);
}

/* --- Video Player --- */

.videoContainer {
position: relative;
width: 100%;
max-width: 800px;
border-radius: 12px;
overflow: hidden;
border: 1px solid var(--landing_border);
background: black;
}

.video {
display: block;
width: 100%;
height: auto;
}

.videoCredit {
position: absolute;
bottom: 0.5rem;
right: 0.75rem;
font-size: 0.6875rem;
color: rgba(255, 255, 255, 0.4);
letter-spacing: 0.02em;
}

/* --- GIFit Editor Embed --- */

.editorContainer {
width: 100%;
max-width: 800px;
min-height: 600px;
border-radius: 12px;
overflow: hidden;
border: 1px solid var(--landing_border);
background: var(--color_popup_background);

/* Match the extension's body sizing */
font-size: 16px;
}

/* --- Loading --- */

.loading {
width: 100%;
max-width: 800px;
min-height: 400px;
border-radius: 12px;
border: 1px solid var(--landing_border);
background: var(--landing_surface);
display: flex;
align-items: center;
justify-content: center;
color: var(--landing_text_muted);
font-size: 0.875rem;
}
112 changes: 112 additions & 0 deletions landing/src/DemoSection.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
import { useMemo, useRef, useState, useCallback, useEffect } from 'react';
import css from './DemoSection.module.css';

import { AppCore } from '@/components/AppCore';
import { StoreProvider } from '@/stores/storeContext';
import { DirectVideoAdapter, DirectGifAdapter, InMemoryStorageAdapter } from '@/adapters/direct';

const DEMO_VIDEOS = [
{
id: 'ocean',
label: 'Ocean Waves',
src: import.meta.env.BASE_URL + 'videos/ocean.mp4',
credit: 'Pexels'
},
{
id: 'city',
label: 'City Traffic',
src: import.meta.env.BASE_URL + 'videos/city.mp4',
credit: 'Pexels'
},
{
id: 'nature',
label: 'Forest Stream',
src: import.meta.env.BASE_URL + 'videos/nature.mp4',
credit: 'Pexels'
}
];

export function DemoSection() {
const videoRef = useRef<HTMLVideoElement>(null);
const [selectedVideo, setSelectedVideo] = useState(DEMO_VIDEOS[0]);
const [videoReady, setVideoReady] = useState(false);

const getVideo = useCallback(() => videoRef.current, []);

const videoAdapter = useMemo(() => new DirectVideoAdapter(getVideo), [getVideo]);
const gifAdapter = useMemo(() => new DirectGifAdapter(getVideo), [getVideo]);
const storageAdapter = useMemo(() => new InMemoryStorageAdapter(), []);

const getVideoTitle = useCallback(async () => {
return selectedVideo.label;
}, [selectedVideo]);

// Reset when video changes
useEffect(() => {
setVideoReady(false);
}, [selectedVideo]);

function handleVideoCanPlay() {
setVideoReady(true);
}

function handleVideoSelect(video: typeof DEMO_VIDEOS[number]) {
setSelectedVideo(video);
}

return (
<div className={css.demoContainer}>
{/* Video Selector */}
<div className={css.videoSelector}>
<span className={css.selectorLabel}>Choose a video:</span>
<div className={css.selectorButtons}>
{DEMO_VIDEOS.map((video) => (
<button
key={video.id}
className={`${css.selectorButton} ${selectedVideo.id === video.id ? css.selectorButtonActive : ''}`}
onClick={() => handleVideoSelect(video)}
type="button">
{video.label}
</button>
))}
</div>
</div>

{/* Video Element (hidden, used by adapters) */}
<div className={css.videoContainer}>
<video
ref={videoRef}
src={selectedVideo.src}
crossOrigin="anonymous"
preload="auto"
muted
playsInline
onCanPlay={handleVideoCanPlay}
className={css.video}
/>
<span className={css.videoCredit}>
Video: {selectedVideo.credit}
</span>
</div>

{/* GIFit Editor */}
{videoReady && (
<div className={css.editorContainer}>
<StoreProvider
key={selectedVideo.id}
videoAdapter={videoAdapter}
gifAdapter={gifAdapter}
storageAdapter={storageAdapter}>
<AppCore getVideoTitle={getVideoTitle} hideFooter />
</StoreProvider>
</div>
)}

{!videoReady && (
<div className={css.loading}>
Loading video...
</div>
)}
</div>
);
}
Loading