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
35 changes: 35 additions & 0 deletions frontend/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
{
"name": "stellarpulse-frontend",
"version": "0.1.0",
"private": true,
"scripts": {
"dev": "next dev",
"build": "next build",
"start": "next start",
"test": "vitest run",
"lint": "next lint"
},
"dependencies": {
"@creit.tech/stellar-wallets-kit": "^1.2.0",
"@stellar/stellar-sdk": "^14.5.0",
"buffer": "^6.0.3",
"next": "^14.2.29",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"react-icons": "^5.4.0"
},
"devDependencies": {
"@testing-library/dom": "^10.4.1",
"@testing-library/jest-dom": "^6.6.3",
"@testing-library/react": "^16.1.0",
"@types/node": "^22.13.5",
"@types/react": "^18.3.18",
"@types/react-dom": "^18.3.5",
"autoprefixer": "^10.4.20",
"jsdom": "^25.0.1",
"postcss": "^8.5.3",
"tailwindcss": "^3.4.17",
"typescript": "^5.7.3",
"vitest": "^2.1.8"
}
}
4 changes: 2 additions & 2 deletions frontend/src/__tests__/components/Navbar.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ describe("Navbar", () => {

it("renders the logo / brand name", () => {
render(<Navbar />);
expect(screen.getByText(/StellarPulse/i)).toBeInTheDocument();
expect(screen.getByText(/Stellar Pulse/i)).toBeInTheDocument();
});

it("renders all navigation links", () => {
Expand All @@ -87,7 +87,7 @@ describe("Navbar", () => {

it("logo links to home page", () => {
render(<Navbar />);
const logo = screen.getByText(/StellarPulse/i);
const logo = screen.getByText(/Stellar Pulse/i);
expect(logo.closest("a")).toHaveAttribute("href", "/");
});

Expand Down
36 changes: 35 additions & 1 deletion frontend/src/__tests__/helpers.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ import {
truncateAddress,
isValidAmount,
timeUntil,
formatDate,
formatTimestamp,
calculatePayout,
calculateOdds,
bpsToPercent,
Expand Down Expand Up @@ -128,7 +130,7 @@ describe("isValidAmount", () => {
describe("timeUntil", () => {
beforeEach(() => {
vi.useFakeTimers();
// Pin "now" to a known Unix time: 2026-02-26T00:00:00Z = 1771977600
// Pin "now" to a known Unix time: 2026-02-26T00:00:00Z = 1772064000
vi.setSystemTime(new Date("2026-02-26T00:00:00Z"));
});

Expand Down Expand Up @@ -171,6 +173,38 @@ describe("timeUntil", () => {
});
});

// ── timestamp formatting ─────────────────────────────────────────────────────

describe("formatTimestamp", () => {
it("formats a Unix timestamp with date, time, and timezone", () => {
expect(
formatTimestamp(1772064000, {
locale: "en-US",
timeZone: "UTC",
timeZoneName: "short",
})
).toBe("Feb 26, 2026, 12:00 AM UTC");
});

it("uses the requested timezone for display", () => {
expect(
formatTimestamp(1772064000, {
locale: "en-US",
timeZone: "America/Los_Angeles",
timeZoneName: "short",
})
).toBe("Feb 25, 2026, 04:00 PM PST");
});

it("keeps formatDate on the same formatter", () => {
expect(formatDate(1772064000)).toBe(formatTimestamp(1772064000));
});

it("handles invalid timestamps", () => {
expect(formatTimestamp(Number.NaN)).toBe("Invalid date");
});
});

// ── calculatePayout ───────────────────────────────────────────────────────────

describe("calculatePayout", () => {
Expand Down
1 change: 1 addition & 0 deletions frontend/src/app/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ const inter = Inter({

const poppins = Poppins({
subsets: ["latin"],
weight: ["400", "500", "600", "700"],
variable: "--font-heading",
display: "swap",
});
Expand Down
10 changes: 8 additions & 2 deletions frontend/src/app/markets/[id]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,13 @@ import { useWallet } from "@/hooks/useWallet";
import { useToken } from "@/hooks/useToken";
import { pollMarketEvents } from "@/services/events";
import { getXlmBalance } from "@/services/soroban";
import { displayXLM, formatXLM, calculatePayout, truncateAddress } from "@/utils/helpers";
import {
displayXLM,
formatXLM,
calculatePayout,
truncateAddress,
formatTimestamp,
} from "@/utils/helpers";
import {
WIN_POINTS,
LOSE_POINTS,
Expand Down Expand Up @@ -319,7 +325,7 @@ export default function MarketDetailPage({
</span>
</div>
<span className="text-xs text-slate-600 shrink-0">
{new Date(evt.timestamp * 1000).toLocaleTimeString()}
{formatTimestamp(evt.timestamp)}
</span>
</div>
))}
Expand Down
32 changes: 28 additions & 4 deletions frontend/src/utils/helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -75,16 +75,40 @@ export function timeUntil(timestamp: number): string {
}

/**
* Format a Unix timestamp to a locale-aware date string.
* Format a Unix timestamp to a locale-aware date/time string.
*
* Uses the browser/user locale and timezone by default, while accepting an
* override for deterministic tests and places that need a specific zone.
*/
export function formatDate(timestamp: number): string {
return new Date(timestamp * 1000).toLocaleDateString("en-US", {
export function formatTimestamp(
timestamp: number,
options: {
locale?: string | string[];
timeZone?: string;
timeZoneName?: "short" | "long" | "shortOffset" | "longOffset" | "shortGeneric" | "longGeneric";
} = {}
): string {
const date = new Date(timestamp * 1000);
if (!Number.isFinite(timestamp) || Number.isNaN(date.getTime())) {
return "Invalid date";
}

return new Intl.DateTimeFormat(options.locale, {
year: "numeric",
month: "short",
day: "numeric",
hour: "2-digit",
minute: "2-digit",
});
timeZone: options.timeZone,
timeZoneName: options.timeZoneName ?? "short",
}).format(date);
}

/**
* Format a Unix timestamp to a locale-aware date/time string.
*/
export function formatDate(timestamp: number): string {
return formatTimestamp(timestamp);
}

/**
Expand Down