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
Original file line number Diff line number Diff line change
Expand Up @@ -342,7 +342,7 @@ const SpacesList = ({ toggleMobileNav }: { toggleMobileNav?: () => void }) => {
? `Are you sure you want to delete the space "${pendingDeleteSpace.name}"? This action cannot be undone.`
: ""
}
confirmLabel="Delete"
confirmLabel={removing ? "Deleting..." : "Delete"}
cancelLabel="Cancel"
loading={removing}
onConfirm={confirmRemoveSpace}
Expand Down
26 changes: 2 additions & 24 deletions apps/web/app/(org)/dashboard/caps/components/CapCard/CapCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,6 @@ import {
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { useMutation } from "@tanstack/react-query";
import clsx from "clsx";
import moment from "moment";
import Link from "next/link";
import { useRouter } from "next/navigation";
import { type PropsWithChildren, useState } from "react";
Expand Down Expand Up @@ -105,23 +104,6 @@ export const CapCard = ({

const router = useRouter();

const formatDuration = (duration: string) => {
if (!duration) return "0 secs";

const momentDuration = moment.duration(duration, "milliseconds");
const totalMinutes = Math.floor(momentDuration.asMinutes());
const totalHours = Math.floor(momentDuration.asHours());
const seconds = momentDuration.seconds();

if (totalHours > 0) {
return "1 hr";
} else if (totalMinutes > 0) {
return `${totalMinutes} mins`;
} else {
return `${seconds} secs`;
}
};

const downloadMutation = useMutation({
mutationFn: async () => {
const response = await downloadVideo(cap.id);
Expand Down Expand Up @@ -399,19 +381,14 @@ export const CapCard = ({
icon={<FontAwesomeIcon icon={faVideo} />}
title="Delete Cap"
description={`Are you sure you want to delete the cap "${cap.name}"? This action cannot be undone.`}
confirmLabel="Delete"
confirmLabel={deleteMutation.isPending ? "Deleting..." : "Delete"}
cancelLabel="Cancel"
loading={deleteMutation.isPending}
onConfirm={() => deleteMutation.mutate()}
onCancel={() => setConfirmOpen(false)}
/>
</div>
)}
{cap.metadata?.duration && (
<p className="text-white leading-0 px-2 py-px rounded-full backdrop-blur-sm absolute z-10 left-3 top-[112px] bg-black/50 text-[10px]">
{formatDuration(cap.metadata.duration as string)}
</p>
)}
{!sharedCapCard && onSelectToggle && (
<div
className={clsx(
Expand Down Expand Up @@ -452,6 +429,7 @@ export const CapCard = ({
href={`/s/${cap.id}`}
>
<VideoThumbnail
videoDuration={cap.metadata?.duration}
imageClass={clsx(
anyCapSelected
Comment on lines 431 to 434
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Verification agent

🧩 Analysis chain

Verify duration type/units passed to VideoThumbnail

You’re passing cap.metadata?.duration into videoDuration (typed as string in VideoThumbnail). Please confirm:

  • Type is string or update to number | string (recommended in my VideoThumbnail comment).
  • Units are milliseconds (the formatter assumes ms; if it’s seconds, it will undercount by 1000x).

Run this to find the VideoMetadata definition and the duration field’s type and docstring/usages:


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo 'Searching for VideoMetadata definition...'
rg -n -C3 -P '(interface|type)\s+VideoMetadata\b' --type=ts || true

echo 'Searching for duration field within VideoMetadata...'
rg -n -C3 -P 'VideoMetadata|duration\s*:\s*' --type=ts | sed 's/^/• /' || true

echo 'Searching for where metadata.duration is set (to infer units)...'
rg -n -C3 -P '\bmetadata\b[^;]*\bduration\b' --type=ts -- apps || true

Length of output: 95791


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "=== VideoMetadata interface snippet (first 50 lines) ==="
sed -n '1,50p' packages/database/types/metadata.ts || true

echo
echo "=== Searching for 'duration' property in VideoMetadata ==="
rg -n '^\s*duration\s*:' -C2 packages/database/types/metadata.ts || echo "No duration property found"

Length of output: 1423


Align VideoMetadata.duration (seconds) with VideoThumbnail (milliseconds)

We discovered that VideoMetadata.duration is defined as string | number in seconds:

/** Duration of the video in seconds */
duration?: string | number;

However, VideoThumbnail expects a string representing milliseconds:

interface VideoThumbnailProps {
  videoDuration?: string;
}
const formatDuration = (duration: string) => {
  const durationMs = parseFloat(duration);
  return moment.duration(durationMs, "milliseconds")  
};

Passing raw seconds into a formatter that treats its input as milliseconds will undercount by 1 000×. Please address this mismatch:

In CapCard.tsx (lines 431–434)

  • Convert seconds → milliseconds at the call site, e.g.:
    - <VideoThumbnail
    -   videoDuration={cap.metadata?.duration}
    -
    - />
    + <VideoThumbnail
    +   videoDuration={
    +     cap.metadata?.duration != null
    +       ? String(Number(cap.metadata.duration) * 1000)
    +       : undefined
    +   }
    +
    + />

In VideoThumbnail.tsx

  • Update prop type to accept both string | number:
    - interface VideoThumbnailProps { videoDuration?: string }
    + interface VideoThumbnailProps { videoDuration?: string | number }
  • Enhance formatDuration to detect numbers or adjust the unit (e.g., use "seconds" when appropriate) or normalize all inputs to milliseconds.
🤖 Prompt for AI Agents
In apps/web/app/(org)/dashboard/caps/components/CapCard/CapCard.tsx around lines
431–434, the video duration (seconds) is being passed directly to VideoThumbnail
which expects milliseconds; convert the value to milliseconds at the call site
(e.g., multiply numeric seconds by 1000 and ensure a string is passed). Also
update VideoThumbnail.tsx to accept videoDuration?: string | number and
normalize inside (accept number or string, parse/convert seconds → milliseconds
when needed or treat numeric input as milliseconds consistently), and make
formatDuration resilient by parsing numbers and ensuring it always calls
moment.duration with milliseconds.

? "opacity-50"
Expand Down
3 changes: 1 addition & 2 deletions apps/web/app/(org)/dashboard/caps/components/Folder.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,7 @@ import type { Folder } from "@cap/web-domain";
import { faTrash } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { Fit, Layout, useRive } from "@rive-app/react-canvas";
import { useMutation } from "@tanstack/react-query";
import clsx from "clsx";
import { Effect } from "effect";
import Link from "next/link";
import { useRouter } from "next/navigation";
import { useEffect, useRef, useState } from "react";
Expand Down Expand Up @@ -382,6 +380,7 @@ const FolderCard = ({
icon={<FontAwesomeIcon icon={faTrash} />}
onConfirm={() => deleteFolder.mutate(id)}
onCancel={() => setConfirmDeleteFolderOpen(false)}
confirmLabel={deleteFolder.isPending ? "Deleting..." : "Delete"}
title="Delete Folder"
description={`Are you sure you want to delete the folder "${name}"? This action cannot be undone.`}
/>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@ export const SelectedCapsBar = ({
} cap${
selectedCaps.length === 1 ? "" : "s"
}? This action cannot be undone.`}
confirmLabel="Delete"
confirmLabel={isDeleting ? "Deleting..." : "Delete"}
cancelLabel="Cancel"
confirmVariant="dark"
loading={isDeleting}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ export function CustomDomain() {
icon={<FontAwesomeIcon icon={faGlobe} />}
description={`Are you sure you want to remove this custom domain: ${orgCustomDomain}?`}
onConfirm={handleRemoveDomain}
confirmLabel="Remove"
confirmLabel={removeDomainMutation.isPending ? "Removing..." : "Remove"}
cancelLabel="Cancel"
loading={removeDomainMutation.isPending}
onCancel={() => setConfirmOpen(false)}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,7 @@ export const MembersCard = ({
from your organization? this action cannot be undone.`
: ""
}
confirmLabel={removing ? "Removing..." : "Remove"}
cancelLabel="Cancel"
loading={removing}
onConfirm={confirmRemoveMember}
Expand Down
2 changes: 1 addition & 1 deletion apps/web/app/(org)/dashboard/spaces/browse/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -231,7 +231,7 @@ export default function BrowseSpacesPage() {
? `Are you sure you want to delete the space "${pendingDeleteSpace?.name || "selected"}"? This action cannot be undone.`
: "Are you sure you want to delete this space? This action cannot be undone."
}
confirmLabel="Delete"
confirmLabel={removing ? "Deleting..." : "Delete"}
cancelLabel="Cancel"
loading={removing}
onConfirm={confirmRemoveSpace}
Expand Down
27 changes: 27 additions & 0 deletions apps/web/components/VideoThumbnail.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { LogoSpinner } from "@cap/ui";
import { useQuery } from "@tanstack/react-query";
import clsx from "clsx";
import moment from "moment";
import Image from "next/image";
import { memo, useEffect, useRef, useState } from "react";
import { useUploadingContext } from "@/app/(org)/dashboard/caps/UploadingContext";
Expand All @@ -12,8 +13,28 @@ interface VideoThumbnailProps {
imageClass?: string;
objectFit?: string;
containerClass?: string;
videoDuration?: string;
}

const formatDuration = (duration: string) => {
const durationMs = parseFloat(duration);
const momentDuration = moment.duration(durationMs, "milliseconds");

const totalHours = Math.floor(momentDuration.asHours());
const totalMinutes = Math.floor(momentDuration.asMinutes());
const remainingSeconds = Math.ceil(momentDuration.asSeconds() % 60); // Use ceil to avoid 0 secs

if (totalHours > 0) {
return `${totalHours} hr${totalHours > 1 ? "s" : ""}`;
} else if (totalMinutes > 0) {
return `${totalMinutes} min${totalMinutes > 1 ? "s" : ""}`;
} else if (remainingSeconds > 0) {
return `${remainingSeconds} sec${remainingSeconds !== 1 ? "s" : ""}`;
} else {
return "< 1 sec"; // For very short durations
}
};

function generateRandomGrayScaleColor() {
const minGrayScaleValue = 190;
const maxGrayScaleValue = 235;
Expand All @@ -31,6 +52,7 @@ export const VideoThumbnail: React.FC<VideoThumbnailProps> = memo(
imageClass,
objectFit = "cover",
containerClass,
videoDuration,
}) => {
const imageUrl = useQuery({
queryKey: ["thumbnail", userId, videoId],
Expand Down Expand Up @@ -86,6 +108,11 @@ export const VideoThumbnail: React.FC<VideoThumbnailProps> = memo(
)
)}
</div>
{videoDuration && Number(videoDuration) > 0 && (
<p className="text-white leading-0 px-2 left-3 rounded-full backdrop-blur-sm absolute z-10 bottom-3 bg-black/50 text-[11px]">
{formatDuration(videoDuration)}
</p>
)}
{imageUrl.data && (
<Image
ref={imageRef}
Expand Down
1 change: 0 additions & 1 deletion packages/ui-solid/src/auto-imports.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@
// @ts-nocheck
// noinspection JSUnusedGlobalSymbols
// Generated by unplugin-auto-import
// biome-ignore lint: disable
export {};
declare global {
const IconCapArrows: typeof import("~icons/cap/arrows.jsx")["default"];
Expand Down
Loading