commit 02aaf61bb6887c6e77eaee9d216677c782526b20 Author: Alireza Hassani Date: Sat Aug 29 19:16:32 2026 +0330 Initial import: Majd Trading website Next.js 15 site for گروه بازرگانی بین المللی آریو مجد آسیا, rebuilt from majdtrading.com. Persian, English and Arabic; RTL and LTR. - Pages: home, about, contact, six product categories, blog index and detail - i18n: prose in per-locale JSON, structure in TypeScript, drift checked by `npm run i18n:check`. Blog stays single-language by design - Meshkee backend: blog content, tenant resolution, session via the customer portal, and /meshkee/sitemap-config.json - Live market data from livedata.ir, cached server-side once a minute - Self-hosted fonts: IRANYekan (fa), IBM Plex Sans Arabic (ar), Space Grotesk and BBH Bartle (Latin) Co-Authored-By: Claude Opus 5 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..760a9b2 --- /dev/null +++ b/.gitignore @@ -0,0 +1,11 @@ + +# Local dev preview config +*.tsbuildinfo +.DS_Store +.claude/ +.env*.local +.next/ +next-env.d.ts +node_modules/ +out/ +tsconfig.tsbuildinfo diff --git a/Image Sample/Screenshot 2026-08-25 at 23.04.50.png b/Image Sample/Screenshot 2026-08-25 at 23.04.50.png new file mode 100644 index 0000000..fd547e5 Binary files /dev/null and b/Image Sample/Screenshot 2026-08-25 at 23.04.50.png differ diff --git a/Image Sample/aron-yigin-sNY6B9NsPP8-unsplash.jpg b/Image Sample/aron-yigin-sNY6B9NsPP8-unsplash.jpg new file mode 100644 index 0000000..d04fb81 Binary files /dev/null and b/Image Sample/aron-yigin-sNY6B9NsPP8-unsplash.jpg differ diff --git a/Image Sample/caleb-gVhUdhGW5Wc-unsplash.jpg b/Image Sample/caleb-gVhUdhGW5Wc-unsplash.jpg new file mode 100644 index 0000000..fd3bdb4 Binary files /dev/null and b/Image Sample/caleb-gVhUdhGW5Wc-unsplash.jpg differ diff --git a/Image Sample/delfina-iacub-Jy465_AA0mE-unsplash.jpg b/Image Sample/delfina-iacub-Jy465_AA0mE-unsplash.jpg new file mode 100644 index 0000000..6817c1d Binary files /dev/null and b/Image Sample/delfina-iacub-Jy465_AA0mE-unsplash.jpg differ diff --git a/Image Sample/premium_photo-1661880889658-6c3ac991146f.avif b/Image Sample/premium_photo-1661880889658-6c3ac991146f.avif new file mode 100644 index 0000000..20cc1be Binary files /dev/null and b/Image Sample/premium_photo-1661880889658-6c3ac991146f.avif differ diff --git a/Image Sample/premium_photo-1749672969689-0ef0c6eae1a9.avif b/Image Sample/premium_photo-1749672969689-0ef0c6eae1a9.avif new file mode 100644 index 0000000..a6f61a5 Binary files /dev/null and b/Image Sample/premium_photo-1749672969689-0ef0c6eae1a9.avif differ diff --git a/README.md b/README.md new file mode 100644 index 0000000..e8d9d5f --- /dev/null +++ b/README.md @@ -0,0 +1,526 @@ +# Majd Trading — ARIO MAJD ASIA + +Next.js site for گروه بازرگانی بین المللی آریو مجد آسیا, rebuilt from +[majdtrading.com](https://www.majdtrading.com). Phase one ships three pages in +Persian (RTL); the routing and content layers are already locale-aware so English +(LTR) and Arabic can be added without restructuring. + +## Running it + +```bash +npm run dev +``` + +Then open http://localhost:3001 — `/` redirects to `/fa`. (Port 3001 rather than +3000, which the MeshkeeApp Backend occupies on this machine; change it in +`.claude/launch.json` and the `dev` script if that is not true for you.) + +`npm run build` produces the production build; `npm run start` serves it. + +## Pages + +| Route | Page | +| -------------- | ---------- | +| `/fa` | صفحه اصلی | +| `/fa/about` | درباره ی ما | +| `/fa/contact` | تماس با ما | +| `/fa/products/[slug]` | one page per product category (7) | + +Category slugs: `oil`, `mineral`, `food`, `chemical`, `livestock`, `appliance`, +`construction`. Each is prerendered from `products.items` in the dictionary, and +the same slug selects the page, its icon, and its header-menu entry. + +## Structure + +``` +src/ + app/[locale]/ # locale segment owns and + layout.tsx # header + footer shell, metadata + page.tsx # homepage: slider, services, intro, products, partners, CTA + about/page.tsx # about, vision, mission, values + contact/page.tsx # contact cards, form, map + components/ # Header, Slider, Footer, PageHero, Section, ContactForm + i18n/ + config.ts # locales, direction map, labels + dictionaries.ts # dictionary loader + dictionaries/fa.ts # all Persian copy + lib/site.ts # address, phones, emails, partners — shared across locales + middleware.ts # redirects un-prefixed paths to the default locale +public/ + images/ # slider, logo, service icons, partner logos + fonts/ # IRANYekan (self-hosted) +``` + +## Design tokens + +The palette is taken from the live site's `--wb-*` custom properties and is +defined once in `src/app/globals.css` under `@theme`: + +| Token | Value | Use | +| --------------- | --------- | --------------------------- | +| `--color-brand` | `#1A3851` | headings, header, dark bands | +| `--color-accent`| `#FF214F` | CTAs, active state, rules | +| `--color-muted` | `#838385` | secondary text | +| `--color-brand-500`| `#3E6684`| Latin title lines on light bands | +| `--color-brand-300`| `#A8BECD`| Latin lines and rules on dark bands | +| `--color-surface`| `#F6F7F9`| alternating section bands | +| `--color-accent` | `#FF214F` | **form validation only** — see below | + +Red is not a brand colour here. It appears in exactly three places, all inside +the contact form: the `*` on required fields, the inline error messages, and the +border of a field in error. Everything that used to be red — buttons, active nav, +rules, captions, hover states — is now navy, steel blue, or an invert to white on +dark bands. `grep -rn accent src` should only ever return `ContactForm.tsx`. + +### Type + +Three faces, all self-hosted in `public/fonts/` — no CDN call at runtime, which +matters for visitors on networks that cannot reach Google Fonts. + +| Face | Where | Class | +| ---- | ----- | ----- | +| **IRANYekan** | all Persian text | body default | +| **BBH Bartle** | Latin heading lines only — section headings, page heroes, the slider | `.latin-title` / `font-display` | +| **Space Grotesk** | Latin labels and figures — card captions, the brand line, list numbers | `.latin` | +| **Space Grotesk** | phone numbers, emails, the Instagram handle | `.latin-ui` | + +BBH Bartle is caps-only and ships a single weight, so `.latin-title` sets +`font-weight: 400` and never takes a bold utility — asking for one would give a +synthesised faux bold. It is also why emails do not use it: `ceo.majd@hotmail.com` +would render as `CEO.MAJD@HOTMAIL.COM`. That is what `.latin-ui` is for. + +Only `.latin-ui` sets `direction: ltr`. `.latin` and `.latin-title` isolate their +run but inherit the page direction, so `text-align: start` still resolves to the +right edge under RTL. Adding `direction: ltr` to them silently left-aligns every +caption while the Persian heading beside it stays right-aligned — the same trap +applies to `start-*` / `end-*` inset utilities on such an element. + +### Section headings + +`SectionHeading` and `PageHero` both set the Latin line *behind* the Persian +title as a faint backdrop, echoing the hero slider. It is decorative repetition +of the heading, so it is `aria-hidden`. An optional Persian `subtitle` sits +between the title and the rule. + +The backdrop is centred on the whole lockup, not on the title alone, and capped +at `clamp(1.75rem, 5.5vw, 3rem)` — sized to overhang the Persian title without +running past the section's own width. Raising that ceiling is what makes it +spill off both edges of the viewport. + +`balanceLines` in `src/lib/text.ts` breaks the Latin string across at most two +rows, split where the halves come closest to equal length, so the backdrop reads +as a block behind the lockup rather than one very wide line. Strings of ten +characters or fewer stay on one row — breaking `ABOUT US` would strand `US` on a +line of its own. + +It carries the `font-display` utility rather than `.latin-title`, because any +`direction: ltr` on a positioned element makes `inset-inline` and +`text-align: start` resolve against that element instead of the page — which +left-aligns the backdrop inside an RTL layout. + +Flat by design: no shadows, no rounded corners. Gradients appear only as +readability scrims over photography. Section separation comes from 1px hairline +grids (`gap-px` over a `bg-brand-100` parent) and alternating background bands. + +Photography is used three ways, each with a scrim so type stays legible over any +image. The hero slider and the service tiles share one treatment: the scrim rises +from the bottom edge and clears by mid-frame, so the top of every photo stays +open and the type sits in the dark lower half. The full-bleed products band uses +a vertical vignette instead — dark where the heading and chips sit, open through +the middle. + +## Multi-language + +Three languages for the static pages (`fa`, `en`, `ar`); the blog stays +single-language. + +### Where things live + +``` +src/content/index.ts ids, slugs, image paths, ordering ← developer +src/i18n/locales/fa.json prose only ← translator +src/i18n/locales/en.json +src/i18n/locales/ar.json +src/i18n/types.ts the Dictionary shape +src/i18n/compose.ts joins structure to copy on the id +``` + +The split is by **who edits it**. Image paths and slugs are identical in every +language, so they live once in TypeScript — where slugs also keep their literal +types, which is what lets `ProductIcon name={product.slug}` fail the build if a +category has no icon behind it. Translators only ever open a JSON file of +sentences. + +`src/i18n/locales/.ts` is a three-line wrapper that assigns the JSON to +`Dictionary`, so a **missing** key fails the build. + +### The fa → en/ar workflow + +```bash +npm run i18n:check +``` + +Run it after editing `fa.json`. The type annotation already catches missing keys; +this catches the two things it cannot see — **stale** keys left behind after a +rename, and **arrays whose lengths have drifted** (a mission list of 14 items in +one language and 13 in another renders perfectly and is wrong). + +Partner organisation names live in the locale files too, keyed by the ids in +`src/lib/site.ts` — these bodies have official names in all three languages, so +leaving them in Persian on the English page was wrong. Only the logo and link +stay structural. + +### The blog is outside all of this + +Posts live in `src/lib/posts.ts` with their own `lang` and `dir`. One rule: +**page chrome follows the visitor's locale, the article follows the post.** So +`/en/blog/cargo-insurance` serves an English header and footer, in Space Grotesk, +around a Persian article marked `lang="fa" dir="rtl"` and set in IRANYekan. + +The homepage carousel is rendered only where the visitor's locale matches the +language of the posts (`post.lang === locale`), so `/en` and `/ar` omit the +section rather than showing Persian cards. Adding an English post later makes it +appear on `/en` with no code change. + +Because one article is reachable from three locale paths, each post sets +`alternates.canonical` to its default-locale URL. Static pages instead declare +`hreflang` alternates, since those genuinely are translations of one another. + +### Fonts per language + +| Locale | Body face | +| ------ | --------- | +| `fa` | IRANYekan | +| `ar` | IBM Plex Sans Arabic | +| `en` | Space Grotesk | + +IBM Plex Sans Arabic was chosen over IRANYekan's Arabic coverage: IRANYekan is a +Persian typeface and its Arabic shaping reads foreign. Plex is a corporate face +with a real weight range that sits comfortably beside Space Grotesk. Only the +Arabic subset is downloaded (~34KB per weight). + +Two things make the switching work, and both are easy to break: + +- The `[lang="…"]` rules redefine `--font-sans` **and** set `font-family`. + Redefining the variable alone is not enough — `body` declares + `font-family: var(--font-sans)` itself, so a rule on `` never reaches it. + Setting `font-family` alone is not enough either — the nested blog article + needs the variable rebound for its own subtree. +- They are attribute selectors, not `:lang()`. They match only elements that + actually carry `lang` (the `` element and a blog article), so everything + else inherits and Tailwind's `font-display` utility still wins on the Latin + backdrops. + +`ss01` (Persian digit forms) is keyed on `[lang="fa"]`, not on direction — +Arabic is also RTL but uses different digit shapes. + +### English has no Latin gloss + +Every `*Latin` field is an empty string in `en.json`, and the components skip the +backdrop when it is empty. The device exists to pair a non-Latin heading with an +English one; in English it would print the heading twice. Filling those fields in +turns the backdrops back on. + +## Market data + +A framed panel on the homepage, in all three languages. Server-side, entirely in +this repo (`src/lib/market/`). + +| File | What it is | +| ---- | ---------- | +| `displayed.ts` | **the one list to edit** — which figures appear, in order | +| `instruments.ts` | catalogue of every id the upstream publishes (39 identified) | +| `livedata.ts` | the source adapter — three parsers, see below | +| `index.ts` | caching, failure handling | +| `types.ts` | the `MarketSource` interface | + +The frame has three panes: the id-based figures, the gold table, and world +exchange status. Labels are ours, translated in each locale file — only the +numbers come from upstream. + +The gold table's last row is the world ounce, which is *not* part of that table +upstream — it is read from its own id (`300106`) and appended. That id is +deliberately absent from `displayed.ts`; listing it there too would print the +same figure twice in one frame. Its Toman cell shows an em dash, because +upstream publishes no Toman price for the ounce and deriving one from the gram +rate would be inventing a figure. + +### Three parsers, three levels of fragility + +- **Figures** (gold ounce, Brent, USDT, exchange USD/EUR) are anchored on stable + numeric ids (`s_200101`). The page is ad-heavy and its layout shifts, but an id + has a fixed meaning. Most robust. +- **Gold by purity** has no ids — it is anchored on the section heading and each + row's fixed Persian label. If upstream renames a row, that row drops out and + the others survive. +- **Exchange sessions** are anchored on `class="nma"` plus the status icon class. + +Session state is taken from upstream rather than computed from opening hours, +because upstream **accounts for public holidays** — it reports the actual +reopening date, which a hours-only calculation would get wrong every holiday. +Those dates are reformatted into each reader's own calendar: `Aug 31` in English, +`شهریور ۹` in Persian, `أغسطس ٣١` in Arabic. + +### Why server-side, and why 60s + +livedata.ir sends no `Access-Control-Allow-Origin` (verified with an +`Origin: https://majdtrading.com` request), so a browser fetch is blocked +outright. Rendering on the server also means the upstream is hit **once per +minute for the whole site**, not once per visitor. + +60s and not 30s because the upstream regenerates on the minute — measured at +`04:29:02`, `04:30:02`, `04:30:02` across samples 25s apart. Polling faster +doubles their load for no extra freshness. + +### Presentation notes + +Table headers and their values both align to the page's start edge, and +`dir="ltr"` sits on an inner span around each number rather than on the cell. +Putting it on the cell changes what `text-end` resolves to, which left every +header sitting on the opposite side of its own column from the values beneath it. + + +Upstream groups some figures (`29,136,700`) and not others (`199000`). The +component groups the ungrouped ones so the frame is internally consistent; the +digits themselves are never altered. + +### Swapping the source + +`index.ts` has one line — `const source: MarketSource = livedataSource`. When the +company backend exists, add `backend.ts` implementing `MarketSource` and change +that line. The component, the caching and the selection list stay as they are. + +`getMarketSnapshot()` never throws: an unreachable upstream logs and returns +null, so a build never depends on a third party being up and a visitor never +sees a broken widget. + +### Before this goes live + +The figures are compiled by livedata.ir, whose site is funded by advertising. +The frame carries visible attribution and a link back, but **permission is still +worth getting in writing** — they sell ad space and have a contact page, so a +licence or attribution agreement is a realistic ask. + +## Meshkee backend + +Contract: https://api.meshkee.com/docs/website · base `https://api.meshkee.com/api/v1` + +`src/lib/api.ts` holds the base URL, the tenant domain and a `apiGet` helper that +**returns null instead of throwing** — a marketing page must still render when +the API is down, so every caller degrades to hiding its section. + +`GET /tenants/majdtrading.com` confirms the tenant: business id 25, +`primaryColor: "dark-blue"`, `defaultLocale: "fa"`, modules +`products, blog, finance`. Logo and favicon are still null upstream, so the site +keeps using the local logo files. + +### Blog + +Now served from the backend (`src/lib/blog.ts`), replacing the placeholder posts. + +The two endpoints disagree in shape and both are normalised behind one type: + +| | | +| --- | --- | +| `GET /tenants/{domain}/blogs` | `{ items: [...] }` | +| `GET /tenants/{domain}/blogs/{slug}` | `{ blog: {...} }` | + +Bodies arrive as HTML from the Meshkee editor and are styled by `.prose-brand` +in `globals.css` rather than a typography plugin. The editor emits inline +`float`/`margin` on images, which that rule overrides so images stay in the flow. + +Media comes from `*.parspack.net`, allowlisted in `next.config.ts` — +`next/image` renders an empty box for any host not listed there. + +### Accounts + +Sign-in happens on `customer.{domain}`; this site never sees a password and never +calls `POST /auth/login`. It reads a session token from a cookie, asks +`GET /auth/me` who it belongs to, and renders the name with the honorific plus a +dropdown (profile / sign out, and *manage website* for an administrator). + +**Two things here are unverified and are the first place to look if it misbehaves:** + +- **The cookie.** The API authenticates with a Bearer JWT, which portals usually + keep in localStorage — and localStorage is *not* shared across subdomains. For + this to work, the portal must write the token to a cookie scoped to the apex + (`.majdtrading.com`). Whether it does, and under what name, is not documented. + `SESSION_COOKIE` in `src/lib/auth.ts` is the single place to correct. +- **The `/auth/me` shape.** It has no response schema in the OpenAPI document and + needs a token we do not hold, so it could not be sampled. The parser is + deliberately shape-tolerant across the plausible spellings of name and role. + +Until the cookie name matches, `getSession()` returns null and the header shows +the sign-in link — the correct fallback, just not the whole feature. + +`auth.ts` is `server-only`; the client-safe links live in `portal.ts`. Importing +the former from a client component pulls `next/headers` into the browser bundle +and fails the build. + +### The cost of a personalised header + +Reading cookies in the locale layout opts **every page** into on-demand +rendering. The build table still prints ● against most routes, but that reflects +`generateStaticParams`, not prerendering: `prerender-manifest.json` lists exactly +one route (`/_not-found`). Fetch-level caching is unaffected — the market data is +still fetched once a minute for the whole site — but HTML is no longer prebuilt. + +The alternative is to fetch the session client-side and keep the pages static, at +the cost of a brief signed-out flash. Worth revisiting once the cookie mechanism +is confirmed. + +## SEO and sitemap + +Per the Meshkee SEO brief. + +**No `public/sitemap.xml` or `public/robots.txt`** — nginx proxies both to the +Meshkee API. Adding local copies would shadow the real ones. + +What this site publishes instead is `GET /meshkee/sitemap-config.json` +(`src/app/meshkee/sitemap-config.json/route.ts`): the static pages the API cannot +enumerate. It is generated from the same `locales` and `productCategories` +constants the app renders from, so it cannot drift from what exists. CMS content +reaches the sitemap through the API's own child sitemaps. + +*After deploying, run: business dashboard → Website → Settings → Sync sitemap +config.* + +### Canonical blog URLs + +`/{locale}/blog/{id}/{titleSlug}`, resolved through `GET .../blogs/by-id/{id}`. + +The slug segment is decorative — the id resolves the page — and is built with +`slugify()` from the title. **The API's own `slug` field is deliberately not +used**: on real data it holds legacy values like `item-old-2945`, which the brief +rules out of public URLs. A request whose slug has drifted 307s to the canonical +form. + +Two encoding traps live in that redirect, both of which surface as a 500 rather +than anything obvious: + +- Next hands the route param **percent-encoded**, so it must be decoded before + comparing against a Persian slug — otherwise every request looks like a drift. +- A `Location` header may not carry raw non-ASCII, so the redirect target must be + re-encoded on the way out. + +### On-page + +Every page carries a unique title and meta description, and **exactly one `

