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
1 change: 0 additions & 1 deletion build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,6 @@ dependencies {
implementation 'io.jsonwebtoken:jjwt-api:0.11.2'
runtimeOnly 'io.jsonwebtoken:jjwt-impl:0.11.2',
'io.jsonwebtoken:jjwt-jackson:0.11.2'
implementation 'joda-time:joda-time:2.10.13'
implementation 'org.xerial:sqlite-jdbc:3.36.0.3'

compileOnly 'org.projectlombok:lombok'
Expand Down
5 changes: 3 additions & 2 deletions frontend/components/article/ArticleMeta.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import React from "react";
import ArticleActions from "./ArticleActions";
import CustomImage from "../common/CustomImage";
import CustomLink from "../common/CustomLink";
import { getFullDate, getRelativeTime } from "../../lib/utils/relativeTime";

const ArticleMeta = ({ article }) => {
if (!article) return;
Expand All @@ -24,8 +25,8 @@ const ArticleMeta = ({ article }) => {
>
{article.author?.username}
</CustomLink>
<span className="date">
{new Date(article.createdAt).toDateString()}
<span className="date" title={getFullDate(article.createdAt)}>
{getRelativeTime(article.createdAt)}
</span>
</div>

Expand Down
5 changes: 3 additions & 2 deletions frontend/components/article/ArticlePreview.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import CustomImage from "../common/CustomImage";
import { usePageDispatch } from "../../lib/context/PageContext";
import checkLogin from "../../lib/utils/checkLogin";
import { SERVER_BASE_URL } from "../../lib/utils/constant";
import { getFullDate, getRelativeTime } from "../../lib/utils/relativeTime";
import storage from "../../lib/utils/storage";

const FAVORITED_CLASS = "btn btn-sm btn-primary";
Expand Down Expand Up @@ -90,8 +91,8 @@ const ArticlePreview = ({ article }) => {
>
<span onClick={() => setPage(0)}>{preview.author.username}</span>
</CustomLink>
<span className="date">
{new Date(preview.createdAt).toDateString()}
<span className="date" title={getFullDate(preview.createdAt)}>
{getRelativeTime(preview.createdAt)}
</span>
</div>

Expand Down
8 changes: 6 additions & 2 deletions frontend/components/comment/Comment.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import CustomImage from "../common/CustomImage";
import Maybe from "../common/Maybe";
import DeleteButton from "./DeleteButton";
import checkLogin from "../../lib/utils/checkLogin";
import { getFullDate, getRelativeTime } from "../../lib/utils/relativeTime";
import storage from "../../lib/utils/storage";

const Comment = ({ comment }) => {
Expand Down Expand Up @@ -39,8 +40,11 @@ const Comment = ({ comment }) => {
>
{comment.author.username}
</CustomLink>
<span className="date-posted">
{new Date(comment.createdAt).toDateString()}
<span
className="date-posted"
title={getFullDate(comment.createdAt)}
>
{getRelativeTime(comment.createdAt)}
</span>
<Maybe test={canModify}>
<DeleteButton commentId={comment.id} />
Expand Down
5 changes: 5 additions & 0 deletions frontend/jest.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
module.exports = {
preset: "ts-jest",
testEnvironment: "node",
roots: ["<rootDir>/lib"],
};
4 changes: 2 additions & 2 deletions frontend/lib/types/articleType.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,13 @@ export interface Article {

export type ArticleType = {
tagList: string[];
createdAt: number;
createdAt: string;
author: Author;
description: string;
title: string;
body: string;
slug: string;
updatedAt: number;
updatedAt: string;
favoritesCount: number;
favorited: boolean;
};
Expand Down
4 changes: 2 additions & 2 deletions frontend/lib/types/commentType.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,12 @@ export interface Comments {
}

export type CommentType = {
createdAt: number;
createdAt: string;
id: string;
body: string;
slug: string;
author: Author;
updatedAt: number;
updatedAt: string;
};

export type Author = {
Expand Down
53 changes: 53 additions & 0 deletions frontend/lib/utils/relativeTime.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import { getFullDate, getRelativeTime } from "./relativeTime";

const now = new Date("2026-07-30T20:57:31.000Z");
const isoOffset = (seconds: number): string =>
new Date(now.getTime() - seconds * 1000).toISOString();

describe("getRelativeTime", () => {
it.each([
[10, "10 seconds ago"],
[59, "59 seconds ago"],
[60, "1 minute ago"],
[3599, "59 minutes ago"],
[3600, "1 hour ago"],
[86399, "23 hours ago"],
[86400, "1 day ago"],
[604799, "6 days ago"],
[604800, "1 week ago"],
[2591999, "4 weeks ago"],
[2592000, "1 month ago"],
[31535999, "12 months ago"],
[31536000, "1 year ago"],
])("formats %s seconds as %s", (seconds, expected) => {
expect(getRelativeTime(isoOffset(seconds), now)).toBe(expected);
});

it("formats the API's millisecond ISO-8601 timestamp", () => {
expect(
getRelativeTime("2026-07-29T20:57:31.000Z", now)
).toBe("1 day ago");
});

it("handles very recent, future, invalid, and empty values safely", () => {
expect(getRelativeTime(isoOffset(9), now)).toBe("just now");
expect(
getRelativeTime(new Date(now.getTime() + 60 * 1000).toISOString(), now)
).toBe("in 1 minute");
expect(getRelativeTime("not a date", now)).toBe("");
expect(getRelativeTime("", now)).toBe("");
});
});

describe("getFullDate", () => {
it("returns a human-readable tooltip date", () => {
expect(getFullDate("2026-07-29T20:57:31.000Z")).toBe(
new Date("2026-07-29T20:57:31.000Z").toLocaleString()
);
});

it("handles invalid and empty values safely", () => {
expect(getFullDate("not a date")).toBe("");
expect(getFullDate("")).toBe("");
});
});
45 changes: 45 additions & 0 deletions frontend/lib/utils/relativeTime.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
const UNITS = [
{ name: "year", seconds: 365 * 24 * 60 * 60 },
{ name: "month", seconds: 30 * 24 * 60 * 60 },
{ name: "week", seconds: 7 * 24 * 60 * 60 },
{ name: "day", seconds: 24 * 60 * 60 },
{ name: "hour", seconds: 60 * 60 },
{ name: "minute", seconds: 60 },
{ name: "second", seconds: 1 },
] as const;

const relativeTimeFormatter = new Intl.RelativeTimeFormat(undefined, {
numeric: "always",
});

export const getRelativeTime = (
value: string,
now: Date = new Date()
): string => {
if (!value) return "";

const date = new Date(value);
if (Number.isNaN(date.getTime()) || Number.isNaN(now.getTime())) return "";

const differenceInSeconds = (date.getTime() - now.getTime()) / 1000;
if (Math.abs(differenceInSeconds) < 10) return "just now";

const unit = UNITS.find(
({ seconds }) => Math.abs(differenceInSeconds) >= seconds
);
if (!unit) return "just now";

const amount =
Math.floor(Math.abs(differenceInSeconds) / unit.seconds) *
Math.sign(differenceInSeconds);
return relativeTimeFormatter.format(amount, unit.name);
};

export const getFullDate = (value: string): string => {
if (!value) return "";

const date = new Date(value);
if (Number.isNaN(date.getTime())) return "";

return date.toLocaleString();
};
Loading
Loading