Move coming-soon storefront into website Next.js app.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Alireza Hassani
2026-09-07 21:00:21 +03:30
co-authored by Cursor
parent e171222b16
commit 2f857f1a90
22 changed files with 7427 additions and 1 deletions
+2
View File
@@ -0,0 +1,2 @@
NEXT_PUBLIC_STORE_URL=http://store.mana-collection.com:3001
NEXT_PUBLIC_API_URL=http://api.mana-collection.com:3002/api/v1
+9
View File
@@ -0,0 +1,9 @@
<!-- BEGIN:nextjs-agent-rules -->
# This is NOT the Next.js you know
This version has breaking changes — APIs, conventions, and file structure may all differ from your training data. Read the relevant guide in `node_modules/next/dist/docs/` (resolved from this file's directory; in monorepos the `next` package may not be visible from the repo root) before writing any code. Heed deprecation notices.
This block is written and re-added by `next dev` — verify at `node_modules/next/dist/server/lib/generate-agent-files.js`. Removing it from a diff only re-creates the uncommitted change; committing it with your work keeps the tree clean.
<!-- END:nextjs-agent-rules -->
+18 -1
View File
@@ -2,4 +2,21 @@
Public storefront for ManaCollections.ir (multi-seller catalog, cart, checkout).
Scaffold TBD — API lives in `../backend` (`/api/v1/public/...`).
## Local
```bash
# once (needs admin password)
../scripts/setup-hosts.sh
cp .env.example .env.local
npm install
npm run dev
```
| URL | App |
|-----|-----|
| http://mana-collection.com:3000 | Website (coming soon) |
| http://store.mana-collection.com:3001 | Store dashboard |
| http://api.mana-collection.com:3002/api/v1 | Backend API |
API: `NEXT_PUBLIC_API_URL` · store login: `NEXT_PUBLIC_STORE_URL`
+11
View File
@@ -0,0 +1,11 @@
import { defineConfig, globalIgnores } from "eslint/config";
import nextVitals from "eslint-config-next/core-web-vitals";
import nextTs from "eslint-config-next/typescript";
const eslintConfig = defineConfig([
...nextVitals,
...nextTs,
globalIgnores([".next/**", "out/**", "build/**", "next-env.d.ts"]),
]);
export default eslintConfig;
+18
View File
@@ -0,0 +1,18 @@
import type { NextConfig } from "next";
const nextConfig: NextConfig = {
allowedDevOrigins: [
"mana-collection.com",
"www.mana-collection.com",
"*.mana-collection.com",
],
images: {
localPatterns: [
{
pathname: "/images/**",
},
],
},
};
export default nextConfig;
+6782
View File
File diff suppressed because it is too large Load Diff
+26
View File
@@ -0,0 +1,26 @@
{
"name": "mana-collection-website",
"version": "0.1.0",
"private": true,
"scripts": {
"dev": "next dev -H 0.0.0.0 -p 3000",
"build": "next build",
"start": "next start -H 0.0.0.0 -p 3000",
"lint": "eslint"
},
"dependencies": {
"next": "16.3.4",
"react": "19.2.8",
"react-dom": "19.2.8"
},
"devDependencies": {
"@tailwindcss/postcss": "^4",
"@types/node": "^20",
"@types/react": "^19",
"@types/react-dom": "^19",
"eslint": "^9",
"eslint-config-next": "16.3.4",
"tailwindcss": "^4",
"typescript": "^5"
}
}
+7
View File
@@ -0,0 +1,7 @@
const config = {
plugins: {
"@tailwindcss/postcss": {},
},
};
export default config;
Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1018 KiB

+187
View File
@@ -0,0 +1,187 @@
@import "tailwindcss";
:root {
--background: #fbfbfa;
--foreground: #0a0a0a;
--surface: #ffffff;
--muted: #6b6b6b;
--line: #e4e4e4;
--ink: #0a0a0a;
}
@theme inline {
--color-background: var(--background);
--color-foreground: var(--foreground);
--color-surface: var(--surface);
--color-muted: var(--muted);
--color-line: var(--line);
--color-ink: var(--ink);
--font-sans: var(--font-doran);
--font-display: var(--font-cailyne);
}
body {
background: var(--background);
color: var(--foreground);
font-family: var(--font-doran), Tahoma, sans-serif;
}
.font-display {
font-family: var(--font-cailyne), Georgia, serif;
letter-spacing: 0.04em;
}
.font-montserrat {
font-family: var(--font-montserrat), system-ui, sans-serif;
}
@keyframes coming-soon-rise {
from {
opacity: 0;
transform: translateY(22px);
filter: blur(6px);
}
to {
opacity: 1;
transform: translateY(0);
filter: blur(0);
}
}
@keyframes coming-soon-panel {
from {
opacity: 0;
transform: translateX(24px);
}
to {
opacity: 1;
transform: translateX(0);
}
}
@keyframes coming-soon-drift {
0% {
transform: scale(1) translate3d(0, 0, 0);
}
100% {
transform: scale(1.06) translate3d(-0.8%, 0.5%, 0);
}
}
@keyframes coming-soon-line {
from {
transform: scaleX(0);
opacity: 0;
}
to {
transform: scaleX(1);
opacity: 1;
}
}
@keyframes coming-soon-glow {
0%,
100% {
box-shadow: 0 0 0 0 rgba(10, 10, 10, 0);
}
50% {
box-shadow: 0 0 0 4px rgba(10, 10, 10, 0.06);
}
}
.coming-soon-seq {
opacity: 0;
animation: coming-soon-rise 0.95s cubic-bezier(0.16, 1, 0.3, 1) forwards;
}
.coming-soon-seq-1 {
animation-delay: 0.12s;
}
.coming-soon-seq-2 {
animation-delay: 0.38s;
}
.coming-soon-seq-3 {
animation-delay: 0.58s;
}
.coming-soon-seq-4 {
animation-delay: 0.78s;
}
.coming-soon-seq-5 {
animation-delay: 1s;
}
.coming-soon-seq-5 {
position: relative;
}
.coming-soon-seq-5::before {
content: "";
position: absolute;
top: 0;
inset-inline: 0;
height: 1px;
background: var(--line);
transform-origin: center;
animation: coming-soon-line 0.7s cubic-bezier(0.16, 1, 0.3, 1) 1.05s both;
}
.coming-soon-seq-image {
opacity: 0;
animation: coming-soon-panel 1.1s ease-out 0.05s forwards;
}
.coming-soon-image {
transform: scale(1);
animation: coming-soon-drift 20s ease-in-out 1.2s alternate infinite both;
will-change: transform;
}
.coming-soon-login {
animation: coming-soon-glow 2.4s ease-in-out 1.6s 2;
}
@media (prefers-reduced-motion: reduce) {
.coming-soon-seq,
.coming-soon-seq-image,
.coming-soon-image,
.coming-soon-seq-5::before,
.coming-soon-login {
animation: none !important;
opacity: 1 !important;
filter: none !important;
transform: none !important;
}
}
.mana-canvas {
background:
radial-gradient(ellipse 80% 60% at 20% 20%, rgba(0, 0, 0, 0.02), transparent 55%),
radial-gradient(ellipse 70% 50% at 90% 80%, rgba(0, 0, 0, 0.022), transparent 50%),
linear-gradient(160deg, #fcfcfb 0%, #f7f7f5 48%, #f3f3f0 100%);
}
.mana-canvas::before {
content: "";
position: absolute;
inset: 0;
pointer-events: none;
opacity: 0.45;
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='220' height='220' viewBox='0 0 220 220'%3E%3Cg fill='%230a0a0a'%3E%3Ccircle cx='12' cy='18' r='1.1' opacity='.28'/%3E%3Ccircle cx='48' cy='9' r='0.7' opacity='.18'/%3E%3Ccircle cx='91' cy='27' r='1.4' opacity='.22'/%3E%3Ccircle cx='133' cy='14' r='0.8' opacity='.3'/%3E%3Ccircle cx='176' cy='31' r='1.2' opacity='.16'/%3E%3Ccircle cx='208' cy='8' r='0.9' opacity='.24'/%3E%3Ccircle cx='27' cy='56' r='0.75' opacity='.2'/%3E%3Ccircle cx='69' cy='71' r='1.3' opacity='.26'/%3E%3Ccircle cx='112' cy='49' r='0.65' opacity='.15'/%3E%3Ccircle cx='154' cy='63' r='1.05' opacity='.29'/%3E%3Ccircle cx='197' cy='78' r='0.85' opacity='.19'/%3E%3Ccircle cx='8' cy='99' r='1.15' opacity='.23'/%3E%3Ccircle cx='41' cy='118' r='0.7' opacity='.17'/%3E%3Ccircle cx='83' cy='104' r='1.45' opacity='.27'/%3E%3Ccircle cx='126' cy='129' r='0.9' opacity='.21'/%3E%3Ccircle cx='168' cy='111' r='0.6' opacity='.14'/%3E%3Ccircle cx='201' cy='136' r='1.2' opacity='.25'/%3E%3Ccircle cx='22' cy='158' r='0.95' opacity='.18'/%3E%3Ccircle cx='58' cy='179' r='1.25' opacity='.3'/%3E%3Ccircle cx='97' cy='163' r='0.7' opacity='.16'/%3E%3Ccircle cx='139' cy='188' r='1.1' opacity='.22'/%3E%3Ccircle cx='181' cy='171' r='0.8' opacity='.28'/%3E%3Ccircle cx='213' cy='194' r='1' opacity='.19'/%3E%3Ccircle cx='35' cy='208' r='0.65' opacity='.24'/%3E%3Ccircle cx='74' cy='201' r='1.35' opacity='.15'/%3E%3Ccircle cx='118' cy='214' r='0.85' opacity='.26'/%3E%3Ccircle cx='160' cy='203' r='0.75' opacity='.2'/%3E%3Ccircle cx='16' cy='78' r='0.55' opacity='.12'/%3E%3Ccircle cx='104' cy='86' r='0.5' opacity='.13'/%3E%3Ccircle cx='187' cy='52' r='0.6' opacity='.11'/%3E%3Ccircle cx='149' cy='149' r='0.55' opacity='.14'/%3E%3C/g%3E%3C/svg%3E");
background-size: 220px 220px;
background-repeat: repeat;
}
.mana-canvas::after {
content: "";
position: absolute;
inset: 0;
pointer-events: none;
opacity: 0.16;
background-image: url("data:image/svg+xml,%3Csvg viewBox='0 0 200 200' xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.85' numOctaves='3' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)' opacity='0.45'/%3E%3C/svg%3E");
background-size: 180px 180px;
mix-blend-mode: multiply;
}
+71
View File
@@ -0,0 +1,71 @@
import type { Metadata } from "next";
import { Montserrat } from "next/font/google";
import localFont from "next/font/local";
import "./globals.css";
const doran = localFont({
src: [
{
path: "../fonts/doran/Doran-Thin.woff",
weight: "100",
style: "normal",
},
{
path: "../fonts/doran/Doran-Regular.woff",
weight: "400",
style: "normal",
},
{
path: "../fonts/doran/Doran-Medium.woff",
weight: "500",
style: "normal",
},
{
path: "../fonts/doran/Doran-Bold.woff",
weight: "700",
style: "normal",
},
],
variable: "--font-doran",
display: "swap",
});
const cailyne = localFont({
src: [
{
path: "../fonts/Cailyne.otf",
weight: "400",
style: "normal",
},
],
variable: "--font-cailyne",
display: "swap",
});
const montserrat = Montserrat({
subsets: ["latin"],
weight: ["400", "500", "600"],
variable: "--font-montserrat",
display: "swap",
});
export const metadata: Metadata = {
title: "به‌زودی | مانا کالکشن",
description: "فروشگاه آنلاین مانا کالکشن به‌زودی در دسترس خواهد بود.",
};
export default function RootLayout({
children,
}: Readonly<{
children: React.ReactNode;
}>) {
return (
<html
lang="fa"
dir="rtl"
className={`${doran.variable} ${cailyne.variable} ${montserrat.variable} h-full antialiased`}
>
<body className="min-h-full font-sans text-foreground">{children}</body>
</html>
);
}
+81
View File
@@ -0,0 +1,81 @@
import Image from "next/image";
import ManaLogo from "@/components/ui/ManaLogo";
const STORE_LOGIN =
process.env.NEXT_PUBLIC_STORE_URL?.replace(/\/$/, "") ||
"http://store.mana-collection.com:3001";
export default function HomePage() {
return (
<main className="coming-soon relative min-h-dvh overflow-hidden bg-[#f7f7f5] text-ink">
{/* dir=ltr keeps content on visual left, image on visual right */}
<div dir="ltr" className="grid min-h-dvh grid-cols-1 lg:grid-cols-2">
{/* Content — left */}
<section className="relative order-2 flex min-h-[52dvh] flex-col lg:order-1 lg:min-h-dvh">
<div className="mana-canvas absolute inset-0" aria-hidden />
<div
dir="rtl"
className="relative z-10 flex flex-1 flex-col px-7 py-8 sm:px-12 sm:py-10 lg:px-16 lg:py-14"
>
<header className="coming-soon-seq coming-soon-seq-1 flex shrink-0 justify-center">
<ManaLogo
href=""
width={400}
priority
showTagline
variant="white"
blackBg
className="w-[400px] max-w-[min(400px,100%)]"
/>
</header>
<div className="flex flex-1 flex-col items-center justify-center py-12 text-center sm:py-16">
<div className="max-w-md">
<p
dir="ltr"
className="coming-soon-seq coming-soon-seq-2 mb-4 font-display text-sm tracking-[0.2em] text-muted sm:text-base"
>
Coming soon
</p>
<h1 className="coming-soon-seq coming-soon-seq-3 mb-5 text-4xl leading-tight text-ink sm:text-5xl">
به‌زودی
</h1>
<p className="coming-soon-seq coming-soon-seq-4 text-sm leading-8 text-muted sm:text-base sm:leading-8">
در این آدرس میزبان شما خواهیم بود.
</p>
</div>
</div>
<footer className="coming-soon-seq coming-soon-seq-5 shrink-0 pt-6">
<div className="flex flex-wrap items-center justify-between gap-4">
<p className="text-xs leading-6 text-muted">
حساب دارید؟ وارد شوید.
</p>
<a
href={`${STORE_LOGIN}/login`}
className="coming-soon-login inline-flex items-center justify-center border border-ink bg-ink px-7 py-2.5 text-sm text-white transition-colors hover:bg-white hover:text-ink"
>
ورود
</a>
</div>
</footer>
</div>
</section>
{/* Image — right */}
<aside className="coming-soon-seq-image relative order-1 min-h-[42dvh] overflow-hidden lg:order-2 lg:min-h-dvh">
<Image
src="/images/image1.jpg"
alt="مانا کالکشن"
fill
priority
sizes="(max-width: 1024px) 100vw, 50vw"
className="coming-soon-image object-cover object-center"
/>
<div className="pointer-events-none absolute inset-0 bg-gradient-to-l from-transparent via-transparent to-black/10" />
</aside>
</div>
</main>
);
}
+185
View File
@@ -0,0 +1,185 @@
import Image from "next/image";
import Link from "next/link";
const LOGO_ASPECT = 1172 / 304;
export const MANA_LOGO_SRC = {
default: "/images/brand/Mana-Logo.png",
black: "/images/brand/Mana-Logo-Black.png",
white: "/images/brand/Mana-Logo-White.png",
} as const;
type ManaLogoProps = {
href?: string;
className?: string;
priority?: boolean;
/** Visual height of the wordmark */
height?: number;
/** Visual width of the wordmark (overrides height when set) */
width?: number;
/** Framed lockup like the brand board */
framed?: boolean;
/** Show “The Style you desire” under the mark */
showTagline?: boolean;
taglineClassName?: string;
/** Logo color version */
variant?: keyof typeof MANA_LOGO_SRC;
/** Wrap mark on a black plate (slogan stays outside) */
blackBg?: boolean;
/** Slogan font size in px (default 24) */
taglineSize?: number;
/** On hover, invert logo plate + slogan colors */
hoverSwap?: boolean;
};
function LogoImage({
src,
width,
height,
priority,
className = "",
}: {
src: string;
width: number;
height: number;
priority?: boolean;
className?: string;
}) {
return (
<Image
src={src}
alt="Mana Collection"
width={width}
height={height}
priority={priority}
unoptimized
style={{ width, height: "auto" }}
className={`object-contain ${className}`}
/>
);
}
export default function ManaLogo({
href = "/",
className = "",
priority = false,
height = 36,
width: widthProp,
framed = false,
showTagline = false,
taglineClassName = "",
variant = "default",
blackBg = false,
taglineSize = 24,
hoverSwap = false,
}: ManaLogoProps) {
const width = widthProp ?? Math.round(height * LOGO_ASPECT);
const renderHeight = widthProp
? Math.round(widthProp / LOGO_ASPECT)
: height;
const src =
variant === "white"
? MANA_LOGO_SRC.white
: variant === "black"
? MANA_LOGO_SRC.black
: MANA_LOGO_SRC.default;
const swapSrc =
variant === "white" ? MANA_LOGO_SRC.black : MANA_LOGO_SRC.white;
const image = (
<span className="relative inline-flex">
<LogoImage
src={src}
width={width}
height={renderHeight}
priority={priority}
className={
hoverSwap
? "transition-opacity duration-500 group-hover:opacity-0"
: ""
}
/>
{hoverSwap ? (
<LogoImage
src={swapSrc}
width={width}
height={renderHeight}
className="pointer-events-none absolute inset-0 opacity-0 transition-opacity duration-500 group-hover:opacity-100"
/>
) : null}
</span>
);
const markInner = framed ? (
<span
className="inline-flex border border-ink/80 p-[3px]"
style={{ boxShadow: "inset 0 0 0 1px rgba(10,10,10,0.55)" }}
>
<span className="inline-flex items-center justify-center px-5 py-3 sm:px-7 sm:py-3.5">
{image}
</span>
</span>
) : (
image
);
const mark = blackBg ? (
<span
className={`inline-flex w-full items-center justify-center bg-ink px-2.5 py-2 sm:px-3 sm:py-2.5 ${
hoverSwap ? "mana-logo-swap-plate" : ""
}`}
>
{markInner}
</span>
) : (
markInner
);
const tagline = showTagline ? (
<span
className={`self-start bg-white px-4 py-1 font-display tracking-[0.12em] text-[#8a8a8a] sm:px-5 ${
hoverSwap ? "mana-logo-swap-tagline" : ""
} ${taglineClassName}`}
style={{ fontSize: taglineSize }}
>
<span>The Style you desire</span>
</span>
) : null;
const lockup = (
<span
dir="ltr"
className={`inline-flex flex-col items-stretch gap-0 ${className}`}
>
{mark}
{tagline}
</span>
);
if (!href) {
return lockup;
}
const isExternal = /^https?:\/\//i.test(href);
const interactiveClass = `group inline-flex cursor-pointer ${
hoverSwap ? "" : "transition-opacity duration-200 hover:opacity-80"
}`;
if (isExternal) {
return (
<a
href={href}
className={interactiveClass}
aria-label="Mana Collection"
>
{lockup}
</a>
);
}
return (
<Link href={href} className={interactiveClass} aria-label="Mana Collection">
{lockup}
</Link>
);
}
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+30
View File
@@ -0,0 +1,30 @@
{
"compilerOptions": {
"target": "ES2017",
"lib": ["dom", "dom.iterable", "esnext"],
"allowJs": true,
"skipLibCheck": true,
"strict": true,
"noEmit": true,
"esModuleInterop": true,
"module": "esnext",
"moduleResolution": "bundler",
"resolveJsonModule": true,
"isolatedModules": true,
"jsx": "react-jsx",
"incremental": true,
"plugins": [{ "name": "next" }],
"paths": {
"@/*": ["./src/*"]
}
},
"include": [
"next-env.d.ts",
"**/*.ts",
"**/*.tsx",
".next/types/**/*.ts",
".next/dev/types/**/*.ts",
"**/*.mts"
],
"exclude": ["node_modules"]
}