refactor: Update formatUtils to use formatRelativeDownloadTime function
This commit is contained in:
@@ -41,12 +41,11 @@ const CollectionCard: React.FC<CollectionCardProps> = ({ collection, videos }) =
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
position: 'relative',
|
||||
transition: 'transform 0.2s, box-shadow 0.2s, background-color 0.3s, color 0.3s, border-color 0.3s',
|
||||
transition: 'transform 0.2s, box-shadow 0.2s, background-color 0.3s, color 0.3s',
|
||||
'&:hover': {
|
||||
transform: 'translateY(-4px)',
|
||||
boxShadow: theme.shadows[8],
|
||||
},
|
||||
border: `1px solid ${theme.palette.secondary.main}`
|
||||
}
|
||||
}}
|
||||
>
|
||||
<CardActionArea onClick={handleClick} sx={{ flexGrow: 1, display: 'flex', flexDirection: 'column', alignItems: 'stretch' }}>
|
||||
@@ -76,7 +75,7 @@ const CollectionCard: React.FC<CollectionCardProps> = ({ collection, videos }) =
|
||||
|
||||
<Chip
|
||||
icon={<Folder />}
|
||||
label={`${collection.videos.length} videos`}
|
||||
label={collection.videos.length}
|
||||
color="secondary"
|
||||
size="small"
|
||||
sx={{ position: 'absolute', bottom: 8, right: 8 }}
|
||||
|
||||
@@ -2,7 +2,7 @@ import { Box, CardContent, Typography } from '@mui/material';
|
||||
import React from 'react';
|
||||
import { useLanguage } from '../../contexts/LanguageContext';
|
||||
import { Video } from '../../types';
|
||||
import { formatDate } from '../../utils/formatUtils';
|
||||
import { formatRelativeDownloadTime } from '../../utils/formatUtils';
|
||||
import { VideoCardCollectionInfo } from '../../utils/videoCardUtils';
|
||||
|
||||
interface VideoCardContentProps {
|
||||
@@ -72,7 +72,7 @@ export const VideoCardContent: React.FC<VideoCardContentProps> = ({
|
||||
</Typography>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', flexShrink: 0 }}>
|
||||
<Typography variant="caption" color="text.secondary">
|
||||
{formatDate(video.date)}
|
||||
{formatRelativeDownloadTime(video.addedAt, video.date, t)}
|
||||
</Typography>
|
||||
<Typography variant="caption" color="text.secondary" sx={{ ml: 1 }}>
|
||||
{video.viewCount || 0} {t('views')}
|
||||
|
||||
@@ -10,6 +10,7 @@ vi.mock('../../../contexts/LanguageContext', () => ({
|
||||
|
||||
vi.mock('../../../utils/formatUtils', () => ({
|
||||
formatDate: () => '2023-01-01',
|
||||
formatRelativeDownloadTime: () => '2023-01-01',
|
||||
}));
|
||||
|
||||
|
||||
|
||||
@@ -86,7 +86,7 @@ describe('CollectionCard', () => {
|
||||
);
|
||||
|
||||
expect(screen.getByText(/Test Collection/i)).toBeInTheDocument();
|
||||
expect(screen.getByText(/2 videos/i)).toBeInTheDocument();
|
||||
expect(screen.getByText('2')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders collection creation date', () => {
|
||||
@@ -131,7 +131,7 @@ describe('CollectionCard', () => {
|
||||
);
|
||||
|
||||
// Should show folder icon (via Material-UI icon)
|
||||
expect(screen.getByText(/0 videos/i)).toBeInTheDocument();
|
||||
expect(screen.getByText('0')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('displays up to 4 thumbnails in grid', () => {
|
||||
|
||||
@@ -1,143 +1,297 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { formatDate, formatDuration, formatSize, parseDuration } from '../formatUtils';
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import {
|
||||
formatDate,
|
||||
formatDuration,
|
||||
formatRelativeDownloadTime,
|
||||
formatSize,
|
||||
parseDuration,
|
||||
} from "../formatUtils";
|
||||
|
||||
describe('formatUtils', () => {
|
||||
describe('parseDuration', () => {
|
||||
it('should return 0 for undefined', () => {
|
||||
expect(parseDuration(undefined)).toBe(0);
|
||||
});
|
||||
|
||||
it('should return number as-is', () => {
|
||||
expect(parseDuration(100)).toBe(100);
|
||||
expect(parseDuration(0)).toBe(0);
|
||||
});
|
||||
|
||||
it('should parse HH:MM:SS format', () => {
|
||||
expect(parseDuration('1:30:45')).toBe(5445); // 1*3600 + 30*60 + 45 = 3600 + 1800 + 45
|
||||
expect(parseDuration('0:5:30')).toBe(330); // 0*3600 + 5*60 + 30 = 0 + 300 + 30
|
||||
expect(parseDuration('2:0:0')).toBe(7200); // 2*3600 + 0*60 + 0 = 7200
|
||||
});
|
||||
|
||||
it('should parse MM:SS format', () => {
|
||||
expect(parseDuration('5:30')).toBe(330); // 5*60 + 30
|
||||
expect(parseDuration('10:15')).toBe(615); // 10*60 + 15
|
||||
expect(parseDuration('0:45')).toBe(45);
|
||||
});
|
||||
|
||||
it('should parse numeric string', () => {
|
||||
expect(parseDuration('100')).toBe(100);
|
||||
expect(parseDuration('0')).toBe(0);
|
||||
});
|
||||
|
||||
it('should return 0 for invalid string', () => {
|
||||
expect(parseDuration('invalid')).toBe(0);
|
||||
// 'abc:def' will be parsed as NaN for each part, but the function
|
||||
// will try parseInt on the whole string which also returns NaN -> 0
|
||||
expect(parseDuration('abc:def')).toBe(0);
|
||||
expect(parseDuration('not-a-number')).toBe(0);
|
||||
});
|
||||
describe("formatUtils", () => {
|
||||
describe("parseDuration", () => {
|
||||
it("should return 0 for undefined", () => {
|
||||
expect(parseDuration(undefined)).toBe(0);
|
||||
});
|
||||
|
||||
describe('formatDuration', () => {
|
||||
it('should return 00:00 for undefined', () => {
|
||||
expect(formatDuration(undefined)).toBe('00:00');
|
||||
});
|
||||
|
||||
it('should return formatted string as-is if already formatted', () => {
|
||||
expect(formatDuration('1:30:45')).toBe('1:30:45');
|
||||
expect(formatDuration('5:30')).toBe('5:30');
|
||||
});
|
||||
|
||||
it('should format seconds to MM:SS', () => {
|
||||
expect(formatDuration(65)).toBe('1:05'); // 1 minute 5 seconds
|
||||
expect(formatDuration(125)).toBe('2:05'); // 2 minutes 5 seconds
|
||||
expect(formatDuration(45)).toBe('0:45'); // 45 seconds
|
||||
expect(formatDuration(0)).toBe('00:00');
|
||||
});
|
||||
|
||||
it('should format seconds to H:MM:SS for hours', () => {
|
||||
expect(formatDuration(3665)).toBe('1:01:05'); // 1 hour 1 minute 5 seconds
|
||||
expect(formatDuration(3600)).toBe('1:00:00'); // 1 hour
|
||||
expect(formatDuration(7325)).toBe('2:02:05'); // 2 hours 2 minutes 5 seconds
|
||||
});
|
||||
|
||||
it('should format numeric string', () => {
|
||||
expect(formatDuration('65')).toBe('1:05');
|
||||
expect(formatDuration('3665')).toBe('1:01:05');
|
||||
});
|
||||
|
||||
it('should return 00:00 for invalid input', () => {
|
||||
expect(formatDuration('invalid')).toBe('00:00');
|
||||
expect(formatDuration(NaN)).toBe('00:00');
|
||||
});
|
||||
it("should return number as-is", () => {
|
||||
expect(parseDuration(100)).toBe(100);
|
||||
expect(parseDuration(0)).toBe(0);
|
||||
});
|
||||
|
||||
describe('formatSize', () => {
|
||||
it('should return "0 B" for undefined', () => {
|
||||
expect(formatSize(undefined)).toBe('0 B');
|
||||
});
|
||||
|
||||
it('should format bytes', () => {
|
||||
expect(formatSize(0)).toBe('0 B');
|
||||
expect(formatSize(500)).toBe('500 B');
|
||||
expect(formatSize(1023)).toBe('1023 B');
|
||||
});
|
||||
|
||||
it('should format kilobytes', () => {
|
||||
expect(formatSize(1024)).toBe('1 KB');
|
||||
expect(formatSize(1536)).toBe('1.5 KB');
|
||||
expect(formatSize(2048)).toBe('2 KB');
|
||||
expect(formatSize(10240)).toBe('10 KB');
|
||||
});
|
||||
|
||||
it('should format megabytes', () => {
|
||||
expect(formatSize(1048576)).toBe('1 MB'); // 1024 * 1024
|
||||
expect(formatSize(1572864)).toBe('1.5 MB');
|
||||
expect(formatSize(5242880)).toBe('5 MB');
|
||||
});
|
||||
|
||||
it('should format gigabytes', () => {
|
||||
expect(formatSize(1073741824)).toBe('1 GB'); // 1024^3
|
||||
expect(formatSize(2147483648)).toBe('2 GB');
|
||||
});
|
||||
|
||||
it('should format terabytes', () => {
|
||||
expect(formatSize(1099511627776)).toBe('1 TB'); // 1024^4
|
||||
});
|
||||
|
||||
it('should format numeric string', () => {
|
||||
expect(formatSize('1024')).toBe('1 KB');
|
||||
expect(formatSize('1048576')).toBe('1 MB');
|
||||
});
|
||||
|
||||
it('should return "0 B" for invalid input', () => {
|
||||
expect(formatSize('invalid')).toBe('0 B');
|
||||
expect(formatSize(NaN)).toBe('0 B');
|
||||
});
|
||||
it("should parse HH:MM:SS format", () => {
|
||||
expect(parseDuration("1:30:45")).toBe(5445); // 1*3600 + 30*60 + 45 = 3600 + 1800 + 45
|
||||
expect(parseDuration("0:5:30")).toBe(330); // 0*3600 + 5*60 + 30 = 0 + 300 + 30
|
||||
expect(parseDuration("2:0:0")).toBe(7200); // 2*3600 + 0*60 + 0 = 7200
|
||||
});
|
||||
|
||||
describe('formatDate', () => {
|
||||
it('should return "Unknown date" for undefined', () => {
|
||||
expect(formatDate(undefined)).toBe('Unknown date');
|
||||
});
|
||||
|
||||
it('should return "Unknown date" for invalid length', () => {
|
||||
expect(formatDate('202301')).toBe('Unknown date');
|
||||
expect(formatDate('202301011')).toBe('Unknown date');
|
||||
expect(formatDate('2023')).toBe('Unknown date');
|
||||
});
|
||||
|
||||
it('should format YYYYMMDD to YYYY-MM-DD', () => {
|
||||
expect(formatDate('20230101')).toBe('2023-01-01');
|
||||
expect(formatDate('20231225')).toBe('2023-12-25');
|
||||
expect(formatDate('20200101')).toBe('2020-01-01');
|
||||
expect(formatDate('20230228')).toBe('2023-02-28');
|
||||
});
|
||||
|
||||
it('should handle edge cases', () => {
|
||||
expect(formatDate('19991231')).toBe('1999-12-31');
|
||||
expect(formatDate('20991231')).toBe('2099-12-31');
|
||||
});
|
||||
it("should parse MM:SS format", () => {
|
||||
expect(parseDuration("5:30")).toBe(330); // 5*60 + 30
|
||||
expect(parseDuration("10:15")).toBe(615); // 10*60 + 15
|
||||
expect(parseDuration("0:45")).toBe(45);
|
||||
});
|
||||
|
||||
it("should parse numeric string", () => {
|
||||
expect(parseDuration("100")).toBe(100);
|
||||
expect(parseDuration("0")).toBe(0);
|
||||
});
|
||||
|
||||
it("should return 0 for invalid string", () => {
|
||||
expect(parseDuration("invalid")).toBe(0);
|
||||
// 'abc:def' will be parsed as NaN for each part, but the function
|
||||
// will try parseInt on the whole string which also returns NaN -> 0
|
||||
expect(parseDuration("abc:def")).toBe(0);
|
||||
expect(parseDuration("not-a-number")).toBe(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe("formatDuration", () => {
|
||||
it("should return 00:00 for undefined", () => {
|
||||
expect(formatDuration(undefined)).toBe("00:00");
|
||||
});
|
||||
|
||||
it("should return formatted string as-is if already formatted", () => {
|
||||
expect(formatDuration("1:30:45")).toBe("1:30:45");
|
||||
expect(formatDuration("5:30")).toBe("5:30");
|
||||
});
|
||||
|
||||
it("should format seconds to MM:SS", () => {
|
||||
expect(formatDuration(65)).toBe("1:05"); // 1 minute 5 seconds
|
||||
expect(formatDuration(125)).toBe("2:05"); // 2 minutes 5 seconds
|
||||
expect(formatDuration(45)).toBe("0:45"); // 45 seconds
|
||||
expect(formatDuration(0)).toBe("00:00");
|
||||
});
|
||||
|
||||
it("should format seconds to H:MM:SS for hours", () => {
|
||||
expect(formatDuration(3665)).toBe("1:01:05"); // 1 hour 1 minute 5 seconds
|
||||
expect(formatDuration(3600)).toBe("1:00:00"); // 1 hour
|
||||
expect(formatDuration(7325)).toBe("2:02:05"); // 2 hours 2 minutes 5 seconds
|
||||
});
|
||||
|
||||
it("should format numeric string", () => {
|
||||
expect(formatDuration("65")).toBe("1:05");
|
||||
expect(formatDuration("3665")).toBe("1:01:05");
|
||||
});
|
||||
|
||||
it("should return 00:00 for invalid input", () => {
|
||||
expect(formatDuration("invalid")).toBe("00:00");
|
||||
expect(formatDuration(NaN)).toBe("00:00");
|
||||
});
|
||||
});
|
||||
|
||||
describe("formatSize", () => {
|
||||
it('should return "0 B" for undefined', () => {
|
||||
expect(formatSize(undefined)).toBe("0 B");
|
||||
});
|
||||
|
||||
it("should format bytes", () => {
|
||||
expect(formatSize(0)).toBe("0 B");
|
||||
expect(formatSize(500)).toBe("500 B");
|
||||
expect(formatSize(1023)).toBe("1023 B");
|
||||
});
|
||||
|
||||
it("should format kilobytes", () => {
|
||||
expect(formatSize(1024)).toBe("1 KB");
|
||||
expect(formatSize(1536)).toBe("1.5 KB");
|
||||
expect(formatSize(2048)).toBe("2 KB");
|
||||
expect(formatSize(10240)).toBe("10 KB");
|
||||
});
|
||||
|
||||
it("should format megabytes", () => {
|
||||
expect(formatSize(1048576)).toBe("1 MB"); // 1024 * 1024
|
||||
expect(formatSize(1572864)).toBe("1.5 MB");
|
||||
expect(formatSize(5242880)).toBe("5 MB");
|
||||
});
|
||||
|
||||
it("should format gigabytes", () => {
|
||||
expect(formatSize(1073741824)).toBe("1 GB"); // 1024^3
|
||||
expect(formatSize(2147483648)).toBe("2 GB");
|
||||
});
|
||||
|
||||
it("should format terabytes", () => {
|
||||
expect(formatSize(1099511627776)).toBe("1 TB"); // 1024^4
|
||||
});
|
||||
|
||||
it("should format numeric string", () => {
|
||||
expect(formatSize("1024")).toBe("1 KB");
|
||||
expect(formatSize("1048576")).toBe("1 MB");
|
||||
});
|
||||
|
||||
it('should return "0 B" for invalid input', () => {
|
||||
expect(formatSize("invalid")).toBe("0 B");
|
||||
expect(formatSize(NaN)).toBe("0 B");
|
||||
});
|
||||
});
|
||||
|
||||
describe("formatDate", () => {
|
||||
it('should return "Unknown date" for undefined', () => {
|
||||
expect(formatDate(undefined)).toBe("Unknown date");
|
||||
});
|
||||
|
||||
it('should return "Unknown date" for invalid length', () => {
|
||||
expect(formatDate("202301")).toBe("Unknown date");
|
||||
expect(formatDate("202301011")).toBe("Unknown date");
|
||||
expect(formatDate("2023")).toBe("Unknown date");
|
||||
});
|
||||
|
||||
it("should format YYYYMMDD to YYYY-MM-DD", () => {
|
||||
expect(formatDate("20230101")).toBe("2023-01-01");
|
||||
expect(formatDate("20231225")).toBe("2023-12-25");
|
||||
expect(formatDate("20200101")).toBe("2020-01-01");
|
||||
expect(formatDate("20230228")).toBe("2023-02-28");
|
||||
});
|
||||
|
||||
it("should handle edge cases", () => {
|
||||
expect(formatDate("19991231")).toBe("1999-12-31");
|
||||
expect(formatDate("20991231")).toBe("2099-12-31");
|
||||
});
|
||||
});
|
||||
|
||||
describe("formatRelativeDownloadTime", () => {
|
||||
beforeEach(() => {
|
||||
vi.useFakeTimers();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.useRealTimers();
|
||||
});
|
||||
|
||||
const mockTranslation = (
|
||||
key: string,
|
||||
replacements?: Record<string, string | number>
|
||||
) => {
|
||||
const translations: Record<string, string> = {
|
||||
justNow: "Just now",
|
||||
hoursAgo: `${replacements?.hours || 0} hours ago`,
|
||||
today: "Today",
|
||||
thisWeek: "This week",
|
||||
weeksAgo: `${replacements?.weeks || 0} weeks ago`,
|
||||
unknownDate: "Unknown date",
|
||||
};
|
||||
return translations[key] || key;
|
||||
};
|
||||
|
||||
it('should return "Just now" for less than 1 hour', () => {
|
||||
const now = new Date("2023-01-01T12:00:00Z");
|
||||
vi.setSystemTime(now);
|
||||
const thirtyMinutesAgo = new Date("2023-01-01T11:30:00Z").toISOString();
|
||||
expect(
|
||||
formatRelativeDownloadTime(thirtyMinutesAgo, undefined, mockTranslation)
|
||||
).toBe("Just now");
|
||||
});
|
||||
|
||||
it('should return "X hours ago" for 1-5 hours', () => {
|
||||
const now = new Date("2023-01-01T12:00:00Z");
|
||||
vi.setSystemTime(now);
|
||||
const twoHoursAgo = new Date("2023-01-01T10:00:00Z").toISOString();
|
||||
expect(
|
||||
formatRelativeDownloadTime(twoHoursAgo, undefined, mockTranslation)
|
||||
).toBe("2 hours ago");
|
||||
});
|
||||
|
||||
it('should return "Today" for 5-24 hours', () => {
|
||||
const now = new Date("2023-01-01T12:00:00Z");
|
||||
vi.setSystemTime(now);
|
||||
const tenHoursAgo = new Date("2023-01-01T02:00:00Z").toISOString();
|
||||
expect(
|
||||
formatRelativeDownloadTime(tenHoursAgo, undefined, mockTranslation)
|
||||
).toBe("Today");
|
||||
});
|
||||
|
||||
it('should return "This week" for 1-7 days', () => {
|
||||
const now = new Date("2023-01-01T12:00:00Z");
|
||||
vi.setSystemTime(now);
|
||||
const threeDaysAgo = new Date("2022-12-29T12:00:00Z").toISOString();
|
||||
expect(
|
||||
formatRelativeDownloadTime(threeDaysAgo, undefined, mockTranslation)
|
||||
).toBe("This week");
|
||||
});
|
||||
|
||||
it('should return "X weeks ago" for 1-4 weeks', () => {
|
||||
const now = new Date("2023-01-01T12:00:00Z");
|
||||
vi.setSystemTime(now);
|
||||
const twoWeeksAgo = new Date("2022-12-18T12:00:00Z").toISOString();
|
||||
expect(
|
||||
formatRelativeDownloadTime(twoWeeksAgo, undefined, mockTranslation)
|
||||
).toBe("2 weeks ago");
|
||||
});
|
||||
|
||||
it("should return formatted date for > 4 weeks", () => {
|
||||
const now = new Date("2023-01-01T12:00:00Z");
|
||||
vi.setSystemTime(now);
|
||||
const sixWeeksAgo = new Date("2022-11-20T12:00:00Z").toISOString();
|
||||
const result = formatRelativeDownloadTime(
|
||||
sixWeeksAgo,
|
||||
"20221120",
|
||||
mockTranslation
|
||||
);
|
||||
expect(result).toBe("2022-11-20");
|
||||
});
|
||||
|
||||
it("should use originalDate when provided for > 4 weeks", () => {
|
||||
const now = new Date("2023-01-01T12:00:00Z");
|
||||
vi.setSystemTime(now);
|
||||
const sixWeeksAgo = new Date("2022-11-20T12:00:00Z").toISOString();
|
||||
expect(
|
||||
formatRelativeDownloadTime(sixWeeksAgo, "20221120", mockTranslation)
|
||||
).toBe("2022-11-20");
|
||||
});
|
||||
|
||||
it('should fallback to "Unknown date" when no timestamp provided', () => {
|
||||
expect(
|
||||
formatRelativeDownloadTime(undefined, undefined, mockTranslation)
|
||||
).toBe("Unknown date");
|
||||
});
|
||||
|
||||
it("should use originalDate when no timestamp provided", () => {
|
||||
expect(
|
||||
formatRelativeDownloadTime(undefined, "20230101", mockTranslation)
|
||||
).toBe("2023-01-01");
|
||||
});
|
||||
|
||||
it("should fallback to English when no translation function provided", () => {
|
||||
const now = new Date("2023-01-01T12:00:00Z");
|
||||
vi.setSystemTime(now);
|
||||
const thirtyMinutesAgo = new Date("2023-01-01T11:30:00Z").toISOString();
|
||||
expect(formatRelativeDownloadTime(thirtyMinutesAgo)).toBe("Just now");
|
||||
});
|
||||
|
||||
it("should handle invalid date", () => {
|
||||
expect(
|
||||
formatRelativeDownloadTime("invalid-date", undefined, mockTranslation)
|
||||
).toBe("Unknown date");
|
||||
});
|
||||
|
||||
it("should use originalDate when date is invalid", () => {
|
||||
expect(
|
||||
formatRelativeDownloadTime("invalid-date", "20230101", mockTranslation)
|
||||
).toBe("2023-01-01");
|
||||
});
|
||||
|
||||
it("should format date in UTC to avoid timezone issues", () => {
|
||||
const now = new Date("2023-01-01T12:00:00Z");
|
||||
vi.setSystemTime(now);
|
||||
// Use a date that could be affected by timezone (midnight UTC)
|
||||
const sixWeeksAgo = new Date("2022-11-20T00:00:00Z").toISOString();
|
||||
// Should format as 2022-11-20 regardless of system timezone
|
||||
const result = formatRelativeDownloadTime(
|
||||
sixWeeksAgo,
|
||||
undefined,
|
||||
mockTranslation
|
||||
);
|
||||
expect(result).toBe("2022-11-20");
|
||||
});
|
||||
|
||||
it("should handle date formatting across timezone boundaries", () => {
|
||||
const now = new Date("2023-01-01T12:00:00Z");
|
||||
vi.setSystemTime(now);
|
||||
// Test with a date near midnight UTC to catch timezone edge cases
|
||||
const sixWeeksAgo = new Date("2022-11-20T23:59:59Z").toISOString();
|
||||
const result = formatRelativeDownloadTime(
|
||||
sixWeeksAgo,
|
||||
undefined,
|
||||
mockTranslation
|
||||
);
|
||||
expect(result).toBe("2022-11-20");
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -81,6 +81,106 @@ export const formatDate = (dateString?: string) => {
|
||||
return `${year}-${month}-${day}`;
|
||||
};
|
||||
|
||||
/**
|
||||
* Format relative time from download timestamp to current time
|
||||
* 0 - 1 hour: "Just now"
|
||||
* 1 hour - 5 hours: "X hours ago"
|
||||
* 5 hours - 24 hours: "Today"
|
||||
* 1 day - 7 days: "This week"
|
||||
* 1 week - 4 weeks: "X weeks ago"
|
||||
* > 4 weeks: show actual date
|
||||
*/
|
||||
export const formatRelativeDownloadTime = (
|
||||
downloadTimestamp?: string,
|
||||
originalDate?: string,
|
||||
t?: (key: string, replacements?: Record<string, string | number>) => string
|
||||
): string => {
|
||||
const getTranslation = (
|
||||
key: string,
|
||||
replacements?: Record<string, string | number>
|
||||
): string => {
|
||||
if (t) {
|
||||
return t(key as any, replacements);
|
||||
}
|
||||
// Fallback to English if no translation function provided
|
||||
const fallbacks: Record<string, string> = {
|
||||
justNow: "Just now",
|
||||
hoursAgo: "{hours} hours ago",
|
||||
today: "Today",
|
||||
thisWeek: "This week",
|
||||
weeksAgo: "{weeks} weeks ago",
|
||||
unknownDate: "Unknown date",
|
||||
};
|
||||
let text = fallbacks[key] || key;
|
||||
if (replacements) {
|
||||
Object.entries(replacements).forEach(([placeholder, value]) => {
|
||||
text = text.replace(`{${placeholder}}`, String(value));
|
||||
});
|
||||
}
|
||||
return text;
|
||||
};
|
||||
|
||||
if (!downloadTimestamp) {
|
||||
// Fallback to original date format if no download timestamp
|
||||
return originalDate
|
||||
? formatDate(originalDate)
|
||||
: getTranslation("unknownDate");
|
||||
}
|
||||
|
||||
const downloadDate = new Date(downloadTimestamp);
|
||||
const now = new Date();
|
||||
|
||||
// Check if date is valid
|
||||
if (isNaN(downloadDate.getTime())) {
|
||||
return originalDate
|
||||
? formatDate(originalDate)
|
||||
: getTranslation("unknownDate");
|
||||
}
|
||||
|
||||
const diffMs = now.getTime() - downloadDate.getTime();
|
||||
const diffHours = diffMs / (1000 * 60 * 60);
|
||||
const diffDays = diffMs / (1000 * 60 * 60 * 24);
|
||||
const diffWeeks = diffDays / 7;
|
||||
|
||||
// 0 - 1 hour: "Just now"
|
||||
if (diffHours < 1) {
|
||||
return getTranslation("justNow");
|
||||
}
|
||||
|
||||
// 1 hour - 5 hours: "X hours ago"
|
||||
if (diffHours >= 1 && diffHours < 5) {
|
||||
const hours = Math.floor(diffHours);
|
||||
return getTranslation("hoursAgo", { hours });
|
||||
}
|
||||
|
||||
// 5 hours - 24 hours: "Today"
|
||||
if (diffHours >= 5 && diffHours < 24) {
|
||||
return getTranslation("today");
|
||||
}
|
||||
|
||||
// 1 day - 7 days: "This week"
|
||||
if (diffDays >= 1 && diffDays < 7) {
|
||||
return getTranslation("thisWeek");
|
||||
}
|
||||
|
||||
// 1 week - 4 weeks: "X周前" / "X weeks ago"
|
||||
if (diffWeeks >= 1 && diffWeeks < 4) {
|
||||
const weeks = Math.floor(diffWeeks);
|
||||
return getTranslation("weeksAgo", { weeks });
|
||||
}
|
||||
|
||||
// > 4 weeks: show actual date
|
||||
if (originalDate) {
|
||||
return formatDate(originalDate);
|
||||
}
|
||||
// Format download date as YYYY-MM-DD if no original date
|
||||
// Use UTC methods to ensure timezone independence
|
||||
const year = downloadDate.getUTCFullYear();
|
||||
const month = String(downloadDate.getUTCMonth() + 1).padStart(2, "0");
|
||||
const day = String(downloadDate.getUTCDate()).padStart(2, "0");
|
||||
return `${year}-${month}-${day}`;
|
||||
};
|
||||
|
||||
/**
|
||||
* Generate timestamp string in format YYYY-MM-DD-HH-MM-SS
|
||||
* Matches the backend generateTimestamp() function format
|
||||
@@ -101,14 +201,17 @@ export const generateTimestamp = (): string => {
|
||||
* If path is already a full URL (starts with http:// or https://), return it as is
|
||||
* Otherwise, prepend BACKEND_URL
|
||||
*/
|
||||
export const getFileUrl = (path: string | null | undefined, backendUrl: string): string | undefined => {
|
||||
export const getFileUrl = (
|
||||
path: string | null | undefined,
|
||||
backendUrl: string
|
||||
): string | undefined => {
|
||||
if (!path) return undefined;
|
||||
|
||||
|
||||
// Check if path is already a full URL
|
||||
if (path.startsWith("http://") || path.startsWith("https://")) {
|
||||
return path;
|
||||
}
|
||||
|
||||
|
||||
// Otherwise, prepend backend URL
|
||||
return `${backendUrl}${path}`;
|
||||
};
|
||||
|
||||
@@ -366,6 +366,11 @@ export const ar = {
|
||||
unknownDate: "تاريخ غير معروف",
|
||||
part: "جزء",
|
||||
collection: "مجموعة",
|
||||
justNow: "الآن",
|
||||
hoursAgo: "منذ {hours} ساعة",
|
||||
today: "اليوم",
|
||||
thisWeek: "هذا الأسبوع",
|
||||
weeksAgo: "منذ {weeks} أسبوع",
|
||||
|
||||
// Upload Modal
|
||||
selectVideoFile: "اختر ملف فيديو",
|
||||
|
||||
@@ -334,6 +334,11 @@ export const de = {
|
||||
unknownDate: "Unbekanntes Datum",
|
||||
part: "Teil",
|
||||
collection: "Sammlung",
|
||||
justNow: "Gerade eben",
|
||||
hoursAgo: "vor {hours} Stunden",
|
||||
today: "Heute",
|
||||
thisWeek: "Diese Woche",
|
||||
weeksAgo: "vor {weeks} Wochen",
|
||||
selectVideoFile: "Videodatei Auswählen",
|
||||
pleaseSelectVideo: "Bitte wählen Sie eine Videodatei aus",
|
||||
uploadFailed: "Upload fehlgeschlagen",
|
||||
|
||||
@@ -375,6 +375,11 @@ export const en = {
|
||||
part: "Part",
|
||||
collection: "Collection",
|
||||
new: "NEW",
|
||||
justNow: "Just now",
|
||||
hoursAgo: "{hours} hours ago",
|
||||
today: "Today",
|
||||
thisWeek: "This week",
|
||||
weeksAgo: "{weeks} weeks ago",
|
||||
|
||||
// Upload Modal
|
||||
selectVideoFile: "Select Video File",
|
||||
|
||||
@@ -358,6 +358,11 @@ export const es = {
|
||||
unknownDate: "Fecha desconocida",
|
||||
part: "Parte",
|
||||
collection: "Colección",
|
||||
justNow: "Ahora mismo",
|
||||
hoursAgo: "Hace {hours} horas",
|
||||
today: "Hoy",
|
||||
thisWeek: "Esta semana",
|
||||
weeksAgo: "Hace {weeks} semanas",
|
||||
selectVideoFile: "Seleccionar Archivo de Video",
|
||||
pleaseSelectVideo: "Por favor seleccione un archivo de video",
|
||||
uploadFailed: "Carga fallida",
|
||||
|
||||
@@ -381,6 +381,11 @@ export const fr = {
|
||||
unknownDate: "Date inconnue",
|
||||
part: "Partie",
|
||||
collection: "Collection",
|
||||
justNow: "À l'instant",
|
||||
hoursAgo: "Il y a {hours} heures",
|
||||
today: "Aujourd'hui",
|
||||
thisWeek: "Cette semaine",
|
||||
weeksAgo: "Il y a {weeks} semaines",
|
||||
|
||||
// Upload Modal
|
||||
selectVideoFile: "Sélectionner un fichier vidéo",
|
||||
|
||||
@@ -359,6 +359,11 @@ export const ja = {
|
||||
unknownDate: "不明な日付",
|
||||
part: "パート",
|
||||
collection: "コレクション",
|
||||
justNow: "たった今",
|
||||
hoursAgo: "{hours}時間前",
|
||||
today: "今日",
|
||||
thisWeek: "今週",
|
||||
weeksAgo: "{weeks}週間前",
|
||||
|
||||
// Upload Modal
|
||||
selectVideoFile: "動画ファイルを選択",
|
||||
|
||||
@@ -356,6 +356,11 @@ export const ko = {
|
||||
unknownDate: "알 수 없는 날짜",
|
||||
part: "파트",
|
||||
collection: "컬렉션",
|
||||
justNow: "방금",
|
||||
hoursAgo: "{hours}시간 전",
|
||||
today: "오늘",
|
||||
thisWeek: "이번 주",
|
||||
weeksAgo: "{weeks}주 전",
|
||||
|
||||
// Upload Modal
|
||||
selectVideoFile: "동영상 파일 선택",
|
||||
|
||||
@@ -364,6 +364,11 @@ export const pt = {
|
||||
unknownDate: "Data desconhecida",
|
||||
part: "Parte",
|
||||
collection: "Coleção",
|
||||
justNow: "Agora mesmo",
|
||||
hoursAgo: "Há {hours} horas",
|
||||
today: "Hoje",
|
||||
thisWeek: "Esta semana",
|
||||
weeksAgo: "Há {weeks} semanas",
|
||||
|
||||
// Upload Modal
|
||||
selectVideoFile: "Selecionar Arquivo de Vídeo",
|
||||
|
||||
@@ -374,6 +374,11 @@ export const ru = {
|
||||
unknownDate: "Неизвестная дата",
|
||||
part: "Часть",
|
||||
collection: "Коллекция",
|
||||
justNow: "Только что",
|
||||
hoursAgo: "{hours} часов назад",
|
||||
today: "Сегодня",
|
||||
thisWeek: "На этой неделе",
|
||||
weeksAgo: "{weeks} недель назад",
|
||||
|
||||
// Upload Modal
|
||||
selectVideoFile: "Выберите видеофайл",
|
||||
|
||||
@@ -357,6 +357,11 @@ export const zh = {
|
||||
unknownDate: "未知日期",
|
||||
part: "分P",
|
||||
collection: "合集",
|
||||
justNow: "刚刚",
|
||||
hoursAgo: "{hours}小时前",
|
||||
today: "今天",
|
||||
thisWeek: "本周",
|
||||
weeksAgo: "{weeks}周前",
|
||||
|
||||
// Upload Modal
|
||||
selectVideoFile: "选择视频文件",
|
||||
|
||||
Reference in New Issue
Block a user