Skip to content
Merged
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
3 changes: 3 additions & 0 deletions .github/workflows/deploy.yml
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,9 @@ jobs:
run: |
docker buildx build --platform linux/amd64 \
-t ${{ secrets.DOCKER_USERNAME }}/hscodes:${{ github.run_number }} \
--build-arg VITE_AUTH0_DOMAIN=${{ secrets.AUTH0_DOMAIN }} \
--build-arg VITE_AUTH0_CLIENT_ID=${{ secrets.AUTH0_CLIENTID }} \
--build-arg VITE_AUTH0_AUDIENCE=${{ secrets.AUTH0_AUDIENCE }} \
--file Dockerfile \
--push .

Expand Down
3 changes: 3 additions & 0 deletions Autumn.SPA/.env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
VITE_AUTH0_DOMAIN=your-tenant.auth0.com
VITE_AUTH0_CLIENT_ID=your-client-id
VITE_AUTH0_AUDIENCE=autumnapi
1 change: 1 addition & 0 deletions Autumn.SPA/.gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ node_modules
dist
dist-ssr
*.local
.vite

# Editor directories and files
.vscode/*
Expand Down
99 changes: 99 additions & 0 deletions Autumn.SPA/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Autumn.SPA/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
"preview": "vite preview"
},
"dependencies": {
"@auth0/auth0-react": "^2.14.0",
"@tailwindcss/vite": "^4.1.18",
"lucide-react": "^0.564.0",
"react": "^19.2.0",
Expand Down
22 changes: 16 additions & 6 deletions Autumn.SPA/src/App.jsx
Original file line number Diff line number Diff line change
@@ -1,21 +1,32 @@
import { useState, useEffect } from "react";
import { api } from "./api";
import { useState, useEffect, useContext } from "react";
import { Auth0Context } from "@auth0/auth0-react";
import { api, setAuthToken } from "./api";
import Header from "./components/Header";
import Home from "./components/Home";
import SearchView from "./components/SearchView";
import CalcView from "./components/CalcView";
import BrowseView from "./components/BrowseView";
import Toast from "./components/Toast";
import CookieConsent from "./components/CookieConsent";

export default function App() {
const [mode, setMode] = useState("light");
const [view, setView] = useState("home");
const [query, setQuery] = useState("");
const [country, setCountry] = useState("NG");
const [countries, setCountries] = useState([]);
// Cross-view state for calculator prefill
const [calcInit, setCalcInit] = useState({ hscode: "", product: "" });

// Only use Auth0 context when env vars are configured
const authEnabled = !!(import.meta.env.VITE_AUTH0_DOMAIN && import.meta.env.VITE_AUTH0_CLIENT_ID);
const rawAuth = useContext(Auth0Context);
const auth = authEnabled ? rawAuth : null;

useEffect(() => {
if (!auth?.isAuthenticated || !auth.getAccessTokenSilently) return;
auth.getAccessTokenSilently().then(setAuthToken).catch(() => {});
}, [auth?.isAuthenticated]);

useEffect(() => {
document.documentElement.classList.toggle("dark", mode === "dark");
}, [mode]);
Expand All @@ -33,13 +44,11 @@ export default function App() {
setQuery("");
};

// Navigate from SearchView → CalcView with HS code prefilled
const goToCalc = (hscode, product) => {
setCalcInit({ hscode: hscode || "", product: product || "" });
setView("calculator");
};

// Navigate from CalcView → SearchView to find an HS code
const goToSearch = (product) => {
setQuery(product || "");
setView("results");
Expand All @@ -56,6 +65,7 @@ export default function App() {
mode={mode}
setMode={setMode}
onReset={onReset}
auth={auth}
/>

{view === "home" && (
Expand Down Expand Up @@ -100,10 +110,10 @@ export default function App() {
)}

<Toast />
<CookieConsent />

<footer className="border-t border-border px-6 py-4 text-center text-xs text-fg-dim">
© 2025 HS.Codes — Open source commodity classification.
<a href="https://github.com/samabos/hscodesdotnet" target="_blank" rel="noopener" className="text-accent ml-2 no-underline">GitHub →</a>
</footer>
</div>
);
Expand Down
19 changes: 12 additions & 7 deletions Autumn.SPA/src/api.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
const BASE = '/api';

let _token = null;
export const setAuthToken = (token) => { _token = token; };

const headers = () => _token ? { Authorization: `Bearer ${_token}` } : {};

const json = async (r) => {
if (r.status === 429) {
window.dispatchEvent(new CustomEvent('api:ratelimit'));
Expand All @@ -10,33 +15,33 @@ const json = async (r) => {

export const api = {
search: (keyword) =>
fetch(`${BASE}/search?keyword=${encodeURIComponent(keyword)}`).then(json),
fetch(`${BASE}/search?keyword=${encodeURIComponent(keyword)}`, { headers: headers() }).then(json),

browse: ({ code, parentCode, parentId, level } = {}) => {
const params = new URLSearchParams();
if (code) params.set('code', code);
if (parentCode) params.set('parentCode', parentCode);
if (parentId) params.set('parentId', parentId);
if (level != null) params.set('level', String(level));
return fetch(`${BASE}/browse?${params}`).then(json);
return fetch(`${BASE}/browse?${params}`, { headers: headers() }).then(json);
},

duty: ({ HSCode, Country, ProductDesc, Cost, Freight, Insurance, Currency }) =>
fetch(`${BASE}/duty?${new URLSearchParams({
HSCode, Country, ProductDesc: ProductDesc || '',
Cost: String(Cost), Freight: String(Freight),
Insurance: String(Insurance), Currency: Currency || 'USD'
})}`).then(json),
})}`, { headers: headers() }).then(json),

note: (hscode, country) =>
fetch(`${BASE}/note/${encodeURIComponent(hscode)}?country=${encodeURIComponent(country)}`).then(json),
fetch(`${BASE}/note/${encodeURIComponent(hscode)}?country=${encodeURIComponent(country)}`, { headers: headers() }).then(json),

countries: () =>
fetch(`${BASE}/codelist/countries`).then(json),
fetch(`${BASE}/codelist/countries`, { headers: headers() }).then(json),

currencies: () =>
fetch(`${BASE}/codelist/currency`).then(json),
fetch(`${BASE}/codelist/currency`, { headers: headers() }).then(json),

products: (query) =>
fetch(`${BASE}/codelist/products/${encodeURIComponent(query || '')}`).then(json),
fetch(`${BASE}/codelist/products/${encodeURIComponent(query || '')}`, { headers: headers() }).then(json),
};
67 changes: 67 additions & 0 deletions Autumn.SPA/src/components/CookieConsent.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import { useState, useEffect } from "react";

const GA_ID = "UA-159243847-1";
const CONSENT_KEY = "hs_cookie_consent";

function loadGA() {
if (document.getElementById("ga-script")) return;
const s = document.createElement("script");
s.id = "ga-script";
s.async = true;
s.src = `https://www.googletagmanager.com/gtag/js?id=${GA_ID}`;
document.head.appendChild(s);
window.dataLayer = window.dataLayer || [];
function gtag() { window.dataLayer.push(arguments); }
gtag("js", new Date());
gtag("config", GA_ID);
}

export default function CookieConsent() {
const [show, setShow] = useState(false);

useEffect(() => {
const consent = localStorage.getItem(CONSENT_KEY);
if (consent === "accepted") {
loadGA();
} else if (!consent) {
setShow(true);
}
}, []);

const accept = () => {
localStorage.setItem(CONSENT_KEY, "accepted");
loadGA();
setShow(false);
};

const decline = () => {
localStorage.setItem(CONSENT_KEY, "declined");
setShow(false);
};

if (!show) return null;

return (
<div className="fixed bottom-0 inset-x-0 z-50 p-4">
<div className="mx-auto max-w-2xl bg-surface border border-border rounded-xl shadow-lg px-5 py-4 flex flex-col sm:flex-row items-start sm:items-center gap-3 animate-[slideIn_0.3s_ease-out]">
<p className="text-sm text-fg-sec flex-1">
We use cookies to analyse site usage and improve our service.
</p>
<div className="flex gap-2 shrink-0">
<button
onClick={decline}
className="px-3 py-1.5 text-xs rounded-lg border border-border text-fg-dim hover:text-fg hover:border-border-hover transition-colors cursor-pointer"
>
Decline
</button>
<button
onClick={accept}
className="px-3 py-1.5 text-xs rounded-lg bg-accent text-btn-text hover:bg-accent-hover transition-colors cursor-pointer"
>
Accept
</button>
</div>
</div>
</div>
);
}
Loading