`** +— audited across all locales. The homepage needed one added: its hero is a +rotating slider whose captions are `h2`s, so the document had no `h1` at all. It +ships `sr-only`, keeping the outline correct without inventing a visible title +the design does not have. + +## Grids that follow their data + +`lgColumns()` in `src/lib/grid.ts` maps an item count onto a static +`lg:grid-cols-N` class. Tailwind extracts class names as literal strings, so +`lg:grid-cols-${n}` never reaches the stylesheet — hence the lookup rather than +interpolation. + +Used by the homepage product row and the "other categories" row on each product +page. Both had drifted: removing the home appliances category left six items in a +seven-column grid, and five "others" in a six-column one, each with a dead +column at the end. Deriving the count means that cannot recur. + +## Known gaps + +- **The contact form has no backend.** `ContactForm.tsx` validates on the client + and then shows the success message without sending anything. Point it at a + server action or mail endpoint before launch — the spot is marked with a comment. +- **Content is homepage/about/contact only.** Products, services, news and + catalog exist on the live site as separate pages; they appear here as summary + sections, not as their own routes. +- The map on the contact page is an OpenStreetMap embed centred on Sepah Square, + Qom, with a link out beneath it. +- **Partner names are not sourced from majdtrading.com** — that site carries none + (every `title` attribute is just the URL and every `alt` is the company name). + The logo-to-link mapping in `src/lib/site.ts` is taken from its markup and is + reliable. The names are the organisations' own: ICCIMA and ISIPO were confirmed + from their sites' own titles, IRICA is legible in its logo. Qom Chamber, Iran + Fair and the ministry entry (`dotic.ir`) could not be reached from here and are + named from their logos and naming convention — worth a check before launch. +- **The backend has no published blog posts** for majdtrading.com + (`/tenants/majdtrading.com/blogs` returns `total: 0`), so the homepage carousel + is hidden and `/{locale}/blog` shows its empty state. Both fill in on their own + once posts are published — no code change. +- Home appliances was removed as a product category. The phrase still appears in + the About copy, because that is the company's own description of what it + trades, taken verbatim from majdtrading.com. +- **The product category copy is mine, not the client's.** The intro paragraph + and goods list on each of the seven pages were written for this build. They are + plausible for an Iranian trading company but are not sourced from + majdtrading.com — the client must confirm what they actually trade before + launch. Everything else on the site is their own copy. +- The homepage contact frame repeats the address, phones and email that the + footer shows directly beneath it. Fine if the frame is meant to be the closing + call to action, but it is duplication worth a decision. + +## Assets + +Logo, slider photography, service icons and partner logos were downloaded from +majdtrading.com into `public/images/`. IRANYekan — the same family the live site +loads from a CDN — is self-hosted in `public/fonts/`, alongside Space Grotesk. + +`public/images/sample/` holds the photos from the `Image Sample/` folder, +re-encoded to sRGB JPEG at web sizes. Which slot each one fills is set in +`src/i18n/dictionaries/fa.ts` (`services.items[].image` and +`products.background`) — swapping one is a one-line change. + +Landscape frames go to the full-width slots, portrait frames to the tiles, so +nothing is cropped against its grain. + +| File | Slot | Source | +| ---- | ---- | ------ | +| `deck-containers.jpg` | slide 1 + products band | `premium_photo-1749672969689…` | +| `aerial-vessel.jpg` | slide 2 + service tile — IMPORT AND EXPORT | `premium_photo-1661880889658…` | +| `container-canyon.jpg` | slide 3 + service tile — TECHNICAL SERVICES | `caleb-…` | +| `port-vessel.jpg` | service tile — TRANSPORT | `delfina-iacub-…` | +| `container-stack.jpg` | service tile — DESIRED PRODUCTS | `aron-yigin-…` | + +The header carries a products menu whose dropdown lists the seven categories. It +opens on hover *and* on keyboard focus, which is why the panel is hidden with +`opacity`/`visibility` rather than `display: none` — a panel that is not rendered +cannot take focus, so `group-focus-within` would never fire. Each entry targets +its own card (`#product-oil`, `#product-mineral`, …); the cards carry +`scroll-mt-28` so the sticky header does not cover the one you land on. + +There are no per-category pages yet, so those links jump within the homepage. If +categories become their own routes later, the hrefs in `Header.tsx` are the only +thing to change. + +The seven product categories render as a grid of cards, each with a minimal line +icon above its label. The icons live in `src/components/ProductIcon.tsx` and are +selected by the `icon` key on each entry in `products.items`; they draw in +`currentColor`, so a card recolours its icon along with its text on hover. Adding +a category means adding a shape to that file and naming it in the dictionary — +the union type will flag a name with no shape behind it. + +The slider is three slides, set in `slider` in the same dictionary file. Each +slide carries a Persian `description` shown under its title, and each service +item carries one that is hidden until the tile is hovered — revealed by animating +a wrapper grid row from `0fr` to `1fr` (so no fixed height is baked in) while the +paragraph fades and slides up. Each +slide's `subtitle` is split on spaces and set one word per line as an oversized, +10%-opacity backdrop behind the Persian headline — so `"ARIO MAJD ASIA"` stacks +as ARIO / MAJD / ASIA. It is `aria-hidden`, being decorative repetition of the +heading. Note that the positioning wrapper stays in the RTL flow and only the +words carry `font-display`: putting `.latin-title` (which sets `direction: ltr`) +on the positioned element makes `start-0` resolve against *its own* direction and +anchor to the wrong edge. + +### Licensing — action needed before launch + +`aerial-vessel.jpg` and `deck-containers.jpg` come from the two `.avif` files, +which are **watermarked Unsplash+ preview downloads**. Both now carry slides in +the hero, where the tiled `Unsplash+` watermark is plainly visible at full width +— it is the most prominent thing on the page. These must be replaced with +licensed downloads before launch. Same filenames, no code change. + +`Screenshot 2026-08-25 at 23.04.50.png` was left unused: it is a screen capture +of an Unsplash page, complete with browser UI and a tooltip, and is watermarked +as well. + +`container-stack.jpg` is only 640px wide — the largest the source file offers. +It is slightly soft on a high-DPI screen in its tile; a larger re-download would +fix it. diff --git a/next.config.ts b/next.config.ts new file mode 100644 index 0000000..28f8871 --- /dev/null +++ b/next.config.ts @@ -0,0 +1,16 @@ +import type { NextConfig } from "next"; + +const nextConfig: NextConfig = { + reactStrictMode: true, + images: { + // Blog artwork is served from the Meshkee media host, so next/image needs + // it allowlisted. Anything not listed here renders as an empty box. + remotePatterns: [ + { protocol: "https", hostname: "*.parspack.net" }, + { protocol: "https", hostname: "cdn.meshkee.com" }, + { protocol: "https", hostname: "*.meshkee.com" }, + ], + }, +}; + +export default nextConfig; diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..fbe4e9a --- /dev/null +++ b/package-lock.json @@ -0,0 +1,1602 @@ +{ + "name": "majd-trading", + "version": "0.1.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "majd-trading", + "version": "0.1.0", + "dependencies": { + "next": "^15.5.0", + "react": "^19.1.0", + "react-dom": "^19.1.0", + "server-only": "^0.0.1" + }, + "devDependencies": { + "@tailwindcss/postcss": "^4.1.0", + "@types/node": "^22", + "@types/react": "^19", + "@types/react-dom": "^19", + "tailwindcss": "^4.1.0", + "typescript": "^5.7.0" + } + }, + "node_modules/@alloc/quick-lru": { + "version": "5.2.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@emnapi/runtime": { + "version": "1.11.3", + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@img/colour": { + "version": "1.1.0", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/@img/sharp-darwin-arm64": { + "version": "0.35.4", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-darwin-arm64": "1.3.3" + } + }, + "node_modules/@img/sharp-darwin-x64": { + "version": "0.35.4", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.35.4.tgz", + "integrity": "sha512-hWniXY3bG5qKpkKrAwPe4y+VTPmf086YQAnkxWh7uA1YrlRouWGa0M0Mxj3ZjnXFkv7/TD1bTy9lGUK26vRvWw==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-darwin-x64": "1.3.3" + } + }, + "node_modules/@img/sharp-freebsd-wasm32": { + "version": "0.35.4", + "resolved": "https://registry.npmjs.org/@img/sharp-freebsd-wasm32/-/sharp-freebsd-wasm32-0.35.4.tgz", + "integrity": "sha512-lIsKw/BU+kjB4eZjxrYrZmwOJYi3Ajrv66iAlBmUPyKc3HpnloevB1g3wxGD9P/5BbQ1brBGl65VRRrCvQDEqA==", + "license": "Apache-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "dependencies": { + "@img/sharp-wasm32": "0.35.4" + }, + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-darwin-arm64": { + "version": "1.3.3", + "cpu": [ + "arm64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "darwin" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-darwin-x64": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.3.3.tgz", + "integrity": "sha512-FVJZ5mITMobmXIz/hPDTw0EintTW5H3WfrxwLqEqjiIihlu+hVRyGrFQ60xl0Lxn7Bt3zdpevPaQi0HEzqz9fw==", + "cpu": [ + "x64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "darwin" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-arm": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.3.3.tgz", + "integrity": "sha512-3rbU4vqXXc3hY/OiXdl52xZvT0F1yEngWfvqudtPJg/KkyiaQw2DRsFrNzpmLvfavbwOq3qXn36GP8obHRULQA==", + "cpu": [ + "arm" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-arm64": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.3.3.tgz", + "integrity": "sha512-0DaL0A6Xu6sQSQFwe4iVCrKWU2cCTItnRsYsCdxAMm9NF6twAA9BKnoqy4hqz4+azQ0JHuA26qiUKsf1XJ/v5A==", + "cpu": [ + "arm64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-ppc64": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-ppc64/-/sharp-libvips-linux-ppc64-1.3.3.tgz", + "integrity": "sha512-cdn1OvUBwsXhbC0zSzJnNzf5MZ/mTrobawDvNXBTxe8VtqKAm0sRuEY2Evzovb/w9JMk4TvRxqt1mekSuJz64w==", + "cpu": [ + "ppc64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-riscv64": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-riscv64/-/sharp-libvips-linux-riscv64-1.3.3.tgz", + "integrity": "sha512-HjPVx7yKz+0lqdhDlTw1tt90wamBoxhiXpvl1XZpJLiHH4RCJ5yDTqH+VlYPv2fwFs89JFw4c1IexYOcQUi4IQ==", + "cpu": [ + "riscv64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-s390x": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.3.3.tgz", + "integrity": "sha512-neWLh+3yCNThxnfy3c4BbVBeGgt9aftno+XbT56iK28RgeDs3UOFWviLWlUu0bArYVYJaFDK+RRohbicUNCm8Q==", + "cpu": [ + "s390x" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-x64": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.3.3.tgz", + "integrity": "sha512-4vKmvAst9nrowcqquKFAyZJUDolUaIp8uRiN0mWFguJ1IplC9/pitXtlnnlU4aa/eJw3J7i67V+pwUL+wZGdsA==", + "cpu": [ + "x64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linuxmusl-arm64": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.3.3.tgz", + "integrity": "sha512-Y9kQaLMuNoB0bPYOOdcZMaseNrFpPodIWWMrx+CZyydf2xn68j9WYc6sWWRrDwNkzCQjKYfc68L7jKjGlHMibw==", + "cpu": [ + "arm64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linuxmusl-x64": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.3.3.tgz", + "integrity": "sha512-fj8Mv0HHfD1Rr+4I68+3agJynxDWtBFgicTbSOb9Bke6pIwzGcJ+RX/yHjmiEGFMCavY/dxvem7MyNaJF+wDiw==", + "cpu": [ + "x64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-linux-arm": { + "version": "0.35.4", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.35.4.tgz", + "integrity": "sha512-7OAS8gI0EReKGVN2HssHlM6umJgxF5VI3xN0p9FA91p/YO+ou5hiNghLdZ5BEHztwaaK5+bLKRf8x/o2L2nk9A==", + "cpu": [ + "arm" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-arm": "1.3.3" + } + }, + "node_modules/@img/sharp-linux-arm64": { + "version": "0.35.4", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.35.4.tgz", + "integrity": "sha512-De4jpEnAU8Hd5oT0j1G3uL4ZvTuipVMn7YC6vPaJhy6/7EwEae0SVAoBrUMYQbkLGDm85taVWwuPc1a44LTzCQ==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-arm64": "1.3.3" + } + }, + "node_modules/@img/sharp-linux-ppc64": { + "version": "0.35.4", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-ppc64/-/sharp-linux-ppc64-0.35.4.tgz", + "integrity": "sha512-2oYZJeIl4kCcMGk4ouZVjnkCtFrpQFlNEtJ6GbxzhHQchwH0NH/qEb9ykmOl29dqwMq+JhFdZn+1ak2FKhI9fQ==", + "cpu": [ + "ppc64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-ppc64": "1.3.3" + } + }, + "node_modules/@img/sharp-linux-riscv64": { + "version": "0.35.4", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-riscv64/-/sharp-linux-riscv64-0.35.4.tgz", + "integrity": "sha512-cPbNChoRURAWdebDIHSenxRpgEdy7JkPydSnUxRm9VvKD7m0/xVaR/8Fzlu81pk5nHEvHH87UZUA7cTtwnbJSA==", + "cpu": [ + "riscv64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-riscv64": "1.3.3" + } + }, + "node_modules/@img/sharp-linux-s390x": { + "version": "0.35.4", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.35.4.tgz", + "integrity": "sha512-RY0JFY8Fd6RonCBtHz+DvadaPkXDSI1AUn6yWL9TipqkZ1vY8w8evqdgyDFnkm4/K1ve1TvZiaePP5oSd4+WVQ==", + "cpu": [ + "s390x" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-s390x": "1.3.3" + } + }, + "node_modules/@img/sharp-linux-x64": { + "version": "0.35.4", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.35.4.tgz", + "integrity": "sha512-9qvvEAuk8k89TfWUoX2htWjbAMX8p+NxCppjpcg5k6xMsjhBQPTsoIh36h9Qde4WRuGpJeYnOjdosDn/cnv+OA==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-x64": "1.3.3" + } + }, + "node_modules/@img/sharp-linuxmusl-arm64": { + "version": "0.35.4", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.35.4.tgz", + "integrity": "sha512-KB5jxpfWQTr0nc3xdHtWChdbifHrBGsd2SM62Eyxrl8afikm+f5qGBU75SJIZBT/S1MC8XyacdlXBMSWq6OURA==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linuxmusl-arm64": "1.3.3" + } + }, + "node_modules/@img/sharp-linuxmusl-x64": { + "version": "0.35.4", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.35.4.tgz", + "integrity": "sha512-f+eZJZIQNEEd26RPSW+76chwOf1XtA2Y/O+5ocVyLliHkeih3e+jhLVBdNTd2rS3IbNXK8+ug93Vf5ZXtF5Lxg==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linuxmusl-x64": "1.3.3" + } + }, + "node_modules/@img/sharp-wasm32": { + "version": "0.35.4", + "license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT", + "optional": true, + "dependencies": { + "@emnapi/runtime": "^1.11.3" + }, + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-webcontainers-wasm32": { + "version": "0.35.4", + "resolved": "https://registry.npmjs.org/@img/sharp-webcontainers-wasm32/-/sharp-webcontainers-wasm32-0.35.4.tgz", + "integrity": "sha512-ESfNkywmCfPNyaZjxooddJQiQ+l/nTpGEOGthxiLnIHXC/CmcBixnfwUleX9mCz9ovrUUvKMap/pm8RYbzfwaA==", + "cpu": [ + "wasm32" + ], + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "@img/sharp-wasm32": "0.35.4" + }, + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-arm64": { + "version": "0.35.4", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-arm64/-/sharp-win32-arm64-0.35.4.tgz", + "integrity": "sha512-iNdlBX9gLVvqe2I3uIJSIKTq6wckP/DYxZtcqxm09x5Gi24DnFBmPAWZmr60ZyYMG0xlzo6goG3670ar+RXvRw==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-ia32": { + "version": "0.35.4", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.35.4.tgz", + "integrity": "sha512-kqRsbaa5CS6KHlpxnN7WhE6vAAugXyZButpRdvDWetlv6Qv4N9WTcrWzF7tXfB9T7MsoadqdI8hmwLq6UlLvtw==", + "cpu": [ + "ia32" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-x64": { + "version": "0.35.4", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.35.4.tgz", + "integrity": "sha512-XtmnYhBcrORsJ4XJngyzr/EWP0hRZLAZRFaApdKuviyqF78+ylxh2y06ZmtULAMOnObJ3ucpN0AcwSWnMowTRg==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.6.0", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@next/env": { + "version": "15.5.24", + "license": "MIT" + }, + "node_modules/@next/swc-darwin-arm64": { + "version": "15.5.24", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-darwin-x64": { + "version": "15.5.24", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-15.5.24.tgz", + "integrity": "sha512-9HrQajBMmGcrrrvDfRimiCrbAPh3E6uHJmwBovYr6Yrmi9p9PZqI876BrXX280wICh3o2XwUlp4blkB0NNBqFg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-arm64-gnu": { + "version": "15.5.24", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-15.5.24.tgz", + "integrity": "sha512-rl9LSfE75si0WT3cDgdUC1XYCKS+TgxC+/IjitmeycrAG18X/plIP1/vy8dd/HPycYcIvE688PD7FuvEAiEAew==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-arm64-musl": { + "version": "15.5.24", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-15.5.24.tgz", + "integrity": "sha512-TlNAnpsjxSF3aAUtqnfmtXXf8m9sIDBlmF3c7bTAlnshUYu2U0OxN2uf5d0gcFwqHVEdivJNBcCaqNOwPGNimw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-x64-gnu": { + "version": "15.5.24", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-15.5.24.tgz", + "integrity": "sha512-7dwtlhr0SLndqTG1z9ncRkbJswDZiKWlxzFyXDvJ2RDZRDRHp8zyMJ4D9UH/FgnQeXxxB6gZy2pMcIUoNKQ4pA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-x64-musl": { + "version": "15.5.24", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-15.5.24.tgz", + "integrity": "sha512-kGZxM+WhkYs0276lFrMkj7PRtXT3Btp6cwvfSO/cCVLxJJttB5Ccnl2niaCgUja8HgSbEVnMHpg3FJWoOJ9e/g==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-win32-arm64-msvc": { + "version": "15.5.24", + "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-15.5.24.tgz", + "integrity": "sha512-jBDDkZ/qKAqkWivWDMkJSXUzbzV0QKRBKJjEHUAvSB97Hzw7NLzJ6yV56Lts/wjir7s4P31GYgpbS6ZL+hasAA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-win32-x64-msvc": { + "version": "15.5.24", + "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-15.5.24.tgz", + "integrity": "sha512-JqtwjvvorjacQ0spgjmUJoxySoYgPwdT1sFdQ0/zmW4iMlP2hjYlCoJIyS7o6Epb4Fug8eco3HXFoBDUCDeH7Q==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@swc/helpers": { + "version": "0.5.15", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.8.0" + } + }, + "node_modules/@tailwindcss/node": { + "version": "4.3.3", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/remapping": "^2.3.5", + "enhanced-resolve": "^5.24.1", + "jiti": "^2.7.0", + "lightningcss": "1.32.0", + "magic-string": "^0.30.21", + "source-map-js": "^1.2.1", + "tailwindcss": "4.3.3" + } + }, + "node_modules/@tailwindcss/oxide": { + "version": "4.3.3", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 20" + }, + "optionalDependencies": { + "@tailwindcss/oxide-android-arm64": "4.3.3", + "@tailwindcss/oxide-darwin-arm64": "4.3.3", + "@tailwindcss/oxide-darwin-x64": "4.3.3", + "@tailwindcss/oxide-freebsd-x64": "4.3.3", + "@tailwindcss/oxide-linux-arm-gnueabihf": "4.3.3", + "@tailwindcss/oxide-linux-arm64-gnu": "4.3.3", + "@tailwindcss/oxide-linux-arm64-musl": "4.3.3", + "@tailwindcss/oxide-linux-x64-gnu": "4.3.3", + "@tailwindcss/oxide-linux-x64-musl": "4.3.3", + "@tailwindcss/oxide-wasm32-wasi": "4.3.3", + "@tailwindcss/oxide-win32-arm64-msvc": "4.3.3", + "@tailwindcss/oxide-win32-x64-msvc": "4.3.3" + } + }, + "node_modules/@tailwindcss/oxide-android-arm64": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.3.3.tgz", + "integrity": "sha512-Y85A2gmPSkl5Ve5qR86GL4HT509cFqQh1aes9p3sSkyTPwt0Pppf3GkwGe4JPACcRYjgJIEhQgM6dBClnr0NYw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-darwin-arm64": { + "version": "4.3.3", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-darwin-x64": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.3.3.tgz", + "integrity": "sha512-fAeUqfV5ndhxRwai8cXGzdLvul9utWOmeTkv69unv4ZXixjn61Z+p9lCWdwOwA3TYboG3BwdVuN/RDjhBRl0mw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-freebsd-x64": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.3.3.tgz", + "integrity": "sha512-iyf5bV6+wnAlflVeEy7R25dupxTNECZN5QMI0qNT6eT+EgaGdZcKhGkr5SdoaWiLJ3spLqIY9VCeSGrwmtg4kw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm-gnueabihf": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.3.3.tgz", + "integrity": "sha512-aAYUprJAJQWWbRrPvtjdroZ56Md+JM8pMiopS6xGEwDfLhqj+2ver2p4nU4Mb3CRqcMmNBjo8KkUgcxhkzVQGQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm64-gnu": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.3.3.tgz", + "integrity": "sha512-nDxldcEENOxZRzC2uu9jrutZdAAQtb+8WWDCSnWL1zvBk1+FN+x6MtDViPB5AJMfttVCUhehGWus3XBPgatM/w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm64-musl": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.3.3.tgz", + "integrity": "sha512-Md44bD6veX/PC5iyF8cDVnw4HBIANZepRZZ7a8DQOvkfo5WUBwcp6iAuCUz23u+4SUkhJlD3eL7hNdW8ezd/kA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-x64-gnu": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.3.3.tgz", + "integrity": "sha512-tx7us1muwOKAKWao2v/GaafFeQboE6aj88vC6ziN2NCGcRm8gWUhwjzg+YdVB1e4boAtdtma4L43onunI6NS4w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-x64-musl": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.3.3.tgz", + "integrity": "sha512-SJxX60smvHgasZoBy11dX6YRjXJFovwWBoedhbQPOBzgFWBHGB+TVPWB9BxzR7TTxU8FQZAI2AyiNCMzFm8Img==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-wasm32-wasi": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-wasm32-wasi/-/oxide-wasm32-wasi-4.3.3.tgz", + "integrity": "sha512-jx1+rPhY/5Ympkktd656HBWEBLxP7dH06losBLjjf5vgCODXvi9KhtftWcMIwTFIDqBr7cRnQkdLnAG+IOlGvQ==", + "bundleDependencies": [ + "@napi-rs/wasm-runtime", + "@emnapi/core", + "@emnapi/runtime", + "@tybys/wasm-util", + "@emnapi/wasi-threads", + "tslib" + ], + "cpu": [ + "wasm32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/core": "^1.11.1", + "@emnapi/runtime": "^1.11.1", + "@emnapi/wasi-threads": "^1.2.2", + "@napi-rs/wasm-runtime": "^1.1.4", + "@tybys/wasm-util": "^0.10.2", + "tslib": "^2.8.1" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@tailwindcss/oxide-win32-arm64-msvc": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.3.3.tgz", + "integrity": "sha512-3rc292Ca2ceK6Ulcc/bAVnTs/3nDtoPhyEKlgPv+yQJQi/JS/AMJlqzxvlDacL1nekbrcf6bTqp/jV4qgnPxNQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-win32-x64-msvc": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.3.3.tgz", + "integrity": "sha512-yJ0pwIVc/nYeGoV02WtsN8KYyLQv7kyI2wDnkezyJlGGjkd4QLwDGAwl47YpPJeuI0M0ObaXGSPjvWDPeTPggw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/postcss": { + "version": "4.3.3", + "dev": true, + "license": "MIT", + "dependencies": { + "@alloc/quick-lru": "^5.2.0", + "@tailwindcss/node": "4.3.3", + "@tailwindcss/oxide": "4.3.3", + "postcss": "^8.5.16", + "tailwindcss": "4.3.3" + } + }, + "node_modules/@types/node": { + "version": "22.20.1", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "node_modules/@types/react": { + "version": "19.2.18", + "dev": true, + "license": "MIT", + "dependencies": { + "csstype": "^3.2.2" + } + }, + "node_modules/@types/react-dom": { + "version": "19.2.5", + "dev": true, + "license": "MIT", + "peerDependencies": { + "@types/react": "^19.2.0" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001810", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, + "node_modules/client-only": { + "version": "0.0.1", + "license": "MIT" + }, + "node_modules/csstype": { + "version": "3.2.3", + "dev": true, + "license": "MIT" + }, + "node_modules/detect-libc": { + "version": "2.1.2", + "devOptional": true, + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, + "node_modules/enhanced-resolve": { + "version": "5.24.5", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.4", + "tapable": "^2.3.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "dev": true, + "license": "ISC" + }, + "node_modules/jiti": { + "version": "2.7.0", + "dev": true, + "license": "MIT", + "bin": { + "jiti": "lib/jiti-cli.mjs" + } + }, + "node_modules/lightningcss": { + "version": "1.32.0", + "dev": true, + "license": "MPL-2.0", + "dependencies": { + "detect-libc": "^2.0.3" + }, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "lightningcss-android-arm64": "1.32.0", + "lightningcss-darwin-arm64": "1.32.0", + "lightningcss-darwin-x64": "1.32.0", + "lightningcss-freebsd-x64": "1.32.0", + "lightningcss-linux-arm-gnueabihf": "1.32.0", + "lightningcss-linux-arm64-gnu": "1.32.0", + "lightningcss-linux-arm64-musl": "1.32.0", + "lightningcss-linux-x64-gnu": "1.32.0", + "lightningcss-linux-x64-musl": "1.32.0", + "lightningcss-win32-arm64-msvc": "1.32.0", + "lightningcss-win32-x64-msvc": "1.32.0" + } + }, + "node_modules/lightningcss-android-arm64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.32.0.tgz", + "integrity": "sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-arm64": { + "version": "1.32.0", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-x64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.32.0.tgz", + "integrity": "sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-freebsd-x64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.32.0.tgz", + "integrity": "sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm-gnueabihf": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.32.0.tgz", + "integrity": "sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-gnu": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.32.0.tgz", + "integrity": "sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-musl": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.32.0.tgz", + "integrity": "sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-gnu": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.32.0.tgz", + "integrity": "sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-musl": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.32.0.tgz", + "integrity": "sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-arm64-msvc": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.32.0.tgz", + "integrity": "sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-x64-msvc": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.32.0.tgz", + "integrity": "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/magic-string": { + "version": "0.30.21", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/nanoid": { + "version": "3.3.18", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/next": { + "version": "15.5.24", + "license": "MIT", + "dependencies": { + "@next/env": "15.5.24", + "@swc/helpers": "0.5.15", + "caniuse-lite": "^1.0.30001579", + "postcss": "8.4.31", + "styled-jsx": "5.1.6" + }, + "bin": { + "next": "dist/bin/next" + }, + "engines": { + "node": "^18.18.0 || ^19.8.0 || >= 20.0.0" + }, + "optionalDependencies": { + "@next/swc-darwin-arm64": "15.5.24", + "@next/swc-darwin-x64": "15.5.24", + "@next/swc-linux-arm64-gnu": "15.5.24", + "@next/swc-linux-arm64-musl": "15.5.24", + "@next/swc-linux-x64-gnu": "15.5.24", + "@next/swc-linux-x64-musl": "15.5.24", + "@next/swc-win32-arm64-msvc": "15.5.24", + "@next/swc-win32-x64-msvc": "15.5.24", + "sharp": "^0.34.3 || ^0.35.3" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.1.0", + "@playwright/test": "^1.51.1", + "babel-plugin-react-compiler": "*", + "react": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", + "react-dom": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", + "sass": "^1.3.0" + }, + "peerDependenciesMeta": { + "@opentelemetry/api": { + "optional": true + }, + "@playwright/test": { + "optional": true + }, + "babel-plugin-react-compiler": { + "optional": true + }, + "sass": { + "optional": true + } + } + }, + "node_modules/next/node_modules/postcss": { + "version": "8.4.31", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.6", + "picocolors": "^1.0.0", + "source-map-js": "^1.0.2" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "license": "ISC" + }, + "node_modules/postcss": { + "version": "8.5.26", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.17", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/react": { + "version": "19.2.8", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "19.2.8", + "license": "MIT", + "dependencies": { + "scheduler": "^0.27.0" + }, + "peerDependencies": { + "react": "^19.2.8" + } + }, + "node_modules/scheduler": { + "version": "0.27.0", + "license": "MIT" + }, + "node_modules/semver": { + "version": "7.8.5", + "license": "ISC", + "optional": true, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/server-only": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/server-only/-/server-only-0.0.1.tgz", + "integrity": "sha512-qepMx2JxAa5jjfzxG79yPPq+8BuFToHd1hm7kI+Z4zAq1ftQiP7HcxMhDDItrbtwVeLg/cY2JnKnrcFkmiswNA==", + "license": "MIT" + }, + "node_modules/sharp": { + "version": "0.35.4", + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "@img/colour": "^1.1.0", + "detect-libc": "^2.1.2", + "semver": "^7.8.5" + }, + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-darwin-arm64": "0.35.4", + "@img/sharp-darwin-x64": "0.35.4", + "@img/sharp-freebsd-wasm32": "0.35.4", + "@img/sharp-libvips-darwin-arm64": "1.3.3", + "@img/sharp-libvips-darwin-x64": "1.3.3", + "@img/sharp-libvips-linux-arm": "1.3.3", + "@img/sharp-libvips-linux-arm64": "1.3.3", + "@img/sharp-libvips-linux-ppc64": "1.3.3", + "@img/sharp-libvips-linux-riscv64": "1.3.3", + "@img/sharp-libvips-linux-s390x": "1.3.3", + "@img/sharp-libvips-linux-x64": "1.3.3", + "@img/sharp-libvips-linuxmusl-arm64": "1.3.3", + "@img/sharp-libvips-linuxmusl-x64": "1.3.3", + "@img/sharp-linux-arm": "0.35.4", + "@img/sharp-linux-arm64": "0.35.4", + "@img/sharp-linux-ppc64": "0.35.4", + "@img/sharp-linux-riscv64": "0.35.4", + "@img/sharp-linux-s390x": "0.35.4", + "@img/sharp-linux-x64": "0.35.4", + "@img/sharp-linuxmusl-arm64": "0.35.4", + "@img/sharp-linuxmusl-x64": "0.35.4", + "@img/sharp-webcontainers-wasm32": "0.35.4", + "@img/sharp-win32-arm64": "0.35.4", + "@img/sharp-win32-ia32": "0.35.4", + "@img/sharp-win32-x64": "0.35.4" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/styled-jsx": { + "version": "5.1.6", + "license": "MIT", + "dependencies": { + "client-only": "0.0.1" + }, + "engines": { + "node": ">= 12.0.0" + }, + "peerDependencies": { + "react": ">= 16.8.0 || 17.x.x || ^18.0.0-0 || ^19.0.0-0" + }, + "peerDependenciesMeta": { + "@babel/core": { + "optional": true + }, + "babel-plugin-macros": { + "optional": true + } + } + }, + "node_modules/tailwindcss": { + "version": "4.3.3", + "dev": true, + "license": "MIT" + }, + "node_modules/tapable": { + "version": "2.3.3", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/tslib": { + "version": "2.8.1", + "license": "0BSD" + }, + "node_modules/typescript": { + "version": "5.9.3", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici-types": { + "version": "6.21.0", + "dev": true, + "license": "MIT" + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..0a1c1e3 --- /dev/null +++ b/package.json @@ -0,0 +1,25 @@ +{ + "name": "majd-trading", + "version": "0.1.0", + "private": true, + "scripts": { + "dev": "next dev", + "build": "next build", + "start": "next start", + "i18n:check": "node scripts/check-i18n.mjs" + }, + "dependencies": { + "next": "^15.5.0", + "react": "^19.1.0", + "react-dom": "^19.1.0", + "server-only": "^0.0.1" + }, + "devDependencies": { + "@tailwindcss/postcss": "^4.1.0", + "@types/node": "^22", + "@types/react": "^19", + "@types/react-dom": "^19", + "tailwindcss": "^4.1.0", + "typescript": "^5.7.0" + } +} diff --git a/postcss.config.mjs b/postcss.config.mjs new file mode 100644 index 0000000..61e3684 --- /dev/null +++ b/postcss.config.mjs @@ -0,0 +1,7 @@ +const config = { + plugins: { + "@tailwindcss/postcss": {}, + }, +}; + +export default config; diff --git a/public/fonts/bbh-bartle-latin.woff2 b/public/fonts/bbh-bartle-latin.woff2 new file mode 100644 index 0000000..6f35c16 Binary files /dev/null and b/public/fonts/bbh-bartle-latin.woff2 differ diff --git a/public/fonts/iran-yekan-black.ttf b/public/fonts/iran-yekan-black.ttf new file mode 100644 index 0000000..ec2da33 Binary files /dev/null and b/public/fonts/iran-yekan-black.ttf differ diff --git a/public/fonts/iran-yekan-bold.ttf b/public/fonts/iran-yekan-bold.ttf new file mode 100644 index 0000000..6d98764 Binary files /dev/null and b/public/fonts/iran-yekan-bold.ttf differ diff --git a/public/fonts/iran-yekan-extrabold.ttf b/public/fonts/iran-yekan-extrabold.ttf new file mode 100644 index 0000000..a2810c5 Binary files /dev/null and b/public/fonts/iran-yekan-extrabold.ttf differ diff --git a/public/fonts/iran-yekan-light.ttf b/public/fonts/iran-yekan-light.ttf new file mode 100644 index 0000000..b6114ee Binary files /dev/null and b/public/fonts/iran-yekan-light.ttf differ diff --git a/public/fonts/iran-yekan-medium.ttf b/public/fonts/iran-yekan-medium.ttf new file mode 100644 index 0000000..cc9d9b4 Binary files /dev/null and b/public/fonts/iran-yekan-medium.ttf differ diff --git a/public/fonts/iran-yekan-regular.ttf b/public/fonts/iran-yekan-regular.ttf new file mode 100644 index 0000000..c662408 Binary files /dev/null and b/public/fonts/iran-yekan-regular.ttf differ diff --git a/public/fonts/plex-arabic-400.woff2 b/public/fonts/plex-arabic-400.woff2 new file mode 100644 index 0000000..f0ad6aa Binary files /dev/null and b/public/fonts/plex-arabic-400.woff2 differ diff --git a/public/fonts/plex-arabic-500.woff2 b/public/fonts/plex-arabic-500.woff2 new file mode 100644 index 0000000..1f29b48 Binary files /dev/null and b/public/fonts/plex-arabic-500.woff2 differ diff --git a/public/fonts/plex-arabic-600.woff2 b/public/fonts/plex-arabic-600.woff2 new file mode 100644 index 0000000..a84cb14 Binary files /dev/null and b/public/fonts/plex-arabic-600.woff2 differ diff --git a/public/fonts/plex-arabic-700.woff2 b/public/fonts/plex-arabic-700.woff2 new file mode 100644 index 0000000..aee4bab Binary files /dev/null and b/public/fonts/plex-arabic-700.woff2 differ diff --git a/public/fonts/space-grotesk-latin.woff2 b/public/fonts/space-grotesk-latin.woff2 new file mode 100644 index 0000000..7b0e76a Binary files /dev/null and b/public/fonts/space-grotesk-latin.woff2 differ diff --git a/public/images/brand/bg-footer.jpg b/public/images/brand/bg-footer.jpg new file mode 100644 index 0000000..601d6ec Binary files /dev/null and b/public/images/brand/bg-footer.jpg differ diff --git a/public/images/brand/logo-white.png b/public/images/brand/logo-white.png new file mode 100644 index 0000000..8a88b1b Binary files /dev/null and b/public/images/brand/logo-white.png differ diff --git a/public/images/brand/logo.png b/public/images/brand/logo.png new file mode 100644 index 0000000..d6630a0 Binary files /dev/null and b/public/images/brand/logo.png differ diff --git a/public/images/customers/item1.png b/public/images/customers/item1.png new file mode 100644 index 0000000..e18e17e Binary files /dev/null and b/public/images/customers/item1.png differ diff --git a/public/images/customers/item2.png b/public/images/customers/item2.png new file mode 100644 index 0000000..7dbf4bf Binary files /dev/null and b/public/images/customers/item2.png differ diff --git a/public/images/customers/item3.png b/public/images/customers/item3.png new file mode 100644 index 0000000..05d5069 Binary files /dev/null and b/public/images/customers/item3.png differ diff --git a/public/images/customers/item4.png b/public/images/customers/item4.png new file mode 100644 index 0000000..a54cf74 Binary files /dev/null and b/public/images/customers/item4.png differ diff --git a/public/images/customers/item5.png b/public/images/customers/item5.png new file mode 100644 index 0000000..c499b22 Binary files /dev/null and b/public/images/customers/item5.png differ diff --git a/public/images/customers/item6.png b/public/images/customers/item6.png new file mode 100644 index 0000000..3950fc2 Binary files /dev/null and b/public/images/customers/item6.png differ diff --git a/public/images/features/feature1.png b/public/images/features/feature1.png new file mode 100644 index 0000000..4e18db2 Binary files /dev/null and b/public/images/features/feature1.png differ diff --git a/public/images/features/feature2.png b/public/images/features/feature2.png new file mode 100644 index 0000000..1028c37 Binary files /dev/null and b/public/images/features/feature2.png differ diff --git a/public/images/features/feature3.png b/public/images/features/feature3.png new file mode 100644 index 0000000..43a2e8a Binary files /dev/null and b/public/images/features/feature3.png differ diff --git a/public/images/features/feature4.png b/public/images/features/feature4.png new file mode 100644 index 0000000..376f5d5 Binary files /dev/null and b/public/images/features/feature4.png differ diff --git a/public/images/sample/aerial-vessel.jpg b/public/images/sample/aerial-vessel.jpg new file mode 100644 index 0000000..4ef5dd6 Binary files /dev/null and b/public/images/sample/aerial-vessel.jpg differ diff --git a/public/images/sample/container-canyon.jpg b/public/images/sample/container-canyon.jpg new file mode 100644 index 0000000..ae393bf Binary files /dev/null and b/public/images/sample/container-canyon.jpg differ diff --git a/public/images/sample/container-stack.jpg b/public/images/sample/container-stack.jpg new file mode 100644 index 0000000..1b378da Binary files /dev/null and b/public/images/sample/container-stack.jpg differ diff --git a/public/images/sample/deck-containers.jpg b/public/images/sample/deck-containers.jpg new file mode 100644 index 0000000..5038074 Binary files /dev/null and b/public/images/sample/deck-containers.jpg differ diff --git a/public/images/sample/port-vessel.jpg b/public/images/sample/port-vessel.jpg new file mode 100644 index 0000000..757b671 Binary files /dev/null and b/public/images/sample/port-vessel.jpg differ diff --git a/public/images/slider/slider1.jpg b/public/images/slider/slider1.jpg new file mode 100644 index 0000000..8224670 Binary files /dev/null and b/public/images/slider/slider1.jpg differ diff --git a/public/images/slider/slider2.jpg b/public/images/slider/slider2.jpg new file mode 100644 index 0000000..033ed69 Binary files /dev/null and b/public/images/slider/slider2.jpg differ diff --git a/public/images/slider/slider3.jpg b/public/images/slider/slider3.jpg new file mode 100644 index 0000000..5f14983 Binary files /dev/null and b/public/images/slider/slider3.jpg differ diff --git a/public/images/slider/slider4.jpg b/public/images/slider/slider4.jpg new file mode 100644 index 0000000..4c75972 Binary files /dev/null and b/public/images/slider/slider4.jpg differ diff --git a/public/images/slider/slider5.jpg b/public/images/slider/slider5.jpg new file mode 100644 index 0000000..ffa1333 Binary files /dev/null and b/public/images/slider/slider5.jpg differ diff --git a/public/images/slider/slider6.jpg b/public/images/slider/slider6.jpg new file mode 100644 index 0000000..f90e943 Binary files /dev/null and b/public/images/slider/slider6.jpg differ diff --git a/scripts/check-i18n.mjs b/scripts/check-i18n.mjs new file mode 100644 index 0000000..1735e20 --- /dev/null +++ b/scripts/check-i18n.mjs @@ -0,0 +1,66 @@ +/** + * Compares every locale file against the reference locale (fa). + * + * The `Dictionary` annotation on each wrapper already fails the build on a + * MISSING key. This catches the other two cases it cannot see: keys left behind + * after a rename, and arrays whose length drifted apart (a mission list of 14 + * items in one language and 13 in another renders fine and is wrong). + * + * Run after editing fa.json, before touching en/ar: npm run i18n:check + */ +import { readFileSync } from "node:fs"; +import { fileURLToPath } from "node:url"; +import { dirname, join } from "node:path"; + +const here = dirname(fileURLToPath(import.meta.url)); +const dir = join(here, "..", "src", "i18n", "locales"); +const REFERENCE = "fa"; +const OTHERS = ["en", "ar"]; + +const load = (l) => JSON.parse(readFileSync(join(dir, `${l}.json`), "utf8")); + +/** Flattens to `a.b.c` paths; arrays report as one path plus their length. */ +function paths(value, prefix = "", out = new Map()) { + if (Array.isArray(value)) { + out.set(prefix, `array:${value.length}`); + return out; + } + if (value && typeof value === "object") { + for (const [k, v] of Object.entries(value)) { + paths(v, prefix ? `${prefix}.${k}` : k, out); + } + return out; + } + out.set(prefix, typeof value); + return out; +} + +const reference = paths(load(REFERENCE)); +let problems = 0; + +for (const locale of OTHERS) { + const current = paths(load(locale)); + const missing = [...reference.keys()].filter((k) => !current.has(k)); + const stale = [...current.keys()].filter((k) => !reference.has(k)); + const lengths = [...reference.entries()].filter( + ([k, v]) => current.has(k) && v !== current.get(k) && v.startsWith("array") + ); + + if (!missing.length && !stale.length && !lengths.length) { + console.log(` ${locale}: in sync with ${REFERENCE} (${reference.size} keys)`); + continue; + } + + problems += missing.length + stale.length + lengths.length; + console.log(` ${locale}:`); + for (const k of missing) console.log(` missing ${k}`); + for (const k of stale) console.log(` stale ${k} (not in ${REFERENCE})`); + for (const [k, v] of lengths) + console.log(` length ${k} ${REFERENCE}=${v} ${locale}=${current.get(k)}`); +} + +if (problems) { + console.error(`\ni18n check failed: ${problems} problem(s).`); + process.exit(1); +} +console.log("\ni18n check passed."); diff --git a/src/app/[locale]/about/page.tsx b/src/app/[locale]/about/page.tsx new file mode 100644 index 0000000..703c416 --- /dev/null +++ b/src/app/[locale]/about/page.tsx @@ -0,0 +1,114 @@ +import type { Metadata } from "next"; +import Image from "next/image"; +import { notFound } from "next/navigation"; +import PageHero from "@/components/PageHero"; +import { Section, SectionHeading } from "@/components/Section"; +import { isLocale } from "@/i18n/config"; +import { getDictionary } from "@/i18n/dictionaries"; +import { pageImages } from "@/content"; + +export async function generateMetadata({ + params, +}: { + params: Promise<{ locale: string }>; +}): Promise { + const { locale } = await params; + if (!isLocale(locale)) return {}; + const t = await getDictionary(locale); + return { title: t.about.heading, description: t.about.paragraphs[0] }; +} + +export default async function AboutPage({ + params, +}: { + params: Promise<{ locale: string }>; +}) { + const { locale } = await params; + if (!isLocale(locale)) notFound(); + const t = await getDictionary(locale); + + return ( + <> + + +
+
+
+ {t.about.paragraphs.map((paragraph) => ( +

+ {paragraph} +

+ ))} +
+
+ {t.meta.company} +
+
+
+ +
+ +
+ {t.about.vision.paragraphs.map((paragraph) => ( +

+ {paragraph} +

+ ))} +
+
+ +
+ +
    + {t.about.mission.items.map((item, i) => ( +
  1. + + {String(i + 1).padStart(2, "0")} + +

    {item}

    +
  2. + ))} +
+
+ +
+ +
    + {t.about.values.items.map((value) => ( +
  • + {value} +
  • + ))} +
+
+ + ); +} diff --git a/src/app/[locale]/blog/[id]/[slug]/page.tsx b/src/app/[locale]/blog/[id]/[slug]/page.tsx new file mode 100644 index 0000000..9adb613 --- /dev/null +++ b/src/app/[locale]/blog/[id]/[slug]/page.tsx @@ -0,0 +1,121 @@ +import type { Metadata } from "next"; +import Link from "next/link"; +import { notFound, redirect } from "next/navigation"; +import PageHero from "@/components/PageHero"; +import Reveal from "@/components/Reveal"; +import { Section } from "@/components/Section"; +import { isLocale } from "@/i18n/config"; +import { getDictionary } from "@/i18n/dictionaries"; +import { pageImages } from "@/content"; +import { getPostById } from "@/lib/blog"; + +type Params = { locale: string; id: string; slug: string }; + +/** A malformed escape sequence must not take the page down. */ +function safeDecode(value: string) { + try { + return decodeURIComponent(value); + } catch { + return value; + } +} + +export async function generateMetadata({ + params, +}: { + params: Promise; +}): Promise { + const { locale, id } = await params; + const post = await getPostById(Number(id)); + if (!post) return {}; + + return { + title: post.title, + description: post.abstract ?? undefined, + alternates: { canonical: `/${locale}/blog/${post.id}/${encodeURIComponent(post.slug)}` }, + openGraph: post.imageUrl ? { images: [post.imageUrl], type: "article" } : undefined, + }; +} + +export default async function BlogPostPage({ params }: { params: Promise }) { + const { locale, id, slug } = await params; + if (!isLocale(locale)) notFound(); + + const numericId = Number(id); + if (!Number.isInteger(numericId)) notFound(); + + const post = await getPostById(numericId); + if (!post) notFound(); + + // The slug segment is SEO-only. If it drifts from the canonical form (an old + // link, or the title was edited), send the reader to the canonical URL. + // + // Two encoding traps here, both of which produce a 500 rather than a redirect: + // the incoming param arrives percent-ENCODED, so it must be decoded before + // comparing against a Persian slug; and a Location header may not contain raw + // non-ASCII, so the target must be encoded on the way back out. + const decodedSlug = safeDecode(slug); + if (decodedSlug !== post.slug) { + redirect(`/${locale}/blog/${post.id}/${encodeURIComponent(post.slug)}`); + } + + const t = await getDictionary(locale); + + const publishedAt = post.publishedAt + ? new Intl.DateTimeFormat(locale, { day: "numeric", month: "long", year: "numeric" }).format( + new Date(post.publishedAt) + ) + : null; + + return ( + <> + + +
+
+ + {publishedAt && ( +

+ {t.blog.publishedOn}{" "} + + {post.author && · {post.author}} +

+ )} + + + {post.abstract && ( +

+ {post.abstract} +

+ )} + + {post.bodyHtml && ( + /* Body arrives as HTML from the Meshkee editor — authored in the + client's own dashboard, trusted the way a CMS body is. + `.prose-brand` in globals.css styles it without a plugin. */ +
+ )} + +
+ +
+ + {t.blog.backToList} + +
+
+ + ); +} diff --git a/src/app/[locale]/blog/page.tsx b/src/app/[locale]/blog/page.tsx new file mode 100644 index 0000000..a889f1f --- /dev/null +++ b/src/app/[locale]/blog/page.tsx @@ -0,0 +1,86 @@ +import type { Metadata } from "next"; +import Image from "next/image"; +import Link from "next/link"; +import { notFound } from "next/navigation"; +import PageHero from "@/components/PageHero"; +import Reveal from "@/components/Reveal"; +import { Section } from "@/components/Section"; +import { isLocale } from "@/i18n/config"; +import { getDictionary } from "@/i18n/dictionaries"; +import { pageImages } from "@/content"; +import { getPosts } from "@/lib/blog"; + +export async function generateMetadata({ + params, +}: { + params: Promise<{ locale: string }>; +}): Promise { + const { locale } = await params; + if (!isLocale(locale)) return {}; + const t = await getDictionary(locale); + return { title: t.blog.heading, description: t.blog.subtitle }; +} + +export default async function BlogIndexPage({ + params, +}: { + params: Promise<{ locale: string }>; +}) { + const { locale } = await params; + if (!isLocale(locale)) notFound(); + + const t = await getDictionary(locale); + const posts = await getPosts(24); + + const formatDate = (value: string | null) => + value + ? new Intl.DateTimeFormat(locale, { day: "numeric", month: "long", year: "numeric" }).format( + new Date(value) + ) + : null; + + return ( + <> + + +
+ {posts.length === 0 ? ( +

{t.blog.empty}

+ ) : ( +
+ {posts.map((post, i) => ( + + +
+ {post.imageUrl && ( + + )} +
+
+ {post.publishedAt && ( + + )} +

{post.title}

+ {post.abstract && ( +

+ {post.abstract} +

+ )} +
+ +
+ ))} +
+ )} +
+ + ); +} diff --git a/src/app/[locale]/contact/page.tsx b/src/app/[locale]/contact/page.tsx new file mode 100644 index 0000000..ab969df --- /dev/null +++ b/src/app/[locale]/contact/page.tsx @@ -0,0 +1,188 @@ +import type { Metadata } from "next"; +import type { ReactNode } from "react"; +import { notFound } from "next/navigation"; +import ContactForm from "@/components/ContactForm"; +import PageHero from "@/components/PageHero"; +import { Section, SectionHeading } from "@/components/Section"; +import { isLocale } from "@/i18n/config"; +import { getDictionary } from "@/i18n/dictionaries"; +import { pageImages } from "@/content"; +import { site } from "@/lib/site"; + +export async function generateMetadata({ + params, +}: { + params: Promise<{ locale: string }>; +}): Promise { + const { locale } = await params; + if (!isLocale(locale)) return {}; + const t = await getDictionary(locale); + return { title: t.contact.heading, description: site.address }; +} + +function InfoCard({ + title, + latin, + icon, + children, +}: { + title: string; + latin: string; + icon: ReactNode; + children: ReactNode; +}) { + return ( +
+
{icon}
+

{title}

+ {latin} +
{children}
+
+ ); +} + +export default async function ContactPage({ + params, +}: { + params: Promise<{ locale: string }>; +}) { + const { locale } = await params; + if (!isLocale(locale)) notFound(); + const t = await getDictionary(locale); + + return ( + <> + + +
+
+ + + + + + + +
+
+ +
+
+
+ +
+ +
+
+ +
+