add custom video player
This commit is contained in:
parent
0d0f220e07
commit
458b3f0c1b
13 changed files with 1215 additions and 247 deletions
21
components.json
Normal file
21
components.json
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
{
|
||||
"$schema": "https://ui.shadcn.com/schema.json",
|
||||
"style": "default",
|
||||
"rsc": false,
|
||||
"tsx": true,
|
||||
"tailwind": {
|
||||
"config": "tailwind.config.ts",
|
||||
"css": "src/styles/globals.css",
|
||||
"baseColor": "neutral",
|
||||
"cssVariables": false,
|
||||
"prefix": ""
|
||||
},
|
||||
"aliases": {
|
||||
"components": "~/components",
|
||||
"utils": "~/lib/utils",
|
||||
"ui": "~/components/ui",
|
||||
"lib": "~/lib",
|
||||
"hooks": "~/hooks"
|
||||
},
|
||||
"iconLibrary": "lucide"
|
||||
}
|
||||
874
package-lock.json
generated
874
package-lock.json
generated
File diff suppressed because it is too large
Load diff
|
|
@ -25,6 +25,7 @@
|
|||
"@popperjs/core": "^2.11.7",
|
||||
"@prisma/client": "^4.11.0",
|
||||
"@radix-ui/react-icons": "^1.3.0",
|
||||
"@radix-ui/react-slider": "^1.2.1",
|
||||
"@radix-ui/react-tooltip": "^1.0.5",
|
||||
"@tanstack/react-query": "^4.28.0",
|
||||
"@trpc/client": "^10.18.0",
|
||||
|
|
@ -35,12 +36,16 @@
|
|||
"@upstash/qstash": "^0.3.6",
|
||||
"@upstash/ratelimit": "^0.4.2",
|
||||
"@upstash/redis": "^1.20.4",
|
||||
"@vidstack/react": "^1.12.12",
|
||||
"axios": "^1.3.5",
|
||||
"class-variance-authority": "^0.7.0",
|
||||
"clsx": "^2.1.1",
|
||||
"crisp-sdk-web": "^1.0.18",
|
||||
"dayjs": "^1.11.7",
|
||||
"ebml": "^3.0.0",
|
||||
"file-saver": "^2.0.5",
|
||||
"jotai": "^2.0.4",
|
||||
"lucide-react": "^0.460.0",
|
||||
"micro": "^10.0.1",
|
||||
"micro-cors": "^0.1.1",
|
||||
"next": "^13.4.10",
|
||||
|
|
@ -55,6 +60,8 @@
|
|||
"sharp": "^0.32.3",
|
||||
"stripe": "^12.12.0",
|
||||
"superjson": "1.12.2",
|
||||
"tailwind-merge": "^2.5.4",
|
||||
"tailwindcss-animate": "^1.0.7",
|
||||
"tailwindcss-radix": "^2.8.0",
|
||||
"ts-ebml": "^2.0.2",
|
||||
"zod": "^3.21.4"
|
||||
|
|
|
|||
73
src/components/VideoLayout.tsx
Normal file
73
src/components/VideoLayout.tsx
Normal file
|
|
@ -0,0 +1,73 @@
|
|||
import * as Tooltip from "@radix-ui/react-tooltip";
|
||||
import { Controls, Gesture } from "@vidstack/react";
|
||||
|
||||
import * as Buttons from "./buttons";
|
||||
import * as Sliders from "./sliders";
|
||||
import { TimeGroup } from "./time-group";
|
||||
|
||||
// Offset tooltips/menus/slider previews in the lower controls group so they're clearly visible.
|
||||
const popupOffset = 30;
|
||||
|
||||
export interface VideoLayoutProps {
|
||||
thumbnails?: string;
|
||||
}
|
||||
|
||||
export function VideoLayout({ thumbnails }: VideoLayoutProps) {
|
||||
return (
|
||||
<>
|
||||
<Gestures />
|
||||
<Controls.Root
|
||||
hideDelay={50}
|
||||
hideOnMouseLeave={true}
|
||||
className="absolute inset-0 z-10 flex h-full w-full flex-col bg-gradient-to-t from-black/10 to-transparent opacity-0 transition-opacity duration-300 media-controls:opacity-100"
|
||||
>
|
||||
<Tooltip.Provider>
|
||||
<div className="flex-1" />
|
||||
<Controls.Group className="flex w-full items-center px-2">
|
||||
<Sliders.Time thumbnails={thumbnails} />
|
||||
</Controls.Group>
|
||||
<Controls.Group className="-mt-0.5 flex w-full items-center px-2 pb-2">
|
||||
<Buttons.Play tooltipAlign="start" tooltipOffset={popupOffset} />
|
||||
<Buttons.Mute tooltipOffset={popupOffset} />
|
||||
<Sliders.Volume />
|
||||
<TimeGroup />
|
||||
<div className="flex-1" />
|
||||
<Buttons.PIP tooltipOffset={popupOffset} />
|
||||
<Buttons.Fullscreen
|
||||
tooltipAlign="end"
|
||||
tooltipOffset={popupOffset}
|
||||
/>
|
||||
</Controls.Group>
|
||||
<div className="controlsShadow absolute bottom-0 z-[-1] h-full w-full" />
|
||||
</Tooltip.Provider>
|
||||
</Controls.Root>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function Gestures() {
|
||||
return (
|
||||
<>
|
||||
<Gesture
|
||||
className="absolute inset-0 z-0 block h-full w-full"
|
||||
event="pointerup"
|
||||
action="toggle:paused"
|
||||
/>
|
||||
<Gesture
|
||||
className="absolute inset-0 z-0 block h-full w-full"
|
||||
event="dblpointerup"
|
||||
action="toggle:fullscreen"
|
||||
/>
|
||||
<Gesture
|
||||
className="absolute left-0 top-0 z-10 block h-full w-1/5"
|
||||
event="dblpointerup"
|
||||
action="seek:-10"
|
||||
/>
|
||||
<Gesture
|
||||
className="absolute right-0 top-0 z-10 block h-full w-1/5"
|
||||
event="dblpointerup"
|
||||
action="seek:10"
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
81
src/components/VideoPlayer.tsx
Normal file
81
src/components/VideoPlayer.tsx
Normal file
|
|
@ -0,0 +1,81 @@
|
|||
import "@vidstack/react/player/styles/base.css";
|
||||
|
||||
import React, { useEffect, useRef } from "react";
|
||||
|
||||
import {
|
||||
isHLSProvider,
|
||||
MediaPlayer,
|
||||
MediaProvider,
|
||||
Poster,
|
||||
type MediaCanPlayDetail,
|
||||
type MediaCanPlayEvent,
|
||||
type MediaPlayerInstance,
|
||||
type MediaProviderAdapter,
|
||||
type MediaProviderChangeEvent,
|
||||
} from "@vidstack/react";
|
||||
|
||||
import { VideoLayout } from "./VideoLayout";
|
||||
|
||||
interface Props {
|
||||
video_url: string;
|
||||
thumbnailUrl: string;
|
||||
}
|
||||
|
||||
export default function VideoPlayer({ video_url, thumbnailUrl }: Props) {
|
||||
let player = useRef<MediaPlayerInstance>(null);
|
||||
|
||||
useEffect(() => {
|
||||
// Subscribe to state updates.
|
||||
return player.current!.subscribe(({ paused, viewType }) => {
|
||||
// console.log('is paused?', '->', state.paused);
|
||||
// console.log('is audio view?', '->', state.viewType === 'audio');
|
||||
});
|
||||
}, []);
|
||||
|
||||
function onProviderChange(
|
||||
provider: MediaProviderAdapter | null,
|
||||
nativeEvent: MediaProviderChangeEvent
|
||||
) {
|
||||
// We can configure provider's here.
|
||||
if (isHLSProvider(provider)) {
|
||||
provider.config = {};
|
||||
}
|
||||
}
|
||||
|
||||
// We can listen for the `can-play` event to be notified when the player is ready.
|
||||
function onCanPlay(
|
||||
detail: MediaCanPlayDetail,
|
||||
nativeEvent: MediaCanPlayEvent
|
||||
) {
|
||||
// ...
|
||||
}
|
||||
|
||||
return (
|
||||
<MediaPlayer
|
||||
className="ring-media-focus aspect-video w-full overflow-hidden rounded-md bg-slate-900 font-sans text-white data-[focus]:ring-4"
|
||||
title="Sprite Fight"
|
||||
src={[
|
||||
{
|
||||
src: video_url,
|
||||
type: "video/mp4",
|
||||
},
|
||||
]}
|
||||
crossOrigin
|
||||
playsInline
|
||||
onProviderChange={onProviderChange}
|
||||
onCanPlay={onCanPlay}
|
||||
ref={player}
|
||||
keyTarget="document"
|
||||
>
|
||||
<MediaProvider>
|
||||
<Poster
|
||||
className="absolute inset-0 block h-full w-full rounded-md object-cover opacity-0 transition-opacity data-[visible]:opacity-100"
|
||||
src={thumbnailUrl}
|
||||
alt="Girl walks into campfire with gnomes surrounding her friend ready for their next meal!"
|
||||
/>
|
||||
</MediaProvider>
|
||||
|
||||
<VideoLayout />
|
||||
</MediaPlayer>
|
||||
);
|
||||
}
|
||||
150
src/components/buttons.tsx
Normal file
150
src/components/buttons.tsx
Normal file
|
|
@ -0,0 +1,150 @@
|
|||
import * as Tooltip from "@radix-ui/react-tooltip";
|
||||
import {
|
||||
FullscreenButton,
|
||||
MuteButton,
|
||||
PIPButton,
|
||||
PlayButton,
|
||||
useMediaState,
|
||||
} from "@vidstack/react";
|
||||
import {
|
||||
Minimize as FullscreenExitIcon,
|
||||
Maximize as FullscreenIcon,
|
||||
VolumeX as MuteIcon,
|
||||
PauseIcon,
|
||||
PictureInPictureIcon as PictureInPictureExitIcon,
|
||||
PictureInPicture2 as PictureInPictureIcon,
|
||||
PlayIcon,
|
||||
Volume2 as VolumeHighIcon,
|
||||
Volume1 as VolumeLowIcon,
|
||||
} from "lucide-react";
|
||||
|
||||
export interface MediaButtonProps {
|
||||
tooltipSide?: Tooltip.TooltipContentProps["side"];
|
||||
tooltipAlign?: Tooltip.TooltipContentProps["align"];
|
||||
tooltipOffset?: number;
|
||||
}
|
||||
|
||||
export const buttonClass =
|
||||
"group ring-media-focus relative inline-flex h-10 w-10 cursor-pointer items-center justify-center rounded-md outline-none ring-inset hover:bg-white/20 focus-visible:ring-4 aria-disabled:hidden";
|
||||
|
||||
export const tooltipClass =
|
||||
"animate-out fade-out slide-out-to-bottom-2 data-[state=delayed-open]:animate-in data-[state=delayed-open]:fade-in data-[state=delayed-open]:slide-in-from-bottom-4 z-10 rounded-sm bg-black/90 px-2 py-0.5 text-sm font-medium text-white parent-data-[open]:hidden";
|
||||
|
||||
export function Play({
|
||||
tooltipOffset = 0,
|
||||
tooltipSide = "top",
|
||||
tooltipAlign = "center",
|
||||
}: MediaButtonProps) {
|
||||
const isPaused = useMediaState("paused");
|
||||
return (
|
||||
<Tooltip.Root>
|
||||
<Tooltip.Trigger asChild>
|
||||
<PlayButton className={buttonClass}>
|
||||
{isPaused ? (
|
||||
<PlayIcon className="h-7 w-7 translate-x-px" />
|
||||
) : (
|
||||
<PauseIcon className="h-7 w-7" />
|
||||
)}
|
||||
</PlayButton>
|
||||
</Tooltip.Trigger>
|
||||
<Tooltip.Content
|
||||
className={tooltipClass}
|
||||
side={tooltipSide}
|
||||
align={tooltipAlign}
|
||||
sideOffset={tooltipOffset}
|
||||
>
|
||||
{isPaused ? "Play" : "Pause"}
|
||||
</Tooltip.Content>
|
||||
</Tooltip.Root>
|
||||
);
|
||||
}
|
||||
|
||||
export function Mute({
|
||||
tooltipOffset = 0,
|
||||
tooltipSide = "top",
|
||||
tooltipAlign = "center",
|
||||
}: MediaButtonProps) {
|
||||
const volume = useMediaState("volume"),
|
||||
isMuted = useMediaState("muted");
|
||||
return (
|
||||
<Tooltip.Root>
|
||||
<Tooltip.Trigger asChild>
|
||||
<MuteButton className={buttonClass}>
|
||||
{isMuted || volume == 0 ? (
|
||||
<MuteIcon className="h-7 w-7" />
|
||||
) : volume < 0.5 ? (
|
||||
<VolumeLowIcon className="h-7 w-7" />
|
||||
) : (
|
||||
<VolumeHighIcon className="h-7 w-7" />
|
||||
)}
|
||||
</MuteButton>
|
||||
</Tooltip.Trigger>
|
||||
<Tooltip.Content
|
||||
className={tooltipClass}
|
||||
side={tooltipSide}
|
||||
align={tooltipAlign}
|
||||
sideOffset={tooltipOffset}
|
||||
>
|
||||
{isMuted ? "Unmute" : "Mute"}
|
||||
</Tooltip.Content>
|
||||
</Tooltip.Root>
|
||||
);
|
||||
}
|
||||
|
||||
export function PIP({
|
||||
tooltipOffset = 0,
|
||||
tooltipSide = "top",
|
||||
tooltipAlign = "center",
|
||||
}: MediaButtonProps) {
|
||||
const isActive = useMediaState("pictureInPicture");
|
||||
return (
|
||||
<Tooltip.Root>
|
||||
<Tooltip.Trigger asChild>
|
||||
<PIPButton className={buttonClass}>
|
||||
{isActive ? (
|
||||
<PictureInPictureExitIcon className="h-7 w-7" />
|
||||
) : (
|
||||
<PictureInPictureIcon className="h-7 w-7" />
|
||||
)}
|
||||
</PIPButton>
|
||||
</Tooltip.Trigger>
|
||||
<Tooltip.Content
|
||||
className={tooltipClass}
|
||||
side={tooltipSide}
|
||||
align={tooltipAlign}
|
||||
sideOffset={tooltipOffset}
|
||||
>
|
||||
{isActive ? "Exit PIP" : "Enter PIP"}
|
||||
</Tooltip.Content>
|
||||
</Tooltip.Root>
|
||||
);
|
||||
}
|
||||
|
||||
export function Fullscreen({
|
||||
tooltipOffset = 0,
|
||||
tooltipSide = "top",
|
||||
tooltipAlign = "center",
|
||||
}: MediaButtonProps) {
|
||||
const isActive = useMediaState("fullscreen");
|
||||
return (
|
||||
<Tooltip.Root>
|
||||
<Tooltip.Trigger asChild>
|
||||
<FullscreenButton className={buttonClass}>
|
||||
{isActive ? (
|
||||
<FullscreenExitIcon className="h-7 w-7" />
|
||||
) : (
|
||||
<FullscreenIcon className="h-7 w-7" />
|
||||
)}
|
||||
</FullscreenButton>
|
||||
</Tooltip.Trigger>
|
||||
<Tooltip.Content
|
||||
className={tooltipClass}
|
||||
side={tooltipSide}
|
||||
align={tooltipAlign}
|
||||
sideOffset={tooltipOffset}
|
||||
>
|
||||
{isActive ? "Exit Fullscreen" : "Enter Fullscreen"}
|
||||
</Tooltip.Content>
|
||||
</Tooltip.Root>
|
||||
);
|
||||
}
|
||||
105
src/components/sliders.tsx
Normal file
105
src/components/sliders.tsx
Normal file
|
|
@ -0,0 +1,105 @@
|
|||
import { useEffect, useState } from "react";
|
||||
|
||||
import {
|
||||
formatTime,
|
||||
Thumbnail,
|
||||
useMediaRemote,
|
||||
useMediaState,
|
||||
useSliderPreview,
|
||||
} from "@vidstack/react";
|
||||
import * as Slider from "@radix-ui/react-slider";
|
||||
|
||||
export function Volume() {
|
||||
const volume = useMediaState("volume"),
|
||||
canSetVolume = useMediaState("canSetVolume"),
|
||||
remote = useMediaRemote();
|
||||
|
||||
if (!canSetVolume) return null;
|
||||
|
||||
return (
|
||||
<Slider.Root
|
||||
className="group relative inline-flex h-10 w-full max-w-[80px] cursor-pointer touch-none select-none items-center outline-none"
|
||||
value={[volume * 100]}
|
||||
onValueChange={([value]) => {
|
||||
if (value) remote.changeVolume(value / 100);
|
||||
}}
|
||||
>
|
||||
<Slider.Track className="relative h-[5px] w-full rounded-sm bg-white/30">
|
||||
<Slider.Range className="absolute h-full rounded-sm bg-[#f5f5f5] will-change-[width]" />
|
||||
</Slider.Track>
|
||||
<Slider.Thumb
|
||||
aria-label="Volume"
|
||||
className="block h-[15px] w-[15px] rounded-full border border-[#cacaca] bg-white opacity-0 outline-none ring-white/40 transition-opacity will-change-[left] focus:opacity-100 focus:ring-4 data-[hocus]:opacity-100"
|
||||
/>
|
||||
</Slider.Root>
|
||||
);
|
||||
}
|
||||
|
||||
export interface TimeSliderProps {
|
||||
thumbnails?: string;
|
||||
}
|
||||
|
||||
export function Time({ thumbnails }: TimeSliderProps) {
|
||||
const time = useMediaState("currentTime"),
|
||||
canSeek = useMediaState("canSeek"),
|
||||
duration = useMediaState("duration"),
|
||||
seeking = useMediaState("seeking"),
|
||||
remote = useMediaRemote(),
|
||||
[value, setValue] = useState(0),
|
||||
{ previewRootRef, previewRef, previewValue } = useSliderPreview({
|
||||
clamp: true,
|
||||
offset: 6,
|
||||
orientation: "horizontal",
|
||||
}),
|
||||
previewTime = (previewValue / 100) * duration;
|
||||
|
||||
// Keep slider value in-sync with playback.
|
||||
useEffect(() => {
|
||||
if (seeking) return;
|
||||
setValue((time / duration) * 100);
|
||||
}, [time, duration]);
|
||||
|
||||
return (
|
||||
<Slider.Root
|
||||
className="group relative inline-flex h-9 w-full cursor-pointer touch-none select-none items-center outline-none"
|
||||
value={[value]}
|
||||
disabled={!canSeek}
|
||||
ref={previewRootRef}
|
||||
onValueChange={([value]) => {
|
||||
if (value) {
|
||||
setValue(value);
|
||||
remote.seeking((value / 100) * duration);
|
||||
}
|
||||
}}
|
||||
onValueCommit={([value]) => {
|
||||
if (value) remote.seek((value / 100) * duration);
|
||||
}}
|
||||
>
|
||||
<Slider.Track className="relative h-[5px] w-full rounded-sm bg-white/30">
|
||||
<Slider.Range className="absolute h-full rounded-sm bg-[#f5f5f5] will-change-[width]" />
|
||||
</Slider.Track>
|
||||
|
||||
<Slider.Thumb
|
||||
aria-label="Current Time"
|
||||
className="block h-[15px] w-[15px] rounded-full border border-[#cacaca] bg-white opacity-0 outline-none ring-white/40 transition-opacity will-change-[left] focus:opacity-100 focus:ring-4 group-hocus:opacity-100"
|
||||
/>
|
||||
|
||||
{/* Preview */}
|
||||
<div
|
||||
className="pointer-events-none absolute flex flex-col items-center opacity-0 transition-opacity will-change-[left] duration-200 data-[visible]:opacity-100"
|
||||
ref={previewRef}
|
||||
>
|
||||
{thumbnails ? (
|
||||
<Thumbnail.Root
|
||||
src={thumbnails}
|
||||
time={previewTime}
|
||||
className="mb-2 block h-[var(--thumbnail-height)] max-h-[160px] min-h-[80px] w-[var(--thumbnail-width)] min-w-[120px] max-w-[180px] overflow-hidden border border-white bg-black"
|
||||
>
|
||||
<Thumbnail.Img />
|
||||
</Thumbnail.Root>
|
||||
) : null}
|
||||
<span className="text-[13px]">{formatTime(previewTime)}</span>
|
||||
</div>
|
||||
</Slider.Root>
|
||||
);
|
||||
}
|
||||
11
src/components/time-group.tsx
Normal file
11
src/components/time-group.tsx
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
import { Time } from "@vidstack/react";
|
||||
|
||||
export function TimeGroup() {
|
||||
return (
|
||||
<div className="ml-2.5 flex items-center text-sm font-medium">
|
||||
<Time className="time" type="current" />
|
||||
<div className="mx-1 text-white/80">/</div>
|
||||
<Time className="time" type="duration" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
10
src/components/title.tsx
Normal file
10
src/components/title.tsx
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
import { ChapterTitle } from "@vidstack/react";
|
||||
|
||||
export function Title() {
|
||||
return (
|
||||
<span className="inline-block flex-1 overflow-hidden text-ellipsis whitespace-nowrap px-2 text-sm font-medium text-white/70">
|
||||
<span className="mr-1">|</span>
|
||||
<ChapterTitle />
|
||||
</span>
|
||||
);
|
||||
}
|
||||
6
src/lib/utils.ts
Normal file
6
src/lib/utils.ts
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
import { clsx, type ClassValue } from "clsx"
|
||||
import { twMerge } from "tailwind-merge"
|
||||
|
||||
export function cn(...inputs: ClassValue[]) {
|
||||
return twMerge(clsx(inputs))
|
||||
}
|
||||
|
|
@ -15,6 +15,7 @@ import { useAtom } from "jotai";
|
|||
import recordVideoModalOpen from "~/atoms/recordVideoModalOpen";
|
||||
import VideoRecordModal from "~/components/VideoRecordModal";
|
||||
import defaultProfileIcon from "~/assets/default profile icon.jpg";
|
||||
import VideoPlayer from "~/components/VideoPlayer";
|
||||
|
||||
const VideoList: NextPage = () => {
|
||||
const router = useRouter();
|
||||
|
|
@ -133,38 +134,42 @@ const VideoList: NextPage = () => {
|
|||
<div className="flex aspect-video max-h-[calc(100vh_-_169px)] w-full justify-center bg-black 2xl:max-h-[1160px]">
|
||||
{video?.video_url && (
|
||||
<>
|
||||
<video
|
||||
controls
|
||||
onPlay={() =>
|
||||
posthog?.capture("play video", {
|
||||
videoId: video.id,
|
||||
videoCreatedAt: video.createdAt,
|
||||
videoUpdatedAt: video.updatedAt,
|
||||
videoUser: video.user.id,
|
||||
videoSharing: video.sharing,
|
||||
videoDeleteAfterLinkExpires:
|
||||
video.delete_after_link_expires,
|
||||
videoShareLinkExpiresAt: video.shareLinkExpiresAt,
|
||||
})
|
||||
}
|
||||
onPause={() =>
|
||||
posthog?.capture("pause video", {
|
||||
videoId: video.id,
|
||||
videoCreatedAt: video.createdAt,
|
||||
videoUpdatedAt: video.updatedAt,
|
||||
videoUser: video.user.id,
|
||||
videoSharing: video.sharing,
|
||||
videoDeleteAfterLinkExpires:
|
||||
video.delete_after_link_expires,
|
||||
videoShareLinkExpiresAt: video.shareLinkExpiresAt,
|
||||
})
|
||||
}
|
||||
className="h-full w-full"
|
||||
controlsList="nodownload"
|
||||
>
|
||||
<source src={video.video_url} />
|
||||
Your browser does not support the video tag.
|
||||
</video>
|
||||
<VideoPlayer
|
||||
thumbnailUrl={video.thumbnailUrl}
|
||||
video_url={video.video_url}
|
||||
/>
|
||||
{/*<video*/}
|
||||
{/* controls*/}
|
||||
{/* onPlay={() =>*/}
|
||||
{/* posthog?.capture("play video", {*/}
|
||||
{/* videoId: video.id,*/}
|
||||
{/* videoCreatedAt: video.createdAt,*/}
|
||||
{/* videoUpdatedAt: video.updatedAt,*/}
|
||||
{/* videoUser: video.user.id,*/}
|
||||
{/* videoSharing: video.sharing,*/}
|
||||
{/* videoDeleteAfterLinkExpires:*/}
|
||||
{/* video.delete_after_link_expires,*/}
|
||||
{/* videoShareLinkExpiresAt: video.shareLinkExpiresAt,*/}
|
||||
{/* })*/}
|
||||
{/* }*/}
|
||||
{/* onPause={() =>*/}
|
||||
{/* posthog?.capture("pause video", {*/}
|
||||
{/* videoId: video.id,*/}
|
||||
{/* videoCreatedAt: video.createdAt,*/}
|
||||
{/* videoUpdatedAt: video.updatedAt,*/}
|
||||
{/* videoUser: video.user.id,*/}
|
||||
{/* videoSharing: video.sharing,*/}
|
||||
{/* videoDeleteAfterLinkExpires:*/}
|
||||
{/* video.delete_after_link_expires,*/}
|
||||
{/* videoShareLinkExpiresAt: video.shareLinkExpiresAt,*/}
|
||||
{/* })*/}
|
||||
{/* }*/}
|
||||
{/* className="h-full w-full"*/}
|
||||
{/* controlsList="nodownload"*/}
|
||||
{/*>*/}
|
||||
{/* <source src={video.video_url} />*/}
|
||||
{/* Your browser does not support the video tag.*/}
|
||||
{/*</video>*/}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,3 +1,12 @@
|
|||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
@layer base {
|
||||
:root {
|
||||
--radius: 0.5rem
|
||||
}
|
||||
}
|
||||
|
||||
.controlsShadow {
|
||||
box-shadow: inset 0 calc(-9 * clamp(8px, 14px, 11px)) calc(3 * clamp(8px, 14px, 11px)) calc(-6 * clamp(8px, 14px, 11px)) hsl(0 0% 0% / 0.35), inset 0 calc(-18 * clamp(8px, 14px, 11px)) calc(6 * clamp(8px, 14px, 11px)) calc(-12 * clamp(8px, 14px, 11px)) hsl(0 0% 0% / 0.35), inset 0 calc(-27 * clamp(8px, 14px, 11px)) calc(9 * clamp(8px, 14px, 11px)) calc(-18 * clamp(8px, 14px, 11px)) hsl(0 0% 0% / 0.35)
|
||||
}
|
||||
|
|
@ -1,6 +1,7 @@
|
|||
import { type Config } from "tailwindcss";
|
||||
|
||||
export default {
|
||||
darkMode: ["class"],
|
||||
content: ["./src/**/*.{js,ts,jsx,tsx}"],
|
||||
theme: {
|
||||
extend: {
|
||||
|
|
@ -10,15 +11,50 @@ export default {
|
|||
},
|
||||
keyframes: {
|
||||
marquee: {
|
||||
"0%": { transform: "translateX(0%)" },
|
||||
"100%": { transform: "translateX(-100%)" },
|
||||
"0%": {
|
||||
transform: "translateX(0%)",
|
||||
},
|
||||
"100%": {
|
||||
transform: "translateX(-100%)",
|
||||
},
|
||||
},
|
||||
marquee2: {
|
||||
"0%": { transform: "translateX(100%)" },
|
||||
"100%": { transform: "translateX(0%)" },
|
||||
"0%": {
|
||||
transform: "translateX(100%)",
|
||||
},
|
||||
"100%": {
|
||||
transform: "translateX(0%)",
|
||||
},
|
||||
},
|
||||
},
|
||||
borderRadius: {
|
||||
lg: "var(--radius)",
|
||||
md: "calc(var(--radius) - 2px)",
|
||||
sm: "calc(var(--radius) - 4px)",
|
||||
},
|
||||
plugins: [require("tailwindcss-radix")()],
|
||||
colors: {},
|
||||
},
|
||||
},
|
||||
plugins: [
|
||||
require("tailwindcss-radix")(),
|
||||
require("tailwindcss-animate"),
|
||||
require("@vidstack/react/tailwind.cjs")({
|
||||
prefix: "media",
|
||||
}),
|
||||
customVariants,
|
||||
],
|
||||
} satisfies Config;
|
||||
|
||||
function customVariants({
|
||||
addVariant,
|
||||
matchVariant,
|
||||
}: {
|
||||
addVariant: any;
|
||||
matchVariant: any;
|
||||
}) {
|
||||
// Strict version of `.group` to help with nesting.
|
||||
matchVariant("parent-data", (value: any) => `.parent[data-${value}] > &`);
|
||||
|
||||
addVariant("hocus", ["&:hover", "&:focus-visible"]);
|
||||
addVariant("group-hocus", [".group:hover &", ".group:focus-visible &"]);
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue