Initial commit

This commit is contained in:
zvspany
2026-03-07 16:34:10 +01:00
commit 48d3ac684f
524 changed files with 9352 additions and 0 deletions

75
app/api/rates/route.ts Normal file
View File

@@ -0,0 +1,75 @@
import { NextResponse } from "next/server";
import { fetchUnifiedRates } from "@/lib/api/normalize";
import type { RatesResponse } from "@/lib/rates";
const CACHE_TTL_MS = 300_000;
const CACHE_CONTROL_VALUE = "s-maxage=300, stale-while-revalidate=1800";
let cachedRates: RatesResponse | null = null;
let cacheTimestamp = 0;
let inFlightRequest: Promise<RatesResponse> | null = null;
export const revalidate = 300;
async function getRatesWithCache(): Promise<RatesResponse> {
const now = Date.now();
const hasFreshCache = cachedRates && now - cacheTimestamp < CACHE_TTL_MS;
if (hasFreshCache && cachedRates) {
return cachedRates;
}
if (inFlightRequest) {
return inFlightRequest;
}
inFlightRequest = (async () => {
const freshRates = await fetchUnifiedRates();
cachedRates = freshRates;
cacheTimestamp = Date.now();
return freshRates;
})();
try {
return await inFlightRequest;
} finally {
inFlightRequest = null;
}
}
export async function GET() {
try {
const data = await getRatesWithCache();
return NextResponse.json(data, {
status: 200,
headers: {
"Cache-Control": CACHE_CONTROL_VALUE
}
});
} catch (error) {
if (cachedRates) {
return NextResponse.json(cachedRates, {
status: 200,
headers: {
"Cache-Control": CACHE_CONTROL_VALUE,
"X-Cache-Fallback": "stale-on-error"
}
});
}
const message =
error instanceof Error
? error.message
: "Unexpected error while loading rates";
return NextResponse.json(
{
message
},
{
status: 500
}
);
}
}

78
app/globals.css Normal file
View File

@@ -0,0 +1,78 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
:root {
--background: 222 47% 8%;
--foreground: 213 31% 96%;
--card: 220 34% 11%;
--card-foreground: 210 38% 96%;
--popover: 221 36% 12%;
--popover-foreground: 210 38% 97%;
--primary: 193 92% 58%;
--primary-foreground: 221 39% 10%;
--secondary: 217 20% 18%;
--secondary-foreground: 210 37% 95%;
--muted: 218 18% 18%;
--muted-foreground: 217 17% 72%;
--accent: 202 88% 16%;
--accent-foreground: 198 100% 90%;
--border: 217 24% 24%;
--input: 217 24% 24%;
--ring: 192 89% 60%;
--radius: 0.9rem;
}
* {
@apply border-border;
}
html,
body {
height: 100%;
}
body {
@apply bg-background text-foreground;
font-family: "Manrope", "Avenir Next", "Segoe UI", sans-serif;
position: relative;
z-index: 0;
isolation: isolate;
overflow-x: hidden;
background-color: hsl(var(--background));
}
body::before {
content: "";
position: fixed;
inset: -15vh -15vw;
z-index: -1;
pointer-events: none;
background-image:
radial-gradient(
68% 58% at 50% -10%,
hsl(193 95% 58% / 0.24) 0%,
hsl(193 95% 58% / 0.13) 38%,
transparent 76%
),
radial-gradient(52% 64% at 102% 42%, hsl(162 90% 50% / 0.11) 0%, transparent 74%),
radial-gradient(52% 64% at -2% 78%, hsl(210 92% 56% / 0.1) 0%, transparent 74%),
linear-gradient(180deg, hsl(222 45% 7%) 0%, hsl(223 48% 6%) 100%);
transform: translateZ(0);
will-change: transform;
}
.font-heading {
font-family: "Space Grotesk", "Manrope", "Segoe UI", sans-serif;
}
@layer base {
h1,
h2,
h3,
h4,
h5,
h6 {
@apply font-heading;
}
}

24
app/layout.tsx Normal file
View File

@@ -0,0 +1,24 @@
import type { Metadata } from "next";
import "./globals.css";
import "currency-flags/dist/currency-flags.min.css";
export const metadata: Metadata = {
title: "NexCurrency | Modern Currency & Crypto Converter",
description:
"Convert fiat and crypto assets instantly with live rates, smart formatting, and a premium modern interface."
};
export default function RootLayout({
children
}: Readonly<{
children: React.ReactNode;
}>) {
return (
<html lang="en" className="dark" suppressHydrationWarning>
<body className="font-sans antialiased">
{children}
</body>
</html>
);
}

65
app/page.tsx Normal file
View File

@@ -0,0 +1,65 @@
"use client";
import { useCallback, useState } from "react";
import { ConverterCard } from "@/components/converter/converter-card";
import { Hero } from "@/components/sections/hero";
import { InsightsSection } from "@/components/sections/insights-section";
const DEFAULT_FROM = "USD";
const DEFAULT_TO = "EUR";
export default function HomePage() {
const [selectedFromCode, setSelectedFromCode] = useState(DEFAULT_FROM);
const [selectedToCode, setSelectedToCode] = useState(DEFAULT_TO);
const handleSelectPopularPair = useCallback(
(fromCode: string, toCode: string) => {
setSelectedFromCode(fromCode);
setSelectedToCode(toCode);
},
[],
);
const handlePairChange = useCallback((fromCode: string, toCode: string) => {
setSelectedFromCode(fromCode);
setSelectedToCode(toCode);
}, []);
return (
<main className="relative isolate min-h-[100svh] overflow-hidden pb-20">
<div className="container relative z-10">
<Hero />
<section className="mx-auto mt-10 max-w-5xl animate-in fade-in-0 slide-in-from-bottom-2 duration-700">
<ConverterCard
forcedFromCode={selectedFromCode}
forcedToCode={selectedToCode}
onPairChange={handlePairChange}
/>
<InsightsSection
selectedFromCode={selectedFromCode}
selectedToCode={selectedToCode}
onSelectPopularPair={handleSelectPopularPair}
/>
</section>
</div>
<footer className="container mt-12 flex flex-col items-center gap-2 text-center text-xs text-muted-foreground">
<p>
Market data is provided by Frankfurter and CoinGecko. Rates are
refreshed automatically.
</p>
{/*
<a
href="https://github.com/zvspany/nextcurrency"
target="_blank"
rel="noreferrer"
className="rounded-full border border-border/70 bg-background/50 px-3 py-1.5 text-xs font-medium text-foreground transition-colors hover:border-cyan-400/40 hover:bg-cyan-400/10 hover:text-cyan-100"
>
Repository
</a>
*/}
</footer>
</main>
);
}