diff --git a/frontend/package.json b/frontend/package.json
new file mode 100644
index 0000000..0b4a502
--- /dev/null
+++ b/frontend/package.json
@@ -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"
+ }
+}
diff --git a/frontend/src/__tests__/components/Navbar.test.tsx b/frontend/src/__tests__/components/Navbar.test.tsx
index 222a0ec..69d7287 100644
--- a/frontend/src/__tests__/components/Navbar.test.tsx
+++ b/frontend/src/__tests__/components/Navbar.test.tsx
@@ -70,7 +70,7 @@ describe("Navbar", () => {
it("renders the logo / brand name", () => {
render();
- expect(screen.getByText(/StellarPulse/i)).toBeInTheDocument();
+ expect(screen.getByText(/Stellar Pulse/i)).toBeInTheDocument();
});
it("renders all navigation links", () => {
@@ -87,7 +87,7 @@ describe("Navbar", () => {
it("logo links to home page", () => {
render();
- const logo = screen.getByText(/StellarPulse/i);
+ const logo = screen.getByText(/Stellar Pulse/i);
expect(logo.closest("a")).toHaveAttribute("href", "/");
});
diff --git a/frontend/src/__tests__/helpers.test.ts b/frontend/src/__tests__/helpers.test.ts
index c039046..7c8e243 100644
--- a/frontend/src/__tests__/helpers.test.ts
+++ b/frontend/src/__tests__/helpers.test.ts
@@ -4,6 +4,8 @@ import {
truncateAddress,
isValidAmount,
timeUntil,
+ formatDate,
+ formatTimestamp,
calculatePayout,
calculateOdds,
bpsToPercent,
@@ -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"));
});
@@ -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", () => {
diff --git a/frontend/src/app/layout.tsx b/frontend/src/app/layout.tsx
index 13a914a..06116fa 100644
--- a/frontend/src/app/layout.tsx
+++ b/frontend/src/app/layout.tsx
@@ -13,6 +13,7 @@ const inter = Inter({
const poppins = Poppins({
subsets: ["latin"],
+ weight: ["400", "500", "600", "700"],
variable: "--font-heading",
display: "swap",
});
diff --git a/frontend/src/app/markets/[id]/page.tsx b/frontend/src/app/markets/[id]/page.tsx
index 5ae3a63..917cd83 100644
--- a/frontend/src/app/markets/[id]/page.tsx
+++ b/frontend/src/app/markets/[id]/page.tsx
@@ -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,
@@ -319,7 +325,7 @@ export default function MarketDetailPage({
- {new Date(evt.timestamp * 1000).toLocaleTimeString()}
+ {formatTimestamp(evt.timestamp)}
))}
diff --git a/frontend/src/utils/helpers.ts b/frontend/src/utils/helpers.ts
index 4688130..e2ebb45 100644
--- a/frontend/src/utils/helpers.ts
+++ b/frontend/src/utils/helpers.ts
@@ -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);
}
/**