Skip to content
Merged
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
20 changes: 15 additions & 5 deletions app/(auth)/verify-email/page.tsx
Original file line number Diff line number Diff line change
@@ -1,17 +1,18 @@
"use client";

import { useEffect, useState } from "react";
import { useRouter, useSearchParams } from "next/navigation";
import { useSearchParams } from "next/navigation";
import { MailCheck, XCircle } from "lucide-react";
import { useAuth } from "@/context/auth-context";
import { resendVerificationEmail, verifyEmail } from "@/lib/auth/api";
import { getApiErrorMessage } from "@/lib/api-error";
import { Spinner } from "@/components/ui/spinner";
import { Button } from "@/components/ui/button";

const SESSION_SYNC_RETRY_DELAYS_MS = [150, 300, 600];

export default function VerifyEmailPage() {
const params = useSearchParams();
const router = useRouter();
const { refetchUser } = useAuth();
const token = params.get("token");

Expand All @@ -33,10 +34,19 @@ export default function VerifyEmailPage() {
void (async () => {
try {
await verifyEmail(token);
await refetchUser();
if (!isMounted) return;
setStatus("success");
redirectTimeout = setTimeout(() => router.replace("/explore"), 2500);

for (const delay of SESSION_SYNC_RETRY_DELAYS_MS) {
try {
await refetchUser();
break;
} catch {
await new Promise((resolve) => setTimeout(resolve, delay));
}
}

redirectTimeout = setTimeout(() => window.location.replace("/explore"), 1200);
} catch (error) {
if (!isMounted) return;
setStatus("error");
Expand All @@ -48,7 +58,7 @@ export default function VerifyEmailPage() {
isMounted = false;
if (redirectTimeout) clearTimeout(redirectTimeout);
};
}, [refetchUser, router, token]);
}, [refetchUser, token]);

const handleResend = async () => {
if (!resendEmail || resendStatus === "sending" || resendStatus === "sent") return;
Expand Down
15 changes: 12 additions & 3 deletions app/(feed)/explore/explore-page-client.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
"use client";

import { useCallback, useEffect, useState } from "react";
import { useSearchParams } from "next/navigation";
import { usePathname, useRouter, useSearchParams } from "next/navigation";
import { VideoCard } from "@/components/app/video/video-card";
import { getVideos, type VideoDetails } from "@/lib/video";
import { Spinner } from "@/components/ui/spinner";
Expand Down Expand Up @@ -45,6 +45,8 @@ export default function ExplorePageClient({
initialTotalPages,
initialError,
}: ExplorePageClientProps) {
const router = useRouter();
const pathname = usePathname();
const params = useSearchParams();

const shouldFetchInit = Boolean(initialError);
Expand Down Expand Up @@ -111,8 +113,14 @@ export default function ExplorePageClient({
}, [fetchVideos, shouldFetchInit]);

useEffect(() => {
setIsPopupOpen(params.has("popup"));
}, [params]);
if (!params.has("popup")) return;

const nextParams = new URLSearchParams(params.toString());
nextParams.delete("popup");

const nextUrl = nextParams.toString() ? `${pathname}?${nextParams.toString()}` : pathname;
router.replace(nextUrl, { scroll: false });
}, [params, pathname, router]);

const loadMore = useCallback(() => {
if (isLoading || isLoadingMore || !hasMore) return;
Expand Down Expand Up @@ -151,6 +159,7 @@ export default function ExplorePageClient({
<VideoCard
key={video.id}
thumbnailUrl={video.thumbnailUrl}
durationSeconds={video.duration}
title={video.title}
displayName={video.user?.displayName || video.user?.username}
meta={`${video.viewCount} views • ${new Date(video.createdAt).toLocaleDateString()}`}
Expand Down
11 changes: 7 additions & 4 deletions app/api/auth/logout/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -79,10 +79,13 @@ async function revokeCurrentBackendSession(token: string) {
let revokeResponse: Response;

try {
revokeResponse = await fetch(`${apiBaseUrl}/auth/sessions/${encodeURIComponent(currentSession.id)}`, {
method: "DELETE",
headers: authHeaders,
});
revokeResponse = await fetch(
`${apiBaseUrl}/auth/sessions/${encodeURIComponent(currentSession.id)}`,
{
method: "DELETE",
headers: authHeaders,
},
);
} catch {
return {
ok: false,
Expand Down
1 change: 1 addition & 0 deletions app/channel/[username]/channel-page-client.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -390,6 +390,7 @@ export default function ChannelPageClient({
<VideoCard
key={v.id}
thumbnailUrl={v.thumbnailUrl}
durationSeconds={v.duration}
title={v.title}
displayName={user.displayName || user.username}
meta={meta}
Expand Down
1 change: 1 addition & 0 deletions components/app/search/search-results-list.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ export function SearchResultsList({ results }: SearchResultsListProps) {
<VideoCard
key={`video-${video.id}`}
thumbnailUrl={video.thumbnailUrl}
durationSeconds={video.duration}
title={video.title}
displayName={video.user?.displayName || video.user?.username}
meta={`${video.viewCount} views - ${new Date(video.createdAt).toLocaleDateString()}`}
Expand Down
3 changes: 2 additions & 1 deletion components/app/video/comments.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -345,7 +345,8 @@ export function Comments({ videoId, initialComments, allowComments = true }: Com
) : isUnavailable && !user ? (
<div className="mb-8 flex flex-col items-center gap-4 p-4 text-center">
<p className="text-sm text-muted-foreground">
{errorMessage ?? "Authentication is temporarily unavailable, so posting comments is disabled."}
{errorMessage ??
"Authentication is temporarily unavailable, so posting comments is disabled."}
</p>
<Link href={buildServiceUnavailableHref(`/video/${videoId}#comments`)}>
<Button variant="outline" className="rounded-full px-4 py-2 text-sm font-semibold">
Expand Down
1 change: 1 addition & 0 deletions components/app/video/related-videos.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ export function RelatedVideos({
<VideoCard
key={video.id}
thumbnailUrl={video.thumbnailUrl}
durationSeconds={video.duration}
title={video.title}
displayName={video.user?.displayName || video.user?.username}
meta={`${video.viewCount} views • ${new Date(video.createdAt).toLocaleDateString()}`}
Expand Down
51 changes: 23 additions & 28 deletions components/app/video/video-card.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,11 @@ import Link from "next/link";
import { useRouter } from "next/navigation";
import type { ReactNode } from "react";
import { Badge } from "@/components/ui/badge";
import { formatVideoDuration } from "@/lib/time";

type VideoCardProps = {
thumbnailUrl: string | null;
durationSeconds?: number | null;
title: string;
displayName?: string | null;
meta?: string;
Expand All @@ -24,6 +26,7 @@ type VideoCardProps = {

export function VideoCard({
thumbnailUrl,
durationSeconds,
title,
displayName,
meta,
Expand All @@ -39,6 +42,10 @@ export function VideoCard({
const isGrid = variant === "grid";
const isLarge = variant === "listLarge";
const imgSrc = thumbnailUrl ?? undefined;
const durationLabel =
typeof durationSeconds === "number" && durationSeconds > 0
? formatVideoDuration(durationSeconds)
: null;

const router = useRouter();

Expand All @@ -50,7 +57,6 @@ export function VideoCard({
const resolvedHref = href?.trim();
if (resolvedHref) {
return (
// use div with click handler instead of Link to avoid nested anchors
<div
className={classes}
role="link"
Expand All @@ -67,14 +73,21 @@ export function VideoCard({
return <div className={classes}>{children}</div>;
};

const ImageBlock = (
<div className="relative w-full aspect-video overflow-hidden rounded-xl">
const renderThumbnail = (classes: string) => (
<div className={classes}>
{overlayCenter && <div className="absolute inset-0 z-10">{overlayCenter}</div>}
{overlayTopRight && <div className="absolute top-1 right-1 z-20">{overlayTopRight}</div>}
{overlayTopLeft && <div className="absolute top-1 left-1 z-20">{overlayTopLeft}</div>}
{overlayBottomLeft && (
<div className="absolute bottom-1 left-1 z-20">{overlayBottomLeft}</div>
)}
{durationLabel && (
<div className="pointer-events-none absolute right-2 bottom-2 z-20">
<span className="inline-flex items-center rounded-sm bg-background/80 px-2 py-1 text-xs font-semibold leading-none">
{durationLabel}
</span>
</div>
)}

{imgSrc ? (
<Image
Expand All @@ -92,7 +105,7 @@ export function VideoCard({
if (isGrid) {
return cardWrapper(
<>
{ImageBlock}
{renderThumbnail("relative w-full aspect-video overflow-hidden rounded-xl")}

<div className="flex flex-1 flex-col gap-2 p-3">
<h3 className="font-semibold text-foreground line-clamp-2">{title}</h3>
Expand Down Expand Up @@ -137,18 +150,9 @@ export function VideoCard({
if (isLarge) {
return cardWrapper(
<>
<div className="relative w-full aspect-video sm:w-72 sm:h-40 overflow-hidden rounded-xl shrink-0">
{imgSrc ? (
<Image
src={imgSrc}
alt={title}
fill
className="object-cover transition-transform duration-200 group-hover:scale-[1.02]"
/>
) : (
<div className="h-full w-full bg-muted" />
)}
</div>
{renderThumbnail(
"relative w-full aspect-video overflow-hidden rounded-xl shrink-0 sm:h-40 sm:w-72",
)}

<div className="flex flex-1 flex-col gap-2 sm:gap-1 p-3 sm:py-2">
<h3 className="font-semibold text-xl line-clamp-2">{title}</h3>
Expand All @@ -166,18 +170,9 @@ export function VideoCard({

return cardWrapper(
<>
<div className="relative w-full aspect-video sm:w-45 sm:h-25 overflow-hidden rounded-xl sm:rounded-lg shrink-0">
{imgSrc ? (
<Image
src={imgSrc}
alt={title}
fill
className="object-cover transition-transform duration-200 group-hover:scale-[1.02]"
/>
) : (
<div className="h-full w-full bg-muted" />
)}
</div>
{renderThumbnail(
"relative w-full aspect-video overflow-hidden rounded-xl shrink-0 sm:h-25 sm:w-45 sm:rounded-lg",
)}

<div className="flex flex-1 flex-col gap-2 sm:gap-1 p-3 sm:p-0">
<h3 className="font-semibold text-foreground line-clamp-2">{title}</h3>
Expand Down
2 changes: 1 addition & 1 deletion components/marketing/HomePageClient.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import OpenSourceSection from "@/components/marketing/sections/OpenSourceSection
import PlayerShowcaseSection from "@/components/marketing/sections/PlayerShowcaseSection";
import SupportSection from "@/components/marketing/sections/SupportSection";

const PAGE_SHELL = "mx-auto w-full max-w-7xl px-5 sm:px-8 lg:px-12";
const PAGE_SHELL = "mx-auto w-full max-w-7xl";

function useScrollToHash() {
useEffect(() => {
Expand Down
4 changes: 2 additions & 2 deletions components/marketing/layout/Section.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,8 @@ export default function Section({ children, className, variant = "plain", id }:
<section
id={id}
className={cn(
variant === "banner" && "w-full bg-primary/10 px-5 py-16 sm:px-8 md:px-10 md:py-20",
variant === "plain" && "py-16 md:py-20 lg:py-24",
variant === "banner" && "w-full bg-primary/10 px-5 py-16 sm:px-8 md:py-20 lg:px-12",
variant === "plain" && "w-full px-5 py-16 sm:px-8 md:py-20 lg:px-12 lg:py-24",
className,
)}
>
Expand Down
6 changes: 3 additions & 3 deletions components/marketing/sections/HeroSection.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -121,11 +121,11 @@ export default function HeroSection({

return (
<Section className="relative overflow-hidden">
<BackgroundDecoration />
<BackgroundDecoration disableOnMobile />

<div className="pointer-events-none absolute inset-0 z-[1] bg-gradient-to-b from-transparent via-background/60 to-background" />
<div className="pointer-events-none absolute inset-0 z-[1] hidden bg-gradient-to-b from-transparent via-background/60 to-background md:block" />

<div className="relative z-10 flex w-full flex-col items-center px-5 sm:px-8">
<div className="relative z-10 flex w-full flex-col items-center">
<h1
className={cn(
"mb-6 max-w-3xl text-center transition-all duration-700 ease-out delay-100",
Expand Down
4 changes: 2 additions & 2 deletions components/marketing/sections/MissionSection.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@ export default function MissionSection() {
strokeLinejoin="round"
aria-hidden="true"
role="presentation"
className="pointer-events-none absolute -left-0 top-1/2 -translate-y-1/2 text-primary/20"
className="pointer-events-none absolute -left-0 top-1/2 hidden -translate-y-1/2 text-primary/20 md:block"
style={{ maskImage: MASK_FADE_RIGHT, WebkitMaskImage: MASK_FADE_RIGHT }}
variants={floatVariants}
initial="hidden"
Expand All @@ -99,7 +99,7 @@ export default function MissionSection() {
strokeLinejoin="round"
aria-hidden="true"
role="presentation"
className="pointer-events-none absolute -right-0 bottom-0 text-primary/15"
className="pointer-events-none absolute -right-0 bottom-0 hidden text-primary/15 md:block"
style={{ maskImage: MASK_FADE_LEFT, WebkitMaskImage: MASK_FADE_LEFT }}
variants={floatRightVariants}
initial="hidden"
Expand Down
2 changes: 1 addition & 1 deletion components/marketing/sections/PlayerShowcaseSection.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ export default function PlayerShowcaseSection() {
<div className="relative mx-auto max-w-6xl">
<div className="relative mx-auto">
<div
className="pointer-events-none absolute inset-x-0 -top-8 mx-auto h-[60%] w-[70%] rounded-full opacity-20 blur-3xl"
className="pointer-events-none absolute inset-x-0 -top-8 mx-auto hidden h-[60%] w-[70%] rounded-full opacity-20 blur-3xl md:block"
style={{
background:
"radial-gradient(ellipse at center, hsl(var(--primary)) 0%, transparent 70%)",
Expand Down
45 changes: 42 additions & 3 deletions components/ui/background-decoration.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -92,22 +92,61 @@ const PARTICLES_CONFIG: ISourceOptions = {
detectRetina: true,
};

function BackgroundDecoration({ className }: { className?: string }) {
interface BackgroundDecorationProps {
className?: string;
disableOnMobile?: boolean;
}

const MOBILE_MEDIA_QUERY = "(max-width: 767px)";

function BackgroundDecoration({
className,
disableOnMobile = false,
}: BackgroundDecorationProps) {
const [engineReady, setEngineReady] = useState(false);
const [visible, setVisible] = useState(false);
const [shouldRenderDecoration, setShouldRenderDecoration] = useState(!disableOnMobile);

useEffect(() => {
if (!disableOnMobile) return;

const mediaQuery = window.matchMedia(MOBILE_MEDIA_QUERY);
const syncViewport = () => setShouldRenderDecoration(!mediaQuery.matches);

syncViewport();
mediaQuery.addEventListener("change", syncViewport);

return () => mediaQuery.removeEventListener("change", syncViewport);
}, [disableOnMobile]);

useEffect(() => {
if (!shouldRenderDecoration) return;

let mounted = true;

initParticlesEngine(async (engine) => {
await loadSlim(engine);
}).then(() => setEngineReady(true));
}, []);
}).then(() => {
if (mounted) {
setEngineReady(true);
}
});

return () => {
mounted = false;
};
}, [shouldRenderDecoration]);

useEffect(() => {
if (!engineReady) return;
const t = setTimeout(() => setVisible(true), 300);
return () => clearTimeout(t);
}, [engineReady]);

if (!shouldRenderDecoration) {
return null;
}

return (
<div className={cn("absolute inset-0 z-0", className)}>
{engineReady && (
Expand Down
Loading
Loading