Add Khiyal Studio homepage clone with Meshkee integration.
Initial Next.js site covering home, about, and contact, matching the live studio layout. Co-authored-by: Cursor <cursoragent@cursor.com>
@@ -0,0 +1,39 @@
|
|||||||
|
# dependencies
|
||||||
|
/node_modules
|
||||||
|
/.pnp
|
||||||
|
.pnp.*
|
||||||
|
.yarn/*
|
||||||
|
!.yarn/patches
|
||||||
|
!.yarn/plugins
|
||||||
|
!.yarn/releases
|
||||||
|
!.yarn/versions
|
||||||
|
|
||||||
|
# testing
|
||||||
|
/coverage
|
||||||
|
|
||||||
|
# next.js
|
||||||
|
/.next/
|
||||||
|
/out/
|
||||||
|
|
||||||
|
# production
|
||||||
|
/build
|
||||||
|
|
||||||
|
# misc
|
||||||
|
.DS_Store
|
||||||
|
*.pem
|
||||||
|
|
||||||
|
# debug
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
.pnpm-debug.log*
|
||||||
|
|
||||||
|
# env files
|
||||||
|
.env*
|
||||||
|
|
||||||
|
# vercel
|
||||||
|
.vercel
|
||||||
|
|
||||||
|
# typescript
|
||||||
|
*.tsbuildinfo
|
||||||
|
next-env.d.ts
|
||||||
@@ -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 -->
|
||||||
@@ -0,0 +1,70 @@
|
|||||||
|
# Khiyal Studio Website Context
|
||||||
|
|
||||||
|
## Project
|
||||||
|
|
||||||
|
- English LTR architecture studio site for Khiyal Architecture Studio (Mostafa Heydarhoseini).
|
||||||
|
- Next.js 16 App Router, React 19, TypeScript, Tailwind 4, `src/app`.
|
||||||
|
- Apex domain: `khiyalstudio.com`.
|
||||||
|
- Language: `en`, direction: `ltr`.
|
||||||
|
- Local images live in `public/images`; local fonts live in `public/fonts` (Antonio + Plus Jakarta Sans, matching the source site).
|
||||||
|
- Public routes in this clone: `/`, `/about-us`, `/contact-us`.
|
||||||
|
- Portfolio and blog listing/detail pages are **not** implemented yet; homepage cards use current site data as local placeholders until Meshkee portfolio/blog endpoints are wired.
|
||||||
|
|
||||||
|
## Source clone
|
||||||
|
|
||||||
|
- Visual language cloned from `https://khiyalstudio.com/` (Crafto Architecture theme: dark gray `#191919`, accent `#efff02`, Antonio headings, Plus Jakarta Sans body).
|
||||||
|
- Logo, banners, project photos, about/contact heroes, and partner marks were downloaded from the live theme/storage.
|
||||||
|
- Color tokens: `--base-color: #efff02`, `--dark-gray: #191919`, `--nero-grey: #1F1F1F`, `--medium-gray: #737373`, `--charcoal-grey: #3E3E3E`.
|
||||||
|
- Footer bottom credit row matches `https://safeteb.com/` (Meshkee animated frame + white logo + “Designed And Developed By Meshkee E-Commerce Team”).
|
||||||
|
|
||||||
|
## Meshkee API
|
||||||
|
|
||||||
|
- Canonical docs: `https://api.meshkee.com/docs/website`
|
||||||
|
- API base: `https://api.meshkee.com/api/v1`
|
||||||
|
- Tenant domain: `khiyalstudio.com` (`SITE_DOMAIN` in `src/lib/site.ts`)
|
||||||
|
- All public fetches go through `src/lib/meshkee.ts`. Do not call the API from components.
|
||||||
|
- Empty or failed static-image slots keep local placeholders.
|
||||||
|
- Do not invent dashboard/CMS/upload endpoints.
|
||||||
|
- Do not add `public/sitemap.xml`, `public/robots.txt`, `app/sitemap.ts`, or `app/robots.ts`; nginx proxies these URLs to Meshkee.
|
||||||
|
|
||||||
|
## Homepage static-image slots
|
||||||
|
|
||||||
|
Dashboard import catalog: `GET /meshkee/static-image-slots`.
|
||||||
|
|
||||||
|
| Key | Kind | itemCount | Aspect | Width | Used on |
|
||||||
|
|---|---|---|---|---|---|
|
||||||
|
| `slider` | list | null | 16:9 | 1920 | Hero (`HeroSlider`) |
|
||||||
|
| `signature` | single | 1 | 16:5 | 480 | Intro signature |
|
||||||
|
| `partners` | list | 4 | 3:2 | 320 | Partner / video logos |
|
||||||
|
| `video-cover` | single | 1 | 16:9 | 1200 | Watch-video cover |
|
||||||
|
| `recent-projects` | list | 3 | 16:8 | 1600 | Recent projects slider |
|
||||||
|
| `news` | list | 3 | 4:5 | 800 | Projects under construction |
|
||||||
|
| `about-hero` | single | 1 | 16:6 | 1920 | About header |
|
||||||
|
| `about-gallery` | list | 3 | 3:4 | 720 | About photos |
|
||||||
|
| `about-owner` | single | 1 | 16:9 | 1400 | Founder portrait |
|
||||||
|
| `contact-hero` | single | 1 | 16:6 | 1920 | Contact header |
|
||||||
|
|
||||||
|
If a rendered image size or crop ratio changes, update the catalog in `src/app/meshkee/static-image-slots/route.ts` (and `STATIC_IMAGE_SLOT_CATALOG`).
|
||||||
|
|
||||||
|
## Sitemap and SEO
|
||||||
|
|
||||||
|
- `GET /meshkee/sitemap-config.json` is generated by scanning public static `src/app/**/page.*` routes.
|
||||||
|
- Auth, admin, and Meshkee integration routes are excluded.
|
||||||
|
- Sitemap base URL uses `NEXT_PUBLIC_SITE_URL`, then `SITE_URL`, with `https://khiyalstudio.com` as the fallback.
|
||||||
|
- Future portfolio/blog details must use Meshkee canonical `{id}/{slug}` URLs when those routes are added.
|
||||||
|
- Every crawlable page must have unique metadata, exactly one H1, structured headings, and meaningful image alt text.
|
||||||
|
|
||||||
|
## Contact
|
||||||
|
|
||||||
|
- Phone: `0912 252 50 08` (`09122525008`)
|
||||||
|
- Email: `Mostafa.eng@live.com`
|
||||||
|
- Instagram: `mostafa_heydar_hoseini`
|
||||||
|
- WhatsApp / Telegram / Bale links live in `src/lib/site.ts`
|
||||||
|
- Contact form is client-side (success state only) until a Meshkee contact endpoint is connected.
|
||||||
|
|
||||||
|
## Verification
|
||||||
|
|
||||||
|
- Run `npm run lint` and `npm run build`.
|
||||||
|
- Verify `/meshkee/static-image-slots` returns the slot catalog.
|
||||||
|
- Verify `/meshkee/sitemap-config.json` lists `/`, `/about-us`, and `/contact-us`.
|
||||||
|
- Check homepage, about, and contact on desktop and mobile; hamburger menu, hero slider, and footer Meshkee credit should match the source palette.
|
||||||
@@ -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;
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
import type { NextConfig } from "next";
|
||||||
|
|
||||||
|
const nextConfig: NextConfig = {
|
||||||
|
images: {
|
||||||
|
remotePatterns: [
|
||||||
|
{
|
||||||
|
protocol: "https",
|
||||||
|
hostname: "c804387.parspack.net",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
protocol: "https",
|
||||||
|
hostname: "meshkee-storage.sas.amin.parminstorage.ir",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export default nextConfig;
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
{
|
||||||
|
"name": "khiyal-studio",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"private": true,
|
||||||
|
"scripts": {
|
||||||
|
"dev": "next dev",
|
||||||
|
"build": "next build",
|
||||||
|
"start": "next start",
|
||||||
|
"lint": "eslint"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"next": "16.3.0",
|
||||||
|
"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.0",
|
||||||
|
"tailwindcss": "^4",
|
||||||
|
"typescript": "^5"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
const config = {
|
||||||
|
plugins: {
|
||||||
|
"@tailwindcss/postcss": {},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export default config;
|
||||||
|
After Width: | Height: | Size: 727 B |
|
After Width: | Height: | Size: 300 KiB |
|
After Width: | Height: | Size: 223 KiB |
|
After Width: | Height: | Size: 256 KiB |
|
After Width: | Height: | Size: 437 KiB |
|
After Width: | Height: | Size: 415 KiB |
|
After Width: | Height: | Size: 182 KiB |
|
After Width: | Height: | Size: 368 KiB |
|
After Width: | Height: | Size: 283 KiB |
|
After Width: | Height: | Size: 274 KiB |
|
After Width: | Height: | Size: 2.1 KiB |
|
After Width: | Height: | Size: 4.8 KiB |
@@ -0,0 +1,10 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="17" height="17" viewBox="0 0 17 17">
|
||||||
|
<defs>
|
||||||
|
<clipPath id="clip-Custom_Size_1">
|
||||||
|
<rect width="17" height="17"/>
|
||||||
|
</clipPath>
|
||||||
|
</defs>
|
||||||
|
<g id="Custom_Size_1" data-name="Custom Size – 1" clip-path="url(#clip-Custom_Size_1)">
|
||||||
|
<rect id="Rectangle_1" data-name="Rectangle 1" width="2" height="2" rx="0.5" transform="translate(8 8)" fill="#252525"/>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 479 B |
@@ -0,0 +1,10 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="17" height="17" viewBox="0 0 17 17">
|
||||||
|
<defs>
|
||||||
|
<clipPath id="clip-Custom_Size_1">
|
||||||
|
<rect width="17" height="17"/>
|
||||||
|
</clipPath>
|
||||||
|
</defs>
|
||||||
|
<g id="Custom_Size_1" data-name="Custom Size – 1" clip-path="url(#clip-Custom_Size_1)">
|
||||||
|
<rect id="Rectangle_1" data-name="Rectangle 1" width="2" height="2" rx="0.5" transform="translate(8 8)" fill="#252525"/>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 479 B |
|
After Width: | Height: | Size: 734 B |
|
After Width: | Height: | Size: 1.2 KiB |
|
After Width: | Height: | Size: 1.5 KiB |
|
After Width: | Height: | Size: 2.2 KiB |
|
After Width: | Height: | Size: 727 B |
|
After Width: | Height: | Size: 1.3 KiB |
|
After Width: | Height: | Size: 32 KiB |
|
After Width: | Height: | Size: 1.4 KiB |
@@ -0,0 +1,11 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="150" height="1" viewBox="0 0 151 1">
|
||||||
|
<defs>
|
||||||
|
<clipPath id="clip-Custom_Size_1">
|
||||||
|
<rect width="150" height="1" transform="translate(150 1801)"/>
|
||||||
|
</clipPath>
|
||||||
|
</defs>
|
||||||
|
<g id="Custom_Size_1" data-name="Custom Size – 1" transform="translate(0 -1801)" clip-path="url(#clip-Custom_Size_1)">
|
||||||
|
<rect width="1" height="5000" transform="translate(150 900.466)" fill="#ffffff" opacity=".05"/>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 557 B |
|
After Width: | Height: | Size: 2.0 KiB |
|
After Width: | Height: | Size: 6.1 KiB |
|
After Width: | Height: | Size: 6.8 KiB |
|
After Width: | Height: | Size: 6.1 KiB |
|
After Width: | Height: | Size: 5.8 KiB |
|
After Width: | Height: | Size: 244 KiB |
|
After Width: | Height: | Size: 252 KiB |
|
After Width: | Height: | Size: 232 KiB |
|
After Width: | Height: | Size: 237 KiB |
|
After Width: | Height: | Size: 248 KiB |
|
After Width: | Height: | Size: 217 KiB |
|
After Width: | Height: | Size: 294 KiB |
|
After Width: | Height: | Size: 222 KiB |
|
After Width: | Height: | Size: 392 KiB |
|
After Width: | Height: | Size: 324 KiB |
|
After Width: | Height: | Size: 212 KiB |
|
After Width: | Height: | Size: 341 KiB |
|
After Width: | Height: | Size: 293 KiB |
|
After Width: | Height: | Size: 214 KiB |
@@ -0,0 +1,160 @@
|
|||||||
|
import type { Metadata } from "next";
|
||||||
|
import Link from "next/link";
|
||||||
|
import { features } from "@/data/content";
|
||||||
|
import {
|
||||||
|
ABOUT_SLOT_KEYS,
|
||||||
|
getStaticImageSlots,
|
||||||
|
imageAlt,
|
||||||
|
imageUrl,
|
||||||
|
slotByKey,
|
||||||
|
slotImages,
|
||||||
|
} from "@/lib/meshkee";
|
||||||
|
|
||||||
|
export const metadata: Metadata = {
|
||||||
|
title: "About Studio",
|
||||||
|
description:
|
||||||
|
"Khiyal Architecture Studio is a contemporary reinterpretation of Iranian architecture — a place where every project is shaped through bold vision, rigorous analysis, and a deep focus on human experience.",
|
||||||
|
alternates: { canonical: "/about-us" },
|
||||||
|
};
|
||||||
|
|
||||||
|
const galleryFallback = [
|
||||||
|
"/images/about/1.jpg",
|
||||||
|
"/images/about/2.jpg",
|
||||||
|
"/images/about/3.jpg",
|
||||||
|
];
|
||||||
|
|
||||||
|
export default async function AboutUsPage() {
|
||||||
|
const slots = await getStaticImageSlots("about-us");
|
||||||
|
const hero = slotImages(slotByKey(slots, ABOUT_SLOT_KEYS.hero))[0];
|
||||||
|
const gallery = slotImages(slotByKey(slots, ABOUT_SLOT_KEYS.gallery));
|
||||||
|
const owner = slotImages(slotByKey(slots, ABOUT_SLOT_KEYS.owner))[0];
|
||||||
|
|
||||||
|
const photos = gallery.length >= 3 ? gallery.map((image) => image.url) : galleryFallback;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<main>
|
||||||
|
<section
|
||||||
|
className="page-hero dotted"
|
||||||
|
style={{ backgroundImage: `url(${imageUrl(hero, "/images/about/subheader.jpg")})` }}
|
||||||
|
>
|
||||||
|
<div>
|
||||||
|
<h1>
|
||||||
|
<span style={{ color: "var(--base-color)" }}>about</span> studio
|
||||||
|
</h1>
|
||||||
|
<h2>Let's see who we are</h2>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section className="section dotted">
|
||||||
|
<div className="container-site intro-grid">
|
||||||
|
<div className="about-visual">
|
||||||
|
<img src={photos[0]} alt="Khiyal studio" />
|
||||||
|
<img src={photos[1]} alt="Khiyal architecture" />
|
||||||
|
<img src={photos[2] || photos[0]} alt="Khiyal project" style={{ gridColumn: "1 / -1" }} />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<span className="eyebrow">About Architecture</span>
|
||||||
|
<h2 className="heading-white">Working group creating pure ideas</h2>
|
||||||
|
<p>
|
||||||
|
Khiyal Architecture Studio is a contemporary reinterpretation of Iranian architecture a place where every
|
||||||
|
project is shaped through bold vision, rigorous analysis, and a deep focus on human experience.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
We create spaces that are unique, meaningful, and beyond conventional standards crafted for clients who
|
||||||
|
value quality, detail, and true creativity.
|
||||||
|
</p>
|
||||||
|
<Link href="/#projects" className="link-line">
|
||||||
|
Explore services →
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="container-site" style={{ marginTop: 64 }}>
|
||||||
|
<h3 className="heading-white" style={{ color: "var(--base-color)" }}>
|
||||||
|
Areas of Expertise – Khiyal Architecture Studio
|
||||||
|
</h3>
|
||||||
|
<p>
|
||||||
|
Khiyal Architecture Studio offers a wide spectrum of architectural and design services, distinguished by
|
||||||
|
creating unique, meaningful, and beyond-conventional spaces. Each project is an opportunity to redefine the
|
||||||
|
experience of living and to reinterpret the principles of refined Iranian architecture in a contemporary way.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<h3 className="heading-white" style={{ color: "var(--base-color)", marginTop: 40 }}>
|
||||||
|
About Us
|
||||||
|
</h3>
|
||||||
|
<p>
|
||||||
|
Khiyal Architecture Studio was founded on the belief that architecture must go beyond construction.
|
||||||
|
Architecture is an act of creation the creation of experience, emotion, quality of life, and meaning.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
We see architecture as a tool to redefine the way people live, and every project as an opportunity to build a
|
||||||
|
new world. Our work ranges from private villas and residential buildings to offices and recreational spaces.
|
||||||
|
What distinguishes Khiyal, however, is not the list of services we provide, but the way we approach each
|
||||||
|
project. Our roots lie in the rich tradition of Iranian architecture, yet we do not treat it as something to
|
||||||
|
imitate.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
Instead, we see it as a living source that can be reborn in contemporary, minimalist, and bold forms. Our
|
||||||
|
goal is to elevate the quality of today’s architecture and to create spaces that resonate with contemporary life
|
||||||
|
and its future.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
At Khiyal, every project is understood before it is formed. We begin with a deep understanding of the client
|
||||||
|
and their way of living, we read the site, climate, light, and context, and we shape the design around one
|
||||||
|
essential question: What experience should this project create? The answer guides us toward a form that is
|
||||||
|
not only beautiful, but meaningful. Before construction, all spatial, technical, and structural scenarios are
|
||||||
|
carefully explored, and we remain alongside the client until the project is fully realized.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
Khiyal is not merely a design studio; it is a way of thinking. We believe in boldness but boldness grounded
|
||||||
|
in analysis and authenticity. We believe in innovation but only when it enhances spatial quality. We are
|
||||||
|
close to minimalism yet never detached from depth and meaning. For us, every project is a unique creation,
|
||||||
|
and materials are not just tools, but a language a way to speak to human emotion and thought.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
We do not design for everyone. Khiyal’s clients are those who value quality and detail, seek meaningful and
|
||||||
|
distinctive spaces, and want their project to exceed ordinary standards.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
For us, every project begins with the client’s vision and gains life through our imagination. Our commitment
|
||||||
|
is to create spaces that are enduring, precise, and sustainable — spaces that elevate the quality of life for
|
||||||
|
years to come. To us, architecture is not merely the making of space, but the creation of a new meaning for
|
||||||
|
being.
|
||||||
|
</p>
|
||||||
|
<p className="heading-white">Khiyal’s client is someone who:</p>
|
||||||
|
<p>
|
||||||
|
values quality and detail, seeks distinctive, meaningful, and different spaces, embraces creativity and
|
||||||
|
boldness in the project, and desires a result beyond conventional standards. We partner with projects that
|
||||||
|
think about the future of quality of life not merely about constructing a building. If you are looking for
|
||||||
|
architecture that offers a different perspective, balances imagination with analysis, and creates a
|
||||||
|
project unlike any other this is Khiyal Architecture Office.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="container-site owner" style={{ marginTop: 56, maxWidth: 980 }}>
|
||||||
|
<img
|
||||||
|
src={imageUrl(owner, "/images/about/owner.jpg")}
|
||||||
|
alt={imageAlt(owner, "Mostafa Heydarhoseini")}
|
||||||
|
/>
|
||||||
|
<h3 className="heading-white" style={{ color: "var(--base-color)", marginTop: 20, marginBottom: 8 }}>
|
||||||
|
Mostafa Heydarhoseini
|
||||||
|
</h3>
|
||||||
|
<p>
|
||||||
|
Architect and founder of Khial Architecture Studio. He sees architecture as the art of shaping the
|
||||||
|
experience of living, and his design defined by a modern, minimal, and bold language seek to create spaces
|
||||||
|
that are enduring, identity-driven, and deeply human
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="container-site feature-grid" style={{ marginTop: 56 }}>
|
||||||
|
{features.map((item) => (
|
||||||
|
<div key={item.number} className="feature-item">
|
||||||
|
<span>{item.text}</span>
|
||||||
|
<span>{item.number}</span>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</main>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,89 @@
|
|||||||
|
import type { Metadata } from "next";
|
||||||
|
import { ContactForm } from "@/components/ContactForm";
|
||||||
|
import { socialLinks } from "@/data/content";
|
||||||
|
import { CONTACT_SLOT_KEYS, getStaticImageSlots, imageUrl, slotByKey, slotImages } from "@/lib/meshkee";
|
||||||
|
import { CONTACT } from "@/lib/site";
|
||||||
|
import { SocialIcon } from "@/components/SocialIcon";
|
||||||
|
|
||||||
|
export const metadata: Metadata = {
|
||||||
|
title: "Contact Us",
|
||||||
|
description:
|
||||||
|
"Let's work together. Contact Khiyal Architecture Studio at 0912 252 50 08 or Mostafa.eng@live.com.",
|
||||||
|
alternates: { canonical: "/contact-us" },
|
||||||
|
};
|
||||||
|
|
||||||
|
export default async function ContactUsPage() {
|
||||||
|
const slots = await getStaticImageSlots("contact-us");
|
||||||
|
const hero = slotImages(slotByKey(slots, CONTACT_SLOT_KEYS.hero))[0];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<main>
|
||||||
|
<section
|
||||||
|
className="page-hero dotted"
|
||||||
|
style={{ backgroundImage: `url(${imageUrl(hero, "/images/about/contact-subheader.jpg")})` }}
|
||||||
|
>
|
||||||
|
<div>
|
||||||
|
<h1>
|
||||||
|
<span style={{ color: "var(--base-color)" }}>contact</span> us
|
||||||
|
</h1>
|
||||||
|
<h2>Let's working together</h2>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section className="section dotted">
|
||||||
|
<div className="container-site contact-top">
|
||||||
|
<div>
|
||||||
|
<span className="eyebrow">Get to know us</span>
|
||||||
|
<h2 className="heading-white" style={{ fontSize: 34, margin: 0 }}>
|
||||||
|
Contact us our offices around the world.
|
||||||
|
</h2>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<span className="eyebrow">Contact us</span>
|
||||||
|
<div style={{ height: 1, background: "var(--charcoal-grey)", margin: "18px 0" }} />
|
||||||
|
<p>
|
||||||
|
<a href={`tel:${CONTACT.phone}`} className="heading-white">
|
||||||
|
{CONTACT.phoneLabel}
|
||||||
|
</a>
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
<a href={`mailto:${CONTACT.email}`} className="heading-white">
|
||||||
|
{CONTACT.email}
|
||||||
|
</a>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<span className="eyebrow">Follow us</span>
|
||||||
|
<div style={{ height: 1, background: "var(--charcoal-grey)", margin: "18px 0" }} />
|
||||||
|
<div className="social-row">
|
||||||
|
{socialLinks
|
||||||
|
.filter((item) => item.icon !== "phone")
|
||||||
|
.map((item) => (
|
||||||
|
<a key={item.label} href={item.href} target="_blank" rel="noreferrer" aria-label={item.label}>
|
||||||
|
<SocialIcon name={item.icon} />
|
||||||
|
</a>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section className="section dotted" style={{ paddingTop: 0 }}>
|
||||||
|
<div className="container-site">
|
||||||
|
<div className="form-wrap">
|
||||||
|
<div style={{ display: "flex", justifyContent: "space-between", alignItems: "center", marginBottom: 32 }}>
|
||||||
|
<h2 className="heading-white" style={{ margin: 0, fontSize: 32 }}>
|
||||||
|
<span style={{ color: "var(--base-color)" }}>Let's talk.</span> Have a project in mind?
|
||||||
|
</h2>
|
||||||
|
<img src="/images/contact-icon.jpg" alt="" style={{ height: 50, width: "auto" }} />
|
||||||
|
</div>
|
||||||
|
<ContactForm />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="watermark" style={{ position: "relative", bottom: -30 }}>
|
||||||
|
architecture
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</main>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
import type { Metadata } from "next";
|
||||||
|
import { Footer } from "@/components/Footer";
|
||||||
|
import { Header } from "@/components/Header";
|
||||||
|
import { SITE_DESCRIPTION, SITE_TITLE, SITE_URL } from "@/lib/site";
|
||||||
|
import "./globals.css";
|
||||||
|
|
||||||
|
export const metadata: Metadata = {
|
||||||
|
metadataBase: new URL(SITE_URL),
|
||||||
|
title: {
|
||||||
|
default: SITE_TITLE,
|
||||||
|
template: "%s | Khiyal Architecture Studio",
|
||||||
|
},
|
||||||
|
description: SITE_DESCRIPTION,
|
||||||
|
icons: { icon: "/favicon.png" },
|
||||||
|
openGraph: {
|
||||||
|
title: SITE_TITLE,
|
||||||
|
description: SITE_DESCRIPTION,
|
||||||
|
url: "/",
|
||||||
|
siteName: "Khiyal Architecture Studio",
|
||||||
|
locale: "en_US",
|
||||||
|
type: "website",
|
||||||
|
images: [{ url: "/images/slider/hayat.jpg", alt: "Khiyal Architecture Studio" }],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function RootLayout({
|
||||||
|
children,
|
||||||
|
}: Readonly<{
|
||||||
|
children: React.ReactNode;
|
||||||
|
}>) {
|
||||||
|
return (
|
||||||
|
<html lang="en" dir="ltr">
|
||||||
|
<body>
|
||||||
|
<Header />
|
||||||
|
{children}
|
||||||
|
<Footer />
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,88 @@
|
|||||||
|
import { readdirSync } from "node:fs";
|
||||||
|
import { join } from "node:path";
|
||||||
|
|
||||||
|
export const dynamic = "force-static";
|
||||||
|
export const runtime = "nodejs";
|
||||||
|
|
||||||
|
type StaticPage = {
|
||||||
|
path: string;
|
||||||
|
changefreq: "daily" | "weekly" | "monthly";
|
||||||
|
priority: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
const EXCLUDED_SEGMENTS = new Set([
|
||||||
|
"account",
|
||||||
|
"admin",
|
||||||
|
"api",
|
||||||
|
"auth",
|
||||||
|
"cart",
|
||||||
|
"checkout",
|
||||||
|
"login",
|
||||||
|
"meshkee",
|
||||||
|
"register",
|
||||||
|
]);
|
||||||
|
|
||||||
|
function collectStaticPagePaths(directory: string, routeSegments: string[] = []): string[] {
|
||||||
|
const entries = readdirSync(directory, { withFileTypes: true });
|
||||||
|
const paths: string[] = [];
|
||||||
|
|
||||||
|
if (entries.some((entry) => entry.isFile() && /^page\.[jt]sx?$/.test(entry.name))) {
|
||||||
|
const path = routeSegments.length ? `/${routeSegments.join("/")}` : "/";
|
||||||
|
paths.push(path);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const entry of entries) {
|
||||||
|
if (!entry.isDirectory()) continue;
|
||||||
|
|
||||||
|
const isRouteGroup = entry.name.startsWith("(") && entry.name.endsWith(")");
|
||||||
|
const isPrivateFolder = entry.name.startsWith("_");
|
||||||
|
const isParallelRoute = entry.name.startsWith("@");
|
||||||
|
const isDynamicRoute = entry.name.includes("[");
|
||||||
|
|
||||||
|
if (isPrivateFolder || isParallelRoute || isDynamicRoute || EXCLUDED_SEGMENTS.has(entry.name)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
paths.push(
|
||||||
|
...collectStaticPagePaths(
|
||||||
|
join(directory, entry.name),
|
||||||
|
isRouteGroup ? routeSegments : [...routeSegments, entry.name],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return paths;
|
||||||
|
}
|
||||||
|
|
||||||
|
function toStaticPage(path: string): StaticPage {
|
||||||
|
if (path === "/") return { path, changefreq: "daily", priority: 1 };
|
||||||
|
if (path === "/about-us") return { path, changefreq: "monthly", priority: 0.6 };
|
||||||
|
if (path === "/contact-us") return { path, changefreq: "monthly", priority: 0.5 };
|
||||||
|
return { path, changefreq: "weekly", priority: 0.5 };
|
||||||
|
}
|
||||||
|
|
||||||
|
export function GET() {
|
||||||
|
const baseUrl = (
|
||||||
|
process.env.NEXT_PUBLIC_SITE_URL ||
|
||||||
|
process.env.SITE_URL ||
|
||||||
|
"https://khiyalstudio.com"
|
||||||
|
).replace(/\/$/, "");
|
||||||
|
|
||||||
|
const appDir = join(process.cwd(), "src/app");
|
||||||
|
const staticPages = collectStaticPagePaths(appDir)
|
||||||
|
.sort((first, second) => {
|
||||||
|
if (first === "/") return -1;
|
||||||
|
if (second === "/") return 1;
|
||||||
|
return first.localeCompare(second);
|
||||||
|
})
|
||||||
|
.map(toStaticPage);
|
||||||
|
|
||||||
|
return Response.json(
|
||||||
|
{ baseUrl, staticPages },
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
"Cache-Control": "public, max-age=300, stale-while-revalidate=3600",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
import { STATIC_IMAGE_SLOT_CATALOG } from "@/lib/meshkee";
|
||||||
|
|
||||||
|
export function GET() {
|
||||||
|
return Response.json(
|
||||||
|
{ slots: STATIC_IMAGE_SLOT_CATALOG },
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
"Cache-Control": "public, max-age=300, stale-while-revalidate=3600",
|
||||||
|
"Access-Control-Allow-Origin": "*",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,230 @@
|
|||||||
|
import type { Metadata } from "next";
|
||||||
|
import Link from "next/link";
|
||||||
|
import { Counters } from "@/components/Counters";
|
||||||
|
import { HeroSlider } from "@/components/HeroSlider";
|
||||||
|
import { PortfolioCarousel } from "@/components/PortfolioCarousel";
|
||||||
|
import { MarqueeZoom } from "@/components/MarqueeZoom";
|
||||||
|
import { Testimonials } from "@/components/Testimonials";
|
||||||
|
import {
|
||||||
|
features,
|
||||||
|
heroSlides,
|
||||||
|
listedProjects,
|
||||||
|
newsItems,
|
||||||
|
partners,
|
||||||
|
recentProjects,
|
||||||
|
} from "@/data/content";
|
||||||
|
import {
|
||||||
|
HOME_SLOT_KEYS,
|
||||||
|
getStaticImageSlots,
|
||||||
|
imageAlt,
|
||||||
|
imageUrl,
|
||||||
|
slotByKey,
|
||||||
|
slotImages,
|
||||||
|
} from "@/lib/meshkee";
|
||||||
|
import { CONTACT, SITE_DESCRIPTION, SITE_TITLE } from "@/lib/site";
|
||||||
|
|
||||||
|
export const metadata: Metadata = {
|
||||||
|
title: SITE_TITLE,
|
||||||
|
description: SITE_DESCRIPTION,
|
||||||
|
alternates: { canonical: "/" },
|
||||||
|
};
|
||||||
|
|
||||||
|
export default async function HomePage() {
|
||||||
|
const slots = await getStaticImageSlots("home");
|
||||||
|
const sliderImages = slotImages(slotByKey(slots, HOME_SLOT_KEYS.slider));
|
||||||
|
const slides =
|
||||||
|
sliderImages.length > 0
|
||||||
|
? sliderImages.map((image, index) => ({
|
||||||
|
src: image.url,
|
||||||
|
title: image.titleEn || image.title || heroSlides[index % heroSlides.length]?.title || "Khiyal",
|
||||||
|
number: String(index + 1).padStart(2, "0"),
|
||||||
|
nextNumber: String(((index + 1) % Math.max(sliderImages.length, 1)) + 1).padStart(2, "0"),
|
||||||
|
}))
|
||||||
|
: heroSlides;
|
||||||
|
|
||||||
|
const signature = slotImages(slotByKey(slots, HOME_SLOT_KEYS.signature))[0];
|
||||||
|
const partnerImages = slotImages(slotByKey(slots, HOME_SLOT_KEYS.partners));
|
||||||
|
const videoCover = slotImages(slotByKey(slots, HOME_SLOT_KEYS.videoCover))[0];
|
||||||
|
const recentSlot = slotImages(slotByKey(slots, HOME_SLOT_KEYS.recentProjects));
|
||||||
|
const newsSlot = slotImages(slotByKey(slots, HOME_SLOT_KEYS.news));
|
||||||
|
|
||||||
|
const recent =
|
||||||
|
recentSlot.length > 0
|
||||||
|
? recentSlot.map((image, index) => ({
|
||||||
|
src: image.url,
|
||||||
|
title: image.titleEn || image.title || recentProjects[index]?.title || "Project",
|
||||||
|
category: image.subtext || "Architecture",
|
||||||
|
}))
|
||||||
|
: recentProjects;
|
||||||
|
|
||||||
|
const news =
|
||||||
|
newsSlot.length > 0
|
||||||
|
? newsSlot.map((image, index) => ({
|
||||||
|
src: image.url,
|
||||||
|
title: image.titleEn || image.title || newsItems[index]?.title || "Project",
|
||||||
|
category: image.subtext || "Architect",
|
||||||
|
}))
|
||||||
|
: newsItems;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<main>
|
||||||
|
<HeroSlider slides={slides} />
|
||||||
|
|
||||||
|
<section className="section dotted">
|
||||||
|
<div className="container-site intro-grid">
|
||||||
|
<div>
|
||||||
|
<img
|
||||||
|
src={imageUrl(signature, "/images/brand/signature.png")}
|
||||||
|
alt={imageAlt(signature, "Khiyal signature")}
|
||||||
|
style={{ maxWidth: 280 }}
|
||||||
|
/>
|
||||||
|
<h2 className="heading-white" style={{ fontSize: "28px", marginTop: 20 }}>
|
||||||
|
Khiyal | Between Thought and Construction Architecture, experienced before it is built
|
||||||
|
</h2>
|
||||||
|
</div>
|
||||||
|
<div className="years-wrap">
|
||||||
|
<div className="years-badge">
|
||||||
|
<div className="circle" />
|
||||||
|
<div className="num">15</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<span className="eyebrow" style={{ color: "#fff" }}>
|
||||||
|
Established for 15 years.
|
||||||
|
</span>
|
||||||
|
<p>
|
||||||
|
At Khiyal Architecture Studio, design transcends meaning and becomes space. We craft environments that
|
||||||
|
fulfill functional needs while creating profound and enduring human experiences
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="container-site feature-grid">
|
||||||
|
{features.map((item) => (
|
||||||
|
<div key={item.number} className="feature-item">
|
||||||
|
<span>{item.text}</span>
|
||||||
|
<span>{item.number}</span>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section id="services" className="section dotted">
|
||||||
|
<div className="container-site">
|
||||||
|
<div className="intro-grid" style={{ marginBottom: 48 }}>
|
||||||
|
<div>
|
||||||
|
<span className="eyebrow">Architecture services</span>
|
||||||
|
<h2 className="heading-white" style={{ fontSize: "30px" }}>
|
||||||
|
Creating human-centered, lasting spaces—from villa design to residential and commercial architecture
|
||||||
|
</h2>
|
||||||
|
</div>
|
||||||
|
<p>
|
||||||
|
At Khiyal Studio, design is not merely built—it is brought to life through boldness and innovation; we craft
|
||||||
|
spaces that are distinctive, memorable, and beyond the ordinary
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<PortfolioCarousel />
|
||||||
|
</div>
|
||||||
|
<div className="container-site">
|
||||||
|
<div className="partners">
|
||||||
|
{(partnerImages.length
|
||||||
|
? partnerImages.map((image, index) => ({
|
||||||
|
src: image.url,
|
||||||
|
href: image.linkUrl || partners[index]?.href || "#",
|
||||||
|
alt: imageAlt(image, partners[index]?.alt || "Partner"),
|
||||||
|
}))
|
||||||
|
: partners
|
||||||
|
).map((item) => (
|
||||||
|
<a
|
||||||
|
key={item.src}
|
||||||
|
href={item.href}
|
||||||
|
target={item.href.startsWith("http") ? "_blank" : undefined}
|
||||||
|
rel="noreferrer"
|
||||||
|
>
|
||||||
|
<img src={item.src} alt={item.alt} />
|
||||||
|
</a>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section className="video-split">
|
||||||
|
<div
|
||||||
|
className="video-cover"
|
||||||
|
style={{
|
||||||
|
backgroundImage: `url(${imageUrl(videoCover, "/images/video-cover.jpg")})`,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<div className="video-cta">
|
||||||
|
<a href={CONTACT.video} className="play-btn" target="_blank" rel="noreferrer" aria-label="Watch video">
|
||||||
|
▶
|
||||||
|
</a>
|
||||||
|
<h2>
|
||||||
|
click for <span>watch video</span>
|
||||||
|
</h2>
|
||||||
|
<div className="watermark text-outline">architecture</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section id="projects" className="section dotted" style={{ background: "var(--nero-grey)" }}>
|
||||||
|
<div className="container-site intro-grid">
|
||||||
|
<div>
|
||||||
|
<span className="eyebrow">Best Projects</span>
|
||||||
|
<h2 className="heading-white" style={{ fontSize: 30 }}>
|
||||||
|
Projects brought to life with the collaboration of clients who cherished every corner of their buildings.
|
||||||
|
</h2>
|
||||||
|
<p>
|
||||||
|
At Khiyal Architecture Studio, design goes beyond form and function. Through boldness and creativity, we
|
||||||
|
craft spaces that are meaningful, experiential, and enduring. Our approach blends innovation, attention to
|
||||||
|
detail, and respect for human needs, where every project tells a unique story
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div className="project-list">
|
||||||
|
{listedProjects.map((item) => (
|
||||||
|
<Link key={item.name} href="/#projects">
|
||||||
|
<span style={{ color: "var(--base-color)", fontWeight: 600 }}>{item.year}</span>
|
||||||
|
<span>{item.name}</span>
|
||||||
|
<span style={{ color: "var(--medium-gray)" }}>{item.type}</span>
|
||||||
|
<span>→</span>
|
||||||
|
</Link>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section className="section dotted" style={{ paddingTop: 0 }}>
|
||||||
|
<div className="container-site">
|
||||||
|
<RecentProjects items={recent} />
|
||||||
|
<Counters />
|
||||||
|
<Testimonials />
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section className="section dotted marquee-section">
|
||||||
|
<MarqueeZoom />
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section id="news" className="section dotted news-section">
|
||||||
|
<div className="container-site">
|
||||||
|
<div style={{ textAlign: "center", marginBottom: 36 }}>
|
||||||
|
<span className="eyebrow">Architecture news</span>
|
||||||
|
<h2 className="heading-white">Projects Under Construction Current Developments</h2>
|
||||||
|
</div>
|
||||||
|
<div className="news-grid">
|
||||||
|
{news.map((item) => (
|
||||||
|
<figure key={item.title} className="news-card">
|
||||||
|
<img src={item.src} alt={item.title} />
|
||||||
|
<figcaption>
|
||||||
|
<span className="chip">{item.category}</span>
|
||||||
|
<strong style={{ fontSize: 22 }}>{item.title}</strong>
|
||||||
|
</figcaption>
|
||||||
|
</figure>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="news-watermark" aria-hidden>
|
||||||
|
architecture
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</main>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,59 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import type { FormEvent } from "react";
|
||||||
|
import { useState } from "react";
|
||||||
|
|
||||||
|
export function ContactForm() {
|
||||||
|
const [submitted, setSubmitted] = useState(false);
|
||||||
|
|
||||||
|
const submit = (event: FormEvent<HTMLFormElement>) => {
|
||||||
|
event.preventDefault();
|
||||||
|
setSubmitted(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
if (submitted) {
|
||||||
|
return (
|
||||||
|
<div className="success-box">
|
||||||
|
<h3 className="heading-white">Your message has been sent</h3>
|
||||||
|
<p>The Khiyal studio team will get back to you shortly.</p>
|
||||||
|
<button className="submit" type="button" onClick={() => setSubmitted(false)}>
|
||||||
|
Send another message
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<form className="form-grid" onSubmit={submit}>
|
||||||
|
<div>
|
||||||
|
<label htmlFor="name">Your name*</label>
|
||||||
|
<input id="name" name="name" required placeholder="What's your good name?" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label htmlFor="email">Your email address*</label>
|
||||||
|
<input id="email" name="email" type="email" required placeholder="Enter your email address" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label htmlFor="phone">Your phone number</label>
|
||||||
|
<input id="phone" name="phone" type="tel" placeholder="Enter your phone number" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label htmlFor="subject">Your subject</label>
|
||||||
|
<input id="subject" name="subject" placeholder="How can we help you?" />
|
||||||
|
</div>
|
||||||
|
<div className="full">
|
||||||
|
<label htmlFor="message">Your message</label>
|
||||||
|
<textarea id="message" name="message" rows={4} placeholder="Describe about your project" />
|
||||||
|
</div>
|
||||||
|
<p className="full" style={{ margin: 0 }}>
|
||||||
|
We are committed to protecting your privacy. We will never collect information about you without your explicit
|
||||||
|
consent.
|
||||||
|
</p>
|
||||||
|
<div className="full" style={{ textAlign: "right" }}>
|
||||||
|
<button className="submit" type="submit">
|
||||||
|
Send message
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useEffect, useRef, useState } from "react";
|
||||||
|
import { counters } from "@/data/content";
|
||||||
|
|
||||||
|
export function Counters() {
|
||||||
|
const ref = useRef<HTMLDivElement>(null);
|
||||||
|
const [started, setStarted] = useState(false);
|
||||||
|
const [values, setValues] = useState(counters.map(() => 0));
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const node = ref.current;
|
||||||
|
if (!node) return;
|
||||||
|
const observer = new IntersectionObserver(
|
||||||
|
([entry]) => {
|
||||||
|
if (entry.isIntersecting) setStarted(true);
|
||||||
|
},
|
||||||
|
{ threshold: 0.3 },
|
||||||
|
);
|
||||||
|
observer.observe(node);
|
||||||
|
return () => observer.disconnect();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!started) return;
|
||||||
|
const duration = 1400;
|
||||||
|
const start = performance.now();
|
||||||
|
const frame = (now: number) => {
|
||||||
|
const t = Math.min(1, (now - start) / duration);
|
||||||
|
setValues(counters.map((item) => Math.round(item.value * t)));
|
||||||
|
if (t < 1) requestAnimationFrame(frame);
|
||||||
|
};
|
||||||
|
const id = requestAnimationFrame(frame);
|
||||||
|
return () => cancelAnimationFrame(id);
|
||||||
|
}, [started]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="counters" ref={ref}>
|
||||||
|
{counters.map((item, i) => (
|
||||||
|
<div key={item.label}>
|
||||||
|
<strong>{values[i]}+</strong>
|
||||||
|
<span className="eyebrow">{item.label}</span>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,70 @@
|
|||||||
|
import Link from "next/link";
|
||||||
|
import { socialLinks } from "@/data/content";
|
||||||
|
import { CONTACT } from "@/lib/site";
|
||||||
|
import { SocialIcon } from "./SocialIcon";
|
||||||
|
|
||||||
|
export function Footer() {
|
||||||
|
return (
|
||||||
|
<footer className="site-footer dotted">
|
||||||
|
<div className="container-site">
|
||||||
|
<div style={{ display: "flex", justifyContent: "flex-end", marginBottom: 36 }}>
|
||||||
|
<Link href="/">
|
||||||
|
<img src="/images/brand/logo-footer.png" alt="Khiyal Architecture Studio" style={{ height: 70, width: "auto" }} />
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
<div className="footer-cols">
|
||||||
|
<div>
|
||||||
|
<h3>Appointments</h3>
|
||||||
|
<p>For better access to the office, please visit the contact page.</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h3>Let's talk</h3>
|
||||||
|
<a href={`mailto:${CONTACT.email}`} className="heading-white">
|
||||||
|
{CONTACT.email}
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h3>Connect with us</h3>
|
||||||
|
<div className="social-row">
|
||||||
|
{socialLinks.map((item) => (
|
||||||
|
<a key={item.label} href={item.href} target="_blank" rel="noreferrer" aria-label={item.label}>
|
||||||
|
<SocialIcon name={item.icon} />
|
||||||
|
</a>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="footer-bottom">
|
||||||
|
<div className="container-site footer-bottom-bar">
|
||||||
|
<div className="footer-bottom-site">Khiyal Architecture Studio</div>
|
||||||
|
<a
|
||||||
|
href="https://meshkee.com"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
dir="ltr"
|
||||||
|
className="meshkee-credit"
|
||||||
|
>
|
||||||
|
<span className="meshkee-credit__frame" aria-hidden>
|
||||||
|
<span className="meshkee-credit__line meshkee-credit__line--l" />
|
||||||
|
<span className="meshkee-credit__line meshkee-credit__line--t" />
|
||||||
|
<span className="meshkee-credit__line meshkee-credit__line--r" />
|
||||||
|
<span className="meshkee-credit__line meshkee-credit__line--b" />
|
||||||
|
</span>
|
||||||
|
<img
|
||||||
|
src="/images/brand/meshkee-logo-white.png"
|
||||||
|
alt="Meshkee"
|
||||||
|
className="meshkee-credit__logo"
|
||||||
|
width={42}
|
||||||
|
height={42}
|
||||||
|
/>
|
||||||
|
<span className="meshkee-credit__text">
|
||||||
|
<span className="meshkee-credit__label">Designed And Developed By</span>
|
||||||
|
<span className="meshkee-credit__name">Meshkee E-Commerce Team</span>
|
||||||
|
</span>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</footer>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,89 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import Link from "next/link";
|
||||||
|
import { usePathname } from "next/navigation";
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
|
import { navItems, socialLinks } from "@/data/content";
|
||||||
|
import { CONTACT } from "@/lib/site";
|
||||||
|
import { SocialIcon } from "./SocialIcon";
|
||||||
|
|
||||||
|
export function Header() {
|
||||||
|
const [open, setOpen] = useState(false);
|
||||||
|
const pathname = usePathname();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
document.body.classList.toggle("menu-open", open);
|
||||||
|
return () => document.body.classList.remove("menu-open");
|
||||||
|
}, [open]);
|
||||||
|
|
||||||
|
const close = () => setOpen(false);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<header className="site-header">
|
||||||
|
<Link href="/" className="logo" aria-label="Khiyal Architecture Studio">
|
||||||
|
<img src="/images/brand/logo-white.png" alt="Khiyal Architecture Studio" />
|
||||||
|
</Link>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="hamburger"
|
||||||
|
aria-label="Open menu"
|
||||||
|
aria-expanded={open}
|
||||||
|
onClick={() => setOpen(true)}
|
||||||
|
>
|
||||||
|
<span />
|
||||||
|
<span />
|
||||||
|
<span />
|
||||||
|
<span />
|
||||||
|
</button>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<div className={`menu-overlay ${open ? "open" : ""}`} role="dialog" aria-modal="true">
|
||||||
|
<button type="button" className="menu-close" aria-label="Close menu" onClick={() => setOpen(false)}>
|
||||||
|
×
|
||||||
|
</button>
|
||||||
|
<div className="menu-grid">
|
||||||
|
<div>
|
||||||
|
<Link href="/" onClick={close}>
|
||||||
|
<img src="/images/brand/logo-footer.png" alt="Khiyal" style={{ height: 72, width: "auto" }} />
|
||||||
|
</Link>
|
||||||
|
<div className="menu-meta">
|
||||||
|
<div>
|
||||||
|
<span className="eyebrow">Appointments</span>
|
||||||
|
<p>For better access to the office, please visit the contact page.</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<span className="eyebrow">Let's talk</span>
|
||||||
|
<a href={`mailto:${CONTACT.email}`} className="heading-white">
|
||||||
|
{CONTACT.email}
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<span className="eyebrow">Connect with us</span>
|
||||||
|
<div className="social-row">
|
||||||
|
{socialLinks.map((item) => (
|
||||||
|
<a key={item.label} href={item.href} target="_blank" rel="noreferrer" aria-label={item.label}>
|
||||||
|
<SocialIcon name={item.icon} />
|
||||||
|
</a>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<nav className="menu-nav" aria-label="Primary">
|
||||||
|
{navItems.map((item) => (
|
||||||
|
<Link
|
||||||
|
key={item.label}
|
||||||
|
href={item.href}
|
||||||
|
className={pathname === item.href ? "active" : undefined}
|
||||||
|
onClick={close}
|
||||||
|
>
|
||||||
|
{item.label}
|
||||||
|
</Link>
|
||||||
|
))}
|
||||||
|
</nav>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,58 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import Link from "next/link";
|
||||||
|
import { useEffect, useMemo, useState } from "react";
|
||||||
|
import { heroSlides } from "@/data/content";
|
||||||
|
|
||||||
|
type Slide = {
|
||||||
|
src: string;
|
||||||
|
title: string;
|
||||||
|
number: string;
|
||||||
|
nextNumber: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function HeroSlider({ slides }: { slides?: Slide[] }) {
|
||||||
|
const items = useMemo(() => (slides && slides.length ? slides : heroSlides), [slides]);
|
||||||
|
const [index, setIndex] = useState(0);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const timer = window.setInterval(() => {
|
||||||
|
setIndex((current) => (current + 1) % items.length);
|
||||||
|
}, 5500);
|
||||||
|
return () => window.clearInterval(timer);
|
||||||
|
}, [items.length]);
|
||||||
|
|
||||||
|
const slide = items[index];
|
||||||
|
if (!slide) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<section className="hero">
|
||||||
|
{items.map((item, i) => (
|
||||||
|
<div key={`${item.src}-${i}`} className={`hero-slide ${i === index ? "active" : ""}`}>
|
||||||
|
<img src={item.src} alt={item.title} />
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
<div className="hero-shade" />
|
||||||
|
<div className="hero-number" aria-hidden>
|
||||||
|
{slide.number}
|
||||||
|
</div>
|
||||||
|
<div className="hero-copy">
|
||||||
|
<div>
|
||||||
|
<h1 className="hero-title">{slide.title}</h1>
|
||||||
|
<Link href="/#projects" className="hero-explore">
|
||||||
|
<span className="circle">→</span>
|
||||||
|
Explore project
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="hero-nav">
|
||||||
|
<button type="button" className="next-arrow" aria-label="Next slide" onClick={() => setIndex((i) => (i + 1) % items.length)}>
|
||||||
|
→
|
||||||
|
</button>
|
||||||
|
<button type="button" className="next-num" aria-label="Next slide" onClick={() => setIndex((i) => (i + 1) % items.length)}>
|
||||||
|
{slide.nextNumber}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useEffect, useRef } from "react";
|
||||||
|
|
||||||
|
export function MarqueeZoom() {
|
||||||
|
const ref = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const el = ref.current;
|
||||||
|
if (!el) return;
|
||||||
|
if (window.matchMedia("(prefers-reduced-motion: reduce)").matches) return;
|
||||||
|
|
||||||
|
let ticking = false;
|
||||||
|
|
||||||
|
const update = () => {
|
||||||
|
ticking = false;
|
||||||
|
const rect = el.getBoundingClientRect();
|
||||||
|
const viewHeight = window.innerHeight;
|
||||||
|
const start = viewHeight;
|
||||||
|
const end = -rect.height;
|
||||||
|
const progress = Math.min(1, Math.max(0, (start - rect.top) / (start - end || 1)));
|
||||||
|
const scale = 1.5 + (0.7 - 1.5) * progress;
|
||||||
|
const x = 100 + (-200 - 100) * progress;
|
||||||
|
el.style.transform = `translate3d(${x}px, 0px, 0px) scale(${scale})`;
|
||||||
|
};
|
||||||
|
|
||||||
|
const onScroll = () => {
|
||||||
|
if (ticking) return;
|
||||||
|
ticking = true;
|
||||||
|
requestAnimationFrame(update);
|
||||||
|
};
|
||||||
|
|
||||||
|
update();
|
||||||
|
window.addEventListener("scroll", onScroll, { passive: true });
|
||||||
|
window.addEventListener("resize", onScroll);
|
||||||
|
return () => {
|
||||||
|
window.removeEventListener("scroll", onScroll);
|
||||||
|
window.removeEventListener("resize", onScroll);
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div ref={ref} className="marquee">
|
||||||
|
<span className="text-outline">we love</span> architecture <span className="text-outline">and</span> interior
|
||||||
|
design
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,54 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
|
import { portfolioItems } from "@/data/content";
|
||||||
|
|
||||||
|
export function PortfolioCarousel() {
|
||||||
|
const [index, setIndex] = useState(0);
|
||||||
|
const [paused, setPaused] = useState(false);
|
||||||
|
const [instant, setInstant] = useState(false);
|
||||||
|
const count = portfolioItems.length;
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (paused) return;
|
||||||
|
const timer = window.setInterval(() => {
|
||||||
|
setIndex((current) => current + 1);
|
||||||
|
}, 4000);
|
||||||
|
return () => window.clearInterval(timer);
|
||||||
|
}, [paused]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!instant) return;
|
||||||
|
const frame = requestAnimationFrame(() => {
|
||||||
|
requestAnimationFrame(() => setInstant(false));
|
||||||
|
});
|
||||||
|
return () => cancelAnimationFrame(frame);
|
||||||
|
}, [instant]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={`portfolio-bleed${instant ? " is-instant" : ""}`}
|
||||||
|
style={{ ["--slide" as string]: index }}
|
||||||
|
onMouseEnter={() => setPaused(true)}
|
||||||
|
onMouseLeave={() => setPaused(false)}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className="portfolio-track"
|
||||||
|
onTransitionEnd={() => {
|
||||||
|
if (index < count) return;
|
||||||
|
setInstant(true);
|
||||||
|
setIndex(0);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{[...portfolioItems, ...portfolioItems].map((item, i) => (
|
||||||
|
<article key={`${item.titleEn}-${i}`} className="portfolio-card">
|
||||||
|
<img src={item.image} alt={item.titleEn} />
|
||||||
|
<div className="copy">
|
||||||
|
<h3>{item.titleEn}</h3>
|
||||||
|
</div>
|
||||||
|
</article>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,90 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import Link from "next/link";
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
|
import { recentProjects } from "@/data/content";
|
||||||
|
|
||||||
|
type Item = { src: string; title: string; category: string };
|
||||||
|
|
||||||
|
function ArrowDownLeft() {
|
||||||
|
return (
|
||||||
|
<svg viewBox="0 0 16 16" aria-hidden>
|
||||||
|
<path
|
||||||
|
fill="currentColor"
|
||||||
|
fillRule="evenodd"
|
||||||
|
d="M2 13.5a.5.5 0 0 0 .5.5h8a.5.5 0 0 0 0-1H3.707L13.854 2.854a.5.5 0 0 0-.708-.708L3 12.293V4.5a.5.5 0 0 0-1 0z"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function ArrowUpRight() {
|
||||||
|
return (
|
||||||
|
<svg viewBox="0 0 16 16" aria-hidden>
|
||||||
|
<path
|
||||||
|
fill="currentColor"
|
||||||
|
fillRule="evenodd"
|
||||||
|
d="M14 2.5a.5.5 0 0 0-.5-.5h-8a.5.5 0 0 0 0 1h6.793L2.146 13.146a.5.5 0 0 0 .708.708L13 3.707V11.5a.5.5 0 0 0 1 0z"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function RecentProjects({ items }: { items?: Item[] }) {
|
||||||
|
const slides = items && items.length ? items : recentProjects;
|
||||||
|
const [index, setIndex] = useState(0);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const timer = window.setInterval(() => {
|
||||||
|
setIndex((current) => (current + 1) % slides.length);
|
||||||
|
}, 4000);
|
||||||
|
return () => window.clearInterval(timer);
|
||||||
|
}, [index, slides.length]);
|
||||||
|
|
||||||
|
if (!slides.length) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="recent-slide">
|
||||||
|
<div className="vertical-label" aria-hidden>
|
||||||
|
Recent projects
|
||||||
|
</div>
|
||||||
|
{slides.map((item, i) => (
|
||||||
|
<article key={`${item.src}-${i}`} className={`recent-slide-item${i === index ? " is-active" : ""}`}>
|
||||||
|
<img src={item.src} alt={item.title} />
|
||||||
|
<div className="recent-card">
|
||||||
|
<span className="eyebrow" style={{ color: "#191919" }}>
|
||||||
|
{item.category}
|
||||||
|
</span>
|
||||||
|
<h2
|
||||||
|
className="heading-white"
|
||||||
|
style={{ color: "#191919", fontFamily: "var(--alt-font)", fontSize: "48px", margin: "8px 0 18px" }}
|
||||||
|
>
|
||||||
|
{item.title}
|
||||||
|
</h2>
|
||||||
|
<Link href="/#projects" className="link-line" style={{ color: "#191919" }}>
|
||||||
|
Explore project →
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
</article>
|
||||||
|
))}
|
||||||
|
<div className="recent-nav">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="recent-next"
|
||||||
|
aria-label="Next project"
|
||||||
|
onClick={() => setIndex((i) => (i + 1) % slides.length)}
|
||||||
|
>
|
||||||
|
<ArrowUpRight />
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="recent-prev"
|
||||||
|
aria-label="Previous project"
|
||||||
|
onClick={() => setIndex((i) => (i - 1 + slides.length) % slides.length)}
|
||||||
|
>
|
||||||
|
<ArrowDownLeft />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
export function SocialIcon({ name }: { name: "phone" | "whatsapp" | "telegram" | "bale" | "instagram" }) {
|
||||||
|
const common = { width: 16, height: 16, fill: "currentColor", "aria-hidden": true as const };
|
||||||
|
if (name === "phone") {
|
||||||
|
return (
|
||||||
|
<svg {...common} viewBox="0 0 24 24">
|
||||||
|
<path d="M6.62 10.79a15.05 15.05 0 006.59 6.59l2.2-2.2a1 1 0 011.01-.24c1.12.37 2.33.57 3.57.57a1 1 0 011 1V20a1 1 0 01-1 1C10.07 21 3 13.93 3 5a1 1 0 011-1h3.5a1 1 0 011 1c0 1.24.2 2.45.57 3.57a1 1 0 01-.25 1.02l-2.2 2.2z" />
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (name === "whatsapp") {
|
||||||
|
return (
|
||||||
|
<svg {...common} viewBox="0 0 24 24">
|
||||||
|
<path d="M20.5 3.5A11 11 0 003.4 17.7L2 22l4.4-1.4A11 11 0 1020.5 3.5zm-8.5 17a9.1 9.1 0 01-4.6-1.3l-.33-.2-2.7.85.87-2.63-.21-.34A9.1 9.1 0 1112 20.5zm5-6.8c-.27-.14-1.6-.79-1.85-.88s-.43-.14-.61.14-.7.88-.86 1.06-.32.2-.59.07a7.4 7.4 0 01-2.18-1.34 8.1 8.1 0 01-1.5-1.86c-.16-.27 0-.42.12-.55s.27-.32.41-.49.18-.27.27-.45.04-.34-.02-.48-.61-1.47-.84-2.02c-.22-.53-.45-.46-.61-.47h-.52c-.18 0-.48.07-.73.34s-.96.94-.96 2.29.98 2.66 1.12 2.84 1.93 2.95 4.67 4.14c.65.28 1.16.45 1.56.58.65.2 1.25.18 1.72.11.52-.08 1.6-.65 1.83-1.28s.23-1.17.16-1.28-.24-.18-.51-.32z" />
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (name === "telegram") {
|
||||||
|
return (
|
||||||
|
<svg {...common} viewBox="0 0 24 24">
|
||||||
|
<path d="M21.5 4.3L2.8 11.5c-1.3.5-1.3 1.2-.2 1.5l4.8 1.5 11.1-7c.5-.3.9-.1.6.2l-9 8.2-.3 4.6c.5 0 .7-.2 1-.5l2.3-2.2 4.8 3.5c.9.5 1.5.2 1.7-.8l3.1-14.6c.3-1.2-.5-1.7-1.3-1.4z" />
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (name === "bale") {
|
||||||
|
return (
|
||||||
|
<svg width="16" height="13" viewBox="0 0 16.159 12.176" aria-hidden>
|
||||||
|
<path
|
||||||
|
d="M12.119.553a1.783 1.783 0 0 0-2.583 0L5.554 4.536a1.783 1.783 0 0 0 0 2.583l8.486 8.487a1.826 1.826 0 1 0 2.583-2.583l-7.2-7.2 2.691-2.691A1.783 1.783 0 0 0 12.119.553Z"
|
||||||
|
transform="translate(0 17.176) rotate(-90)"
|
||||||
|
fill="currentColor"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<svg {...common} viewBox="0 0 24 24">
|
||||||
|
<path d="M7 3h10a4 4 0 014 4v10a4 4 0 01-4 4H7a4 4 0 01-4-4V7a4 4 0 014-4zm10 1.8H7A2.2 2.2 0 004.8 7v10A2.2 2.2 0 007 19.2h10a2.2 2.2 0 002.2-2.2V7A2.2 2.2 0 0017 4.8zM12 8.2A3.8 3.8 0 1112 15.8 3.8 3.8 0 0112 8.2zm0 1.6a2.2 2.2 0 100 4.4 2.2 2.2 0 000-4.4zM17.35 6.4a.95.95 0 110 1.9.95.95 0 010-1.9z" />
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,79 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
|
import { testimonials } from "@/data/content";
|
||||||
|
|
||||||
|
function ArrowLeft() {
|
||||||
|
return (
|
||||||
|
<svg viewBox="0 0 16 16" aria-hidden>
|
||||||
|
<path
|
||||||
|
fill="currentColor"
|
||||||
|
fillRule="evenodd"
|
||||||
|
d="M15 8a.5.5 0 0 0-.5-.5H2.707l3.147-3.146a.5.5 0 1 0-.708-.708l-4 4a.5.5 0 0 0 0 .708l4 4a.5.5 0 0 0 .708-.708L2.707 8.5H14.5A.5.5 0 0 0 15 8"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function ArrowRight() {
|
||||||
|
return (
|
||||||
|
<svg viewBox="0 0 16 16" aria-hidden>
|
||||||
|
<path
|
||||||
|
fill="currentColor"
|
||||||
|
fillRule="evenodd"
|
||||||
|
d="M1 8a.5.5 0 0 1 .5-.5h11.793l-3.147-3.146a.5.5 0 0 1 .708-.708l4 4a.5.5 0 0 1 0 .708l-4 4a.5.5 0 0 1-.708-.708L13.293 8.5H1.5A.5.5 0 0 1 1 8"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function Testimonials() {
|
||||||
|
const [index, setIndex] = useState(0);
|
||||||
|
const count = testimonials.length;
|
||||||
|
const progress = ((index + 1) / count) * 100;
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const timer = window.setInterval(() => {
|
||||||
|
setIndex((current) => (current + 1) % count);
|
||||||
|
}, 4000);
|
||||||
|
return () => window.clearInterval(timer);
|
||||||
|
}, [count, index]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="quote-wrap">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="quote-side prev"
|
||||||
|
aria-label="Previous comment"
|
||||||
|
onClick={() => setIndex((i) => (i - 1 + count) % count)}
|
||||||
|
>
|
||||||
|
<ArrowLeft />
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="quote-side next"
|
||||||
|
aria-label="Next comment"
|
||||||
|
onClick={() => setIndex((i) => (i + 1) % count)}
|
||||||
|
>
|
||||||
|
<ArrowRight />
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<div className="quote-slides">
|
||||||
|
{testimonials.map((item, i) => (
|
||||||
|
<div key={item.author} className={`quote${i === index ? " is-active" : ""}`}>
|
||||||
|
{item.quote}
|
||||||
|
<span className="quote-author">- {item.author}</span>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="quote-pager">
|
||||||
|
<span>{String(index + 1).padStart(2, "0")}</span>
|
||||||
|
<div className="quote-progress">
|
||||||
|
<span style={{ width: `${progress}%` }} />
|
||||||
|
</div>
|
||||||
|
<span>{String(count).padStart(2, "0")}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,171 @@
|
|||||||
|
import { CONTACT } from "@/lib/site";
|
||||||
|
|
||||||
|
export const navItems = [
|
||||||
|
{ href: "/", label: "Home" },
|
||||||
|
{ href: "/about-us", label: "About" },
|
||||||
|
{ href: "/#services", label: "Services" },
|
||||||
|
{ href: "/#projects", label: "Projects" },
|
||||||
|
{ href: "/#news", label: "Product design" },
|
||||||
|
{ href: "/contact-us", label: "Contact" },
|
||||||
|
];
|
||||||
|
|
||||||
|
export const heroSlides = [
|
||||||
|
{
|
||||||
|
src: "/images/slider/arsan.jpg",
|
||||||
|
title: "Arsan",
|
||||||
|
number: "01",
|
||||||
|
nextNumber: "02",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
src: "/images/slider/chesmandaz.jpg",
|
||||||
|
title: "Chesmandaz",
|
||||||
|
number: "02",
|
||||||
|
nextNumber: "03",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
src: "/images/slider/tarak.jpg",
|
||||||
|
title: "Tarak",
|
||||||
|
number: "03",
|
||||||
|
nextNumber: "04",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
src: "/images/slider/billiard.jpg",
|
||||||
|
title: "Billiard",
|
||||||
|
number: "04",
|
||||||
|
nextNumber: "05",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
src: "/images/slider/hayat.jpg",
|
||||||
|
title: "Hayat",
|
||||||
|
number: "05",
|
||||||
|
nextNumber: "01",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
export const features = [
|
||||||
|
{
|
||||||
|
number: "01",
|
||||||
|
text: "Meeting the expectations of discerning clients",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
number: "02",
|
||||||
|
text: "Our thinking and design go beyond client expectations.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
number: "03",
|
||||||
|
text: "Over 500 distinctive projects, from the smallest detail to the grandest vision, have been designed at our studio.",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
export const portfolioItems = [
|
||||||
|
{
|
||||||
|
title: "نوجان",
|
||||||
|
titleEn: "Nojan",
|
||||||
|
image: "/images/projects/nojan-thumb.jpg",
|
||||||
|
href: "/#projects",
|
||||||
|
text: "The volumetric design and lines of this building started from the edges of the existing condition of the project land, and during the course of the plan design, with the effort we had to correct the angles and angles of the land, we gradually extended the interior lines to the courtyard, and by continuing these lines and fractures, we formed the volume of the building. As a result, the interior spaces were born without angles and in accordance with the client's wishes, and the volume of the building was coordinated with the landscaping lines by adding a form on the facade.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "حیات",
|
||||||
|
titleEn: "Hayat",
|
||||||
|
image: "/images/projects/hayat-thumb.jpg",
|
||||||
|
href: "/#projects",
|
||||||
|
text: "This image is a view of the Hayat project. In this facade, which is based on vertical bricks and the soft lines of the terraces; The rhythm of the bricks creates a sense of order and continuity, and the protruding window frames play with depth and shadow. Amidst this regular geometry, the greenery of the terraces injects the flow of life into the facade. A balance between urban order and the softness of nature; The same spirit that flows in the designs of Imagination.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "چشم انداز",
|
||||||
|
titleEn: "Chesmandaz",
|
||||||
|
image: "/images/projects/chesmandaz-thumb.jpg",
|
||||||
|
href: "/#projects",
|
||||||
|
text: "The Landscape Building project was the result of a change from the routine requirements in all villa buildings to just a service hall and kitchen, which resulted in a change in the attitude and thought path of architects. Every villa building is a place that can interact with nature and specifically manifest the spirit of greenery. Our team in this project examined all climatic issues and prepared a response for actions that are appropriate to human behavior and performance. Both green space was brought inside and the quality of the view was promoted by expanding the view to new angles and mountain and garden views. The form in this building event is completely based on the climate, disturbing and desirable views, expanding the views that are provided to the audience in a wide frame by increasing the height of the horizon.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "آرسان",
|
||||||
|
titleEn: "Arsan",
|
||||||
|
image: "/images/projects/arsan-thumb.jpg",
|
||||||
|
href: "/#projects",
|
||||||
|
text: "Arsan Company Project. Renovation / Office. The client expected the project to be a space that was progressive and different, considering the nature of their profession, which is a smart building company. They needed a type of space that would be a manifestation of technological advancements and comfort management techniques and a platform for introducing emerging building control products. The outcome of our joint brainstorming sessions with the client was that this design should be something that the viewer would see in terms of form, shape, color, and texture that had not been narrated to them before or interpreted differently here. It should be modern, mysterious, not discovered at first encounter, but transparent and clear so that it could better express the features of the smart building. The use of new colors, circular forms in the middle of the space, and the use of soft lines in surfaces and furniture, dividing the space with smart glass, the effective presence of plants, textured surfaces, and the use of metal elements were the tools used to achieve the goal.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "دی",
|
||||||
|
titleEn: "Dey",
|
||||||
|
image: "/images/projects/dey-thumb.jpg",
|
||||||
|
href: "/#projects",
|
||||||
|
text: "In the design process: A combination of turquoise green granite and beige brick with a touch of metal. The effort that was made was to play with the arrangement of bricks, so that the sunken parts with voluminous forms, although they are the same material and color, but with the subtle volumetric play of bricks, the shape is different here. The green volume was created using the existing features in the structure, which of course was slightly arranged to take the hardness of the concrete features.",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
export const partners = [
|
||||||
|
{
|
||||||
|
src: "/images/partners/client-38.png",
|
||||||
|
href: "https://www.aparat.com/v/uxi63k7",
|
||||||
|
alt: "Render",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
src: "/images/partners/client-39.png",
|
||||||
|
href: "https://www.aparat.com/v/cqbwx3t",
|
||||||
|
alt: "Model",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
src: "/images/partners/client-40.png",
|
||||||
|
href: "https://www.aparat.com/v/zqrq155",
|
||||||
|
alt: "Partner company",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
src: "/images/partners/client-41.png",
|
||||||
|
href: "/about-us",
|
||||||
|
alt: "Authorized store",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
export const listedProjects = [
|
||||||
|
{ year: "2018", name: "Cheshm andaz", type: "Architecture" },
|
||||||
|
{ year: "2015", name: "Billiard", type: "Architecture" },
|
||||||
|
{ year: "2020", name: "Arsan", type: "Architecture" },
|
||||||
|
{ year: "2025", name: "Hayat", type: "Architecture" },
|
||||||
|
];
|
||||||
|
|
||||||
|
export const recentProjects = [
|
||||||
|
{ src: "/images/projects/tarak.jpg", title: "Tarak", category: "Architecture" },
|
||||||
|
{ src: "/images/projects/shahrad.jpg", title: "Shahrad", category: "Architecture" },
|
||||||
|
{ src: "/images/projects/hayat.jpg", title: "Hayat", category: "Architecture" },
|
||||||
|
];
|
||||||
|
|
||||||
|
export const counters = [
|
||||||
|
{ value: 1000, label: "Render" },
|
||||||
|
{ value: 128, label: "Model" },
|
||||||
|
{ value: 269, label: "Partner Company" },
|
||||||
|
{ value: 528, label: "Authorized Store" },
|
||||||
|
];
|
||||||
|
|
||||||
|
export const testimonials = [
|
||||||
|
{
|
||||||
|
quote:
|
||||||
|
"Imagination is the bridge between thought and reality, a place where ideas are born and nurtured before taking shape. In art and architecture, imaginative thinking is the driving force of innovation, breaking conventional boundaries and creating spaces that transcend mere form and function. At Khiyal Architecture Studio, we harness this power with boldness and precision to craft projects that offer profound human experiences while maintaining a distinctive spatial language. Each project tells a story of the interplay between thought and space, inspiration and execution, where architecture is not merely built—it comes alive.",
|
||||||
|
author: "Imagination in Art",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
quote:
|
||||||
|
"Most of our clients describe working with Khiyal Architecture Studio as not only professional but also inspiring and enjoyable. They note that the team listens to ideas with boldness and precision, executes details meticulously, and creates spaces that are both functional and deeply human. Collaborating with Khiyal means experiencing trust, calm, and excitement all at once while shaping a unique environment.",
|
||||||
|
author: "What Most Clients Say",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
quote:
|
||||||
|
"For me, architecture goes beyond constructing buildings. It is about crafting experiences, transforming ideas into spaces, and creating moments in which people live and feel. At Khiyal Studio, each project is an opportunity for bold design, meticulous attention to detail, and the creation of spaces that are both functional and meaningful. I see architecture as a place where art and science, creativity and experience, come together to produce spaces that transcend form and function and truly communicate with their occupants.",
|
||||||
|
author: "Mostafa Heydarhoseini",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
export const newsItems = [
|
||||||
|
{ src: "/images/blog/hayat.jpg", title: "HAYAT", category: "Architect" },
|
||||||
|
{ src: "/images/blog/shahrad.jpg", title: "SHAHRAD", category: "Architect" },
|
||||||
|
{ src: "/images/blog/tarak.jpg", title: "TARAK", category: "Architect" },
|
||||||
|
];
|
||||||
|
|
||||||
|
export const socialLinks = [
|
||||||
|
{ href: `tel:${CONTACT.phone}`, label: "Call", icon: "phone" as const },
|
||||||
|
{ href: CONTACT.whatsapp, label: "WhatsApp", icon: "whatsapp" as const },
|
||||||
|
{ href: CONTACT.telegram, label: "Telegram", icon: "telegram" as const },
|
||||||
|
{ href: CONTACT.bale, label: "Bale", icon: "bale" as const },
|
||||||
|
{ href: CONTACT.instagram, label: "Instagram", icon: "instagram" as const },
|
||||||
|
];
|
||||||
@@ -0,0 +1,185 @@
|
|||||||
|
import type { CSSProperties } from "react";
|
||||||
|
import { API_BASE, SITE_DOMAIN } from "@/lib/site";
|
||||||
|
|
||||||
|
export type StaticImage = {
|
||||||
|
id: string;
|
||||||
|
url: string;
|
||||||
|
title?: string | null;
|
||||||
|
titleFa?: string | null;
|
||||||
|
titleEn?: string | null;
|
||||||
|
subtext?: string | null;
|
||||||
|
linkUrl?: string | null;
|
||||||
|
width?: number | null;
|
||||||
|
height?: number | null;
|
||||||
|
sortOrder?: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type StaticImageSlot = {
|
||||||
|
id: string;
|
||||||
|
key: string;
|
||||||
|
kind: "single" | "list";
|
||||||
|
label: string;
|
||||||
|
aspectRatio?: string | null;
|
||||||
|
recommendedWidth?: number | null;
|
||||||
|
itemCount?: number | null;
|
||||||
|
pageKey?: string;
|
||||||
|
images: StaticImage[];
|
||||||
|
};
|
||||||
|
|
||||||
|
export const HOME_SLOT_KEYS = {
|
||||||
|
slider: "slider",
|
||||||
|
signature: "signature",
|
||||||
|
partners: "partners",
|
||||||
|
videoCover: "video-cover",
|
||||||
|
recentProjects: "recent-projects",
|
||||||
|
news: "news",
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
export const ABOUT_SLOT_KEYS = {
|
||||||
|
gallery: "about-gallery",
|
||||||
|
owner: "about-owner",
|
||||||
|
hero: "about-hero",
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
export const CONTACT_SLOT_KEYS = {
|
||||||
|
hero: "contact-hero",
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
export const STATIC_IMAGE_SLOT_CATALOG = [
|
||||||
|
{
|
||||||
|
key: HOME_SLOT_KEYS.slider,
|
||||||
|
label: "Homepage slider",
|
||||||
|
kind: "list" as const,
|
||||||
|
pageKey: "home",
|
||||||
|
aspectRatio: "16:9",
|
||||||
|
itemCount: null,
|
||||||
|
recommendedWidth: 1920,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: HOME_SLOT_KEYS.signature,
|
||||||
|
label: "Homepage signature",
|
||||||
|
kind: "single" as const,
|
||||||
|
pageKey: "home",
|
||||||
|
aspectRatio: "16:5",
|
||||||
|
itemCount: 1,
|
||||||
|
recommendedWidth: 480,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: HOME_SLOT_KEYS.partners,
|
||||||
|
label: "Partner / video logos",
|
||||||
|
kind: "list" as const,
|
||||||
|
pageKey: "home",
|
||||||
|
aspectRatio: "3:2",
|
||||||
|
itemCount: 4,
|
||||||
|
recommendedWidth: 320,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: HOME_SLOT_KEYS.videoCover,
|
||||||
|
label: "Watch video cover",
|
||||||
|
kind: "single" as const,
|
||||||
|
pageKey: "home",
|
||||||
|
aspectRatio: "16:9",
|
||||||
|
itemCount: 1,
|
||||||
|
recommendedWidth: 1200,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: HOME_SLOT_KEYS.recentProjects,
|
||||||
|
label: "Recent projects slider",
|
||||||
|
kind: "list" as const,
|
||||||
|
pageKey: "home",
|
||||||
|
aspectRatio: "16:8",
|
||||||
|
itemCount: 3,
|
||||||
|
recommendedWidth: 1600,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: HOME_SLOT_KEYS.news,
|
||||||
|
label: "Projects under construction",
|
||||||
|
kind: "list" as const,
|
||||||
|
pageKey: "home",
|
||||||
|
aspectRatio: "4:5",
|
||||||
|
itemCount: 3,
|
||||||
|
recommendedWidth: 800,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: ABOUT_SLOT_KEYS.hero,
|
||||||
|
label: "About page header",
|
||||||
|
kind: "single" as const,
|
||||||
|
pageKey: "about-us",
|
||||||
|
aspectRatio: "16:6",
|
||||||
|
itemCount: 1,
|
||||||
|
recommendedWidth: 1920,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: ABOUT_SLOT_KEYS.gallery,
|
||||||
|
label: "About page gallery",
|
||||||
|
kind: "list" as const,
|
||||||
|
pageKey: "about-us",
|
||||||
|
aspectRatio: "3:4",
|
||||||
|
itemCount: 3,
|
||||||
|
recommendedWidth: 720,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: ABOUT_SLOT_KEYS.owner,
|
||||||
|
label: "Founder portrait",
|
||||||
|
kind: "single" as const,
|
||||||
|
pageKey: "about-us",
|
||||||
|
aspectRatio: "16:9",
|
||||||
|
itemCount: 1,
|
||||||
|
recommendedWidth: 1400,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: CONTACT_SLOT_KEYS.hero,
|
||||||
|
label: "Contact page header",
|
||||||
|
kind: "single" as const,
|
||||||
|
pageKey: "contact-us",
|
||||||
|
aspectRatio: "16:6",
|
||||||
|
itemCount: 1,
|
||||||
|
recommendedWidth: 1920,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
async function meshkeeFetch<T>(path: string, revalidateSeconds = 60): Promise<T | null> {
|
||||||
|
try {
|
||||||
|
const response = await fetch(`${API_BASE}/tenants/${SITE_DOMAIN}${path}`, {
|
||||||
|
next: { revalidate: revalidateSeconds },
|
||||||
|
headers: { Accept: "application/json" },
|
||||||
|
});
|
||||||
|
if (!response.ok) return null;
|
||||||
|
return (await response.json()) as T;
|
||||||
|
} catch {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getStaticImageSlots(pageKey?: string): Promise<StaticImageSlot[]> {
|
||||||
|
const qs = pageKey ? `?pageKey=${encodeURIComponent(pageKey)}` : "";
|
||||||
|
const data = await meshkeeFetch<{ items?: StaticImageSlot[] }>(
|
||||||
|
`/website/static-images${qs}`,
|
||||||
|
);
|
||||||
|
return Array.isArray(data?.items) ? data.items : [];
|
||||||
|
}
|
||||||
|
|
||||||
|
export function slotByKey(slots: StaticImageSlot[], key: string) {
|
||||||
|
return slots.find((slot) => slot.key === key);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function slotImages(slot?: StaticImageSlot) {
|
||||||
|
const images = slot?.images?.filter((image) => image.url) ?? [];
|
||||||
|
if (slot?.kind === "single") return images.slice(0, 1);
|
||||||
|
return [...images].sort((a, b) => (a.sortOrder ?? 0) - (b.sortOrder ?? 0));
|
||||||
|
}
|
||||||
|
|
||||||
|
export function imageUrl(image: StaticImage | undefined, fallback: string) {
|
||||||
|
return image?.url || fallback;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function imageAlt(image: StaticImage | undefined, fallback: string) {
|
||||||
|
return image?.titleEn || image?.title || image?.titleFa || fallback;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function aspectRatioStyle(ratio?: string | null): CSSProperties | undefined {
|
||||||
|
if (!ratio) return undefined;
|
||||||
|
const [width, height] = ratio.split(":");
|
||||||
|
if (!width || !height) return undefined;
|
||||||
|
return { aspectRatio: `${width} / ${height}` };
|
||||||
|
}
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
export const SITE_DOMAIN = "khiyalstudio.com";
|
||||||
|
export const SITE_URL = "https://khiyalstudio.com";
|
||||||
|
export const API_BASE =
|
||||||
|
process.env.NEXT_PUBLIC_MESHKEE_API_URL?.replace(/\/$/, "") ??
|
||||||
|
"https://api.meshkee.com/api/v1";
|
||||||
|
|
||||||
|
export const SITE_NAME = "Khiyal Architecture Studio";
|
||||||
|
export const SITE_TITLE = "Khiyal | Between Thought and Construction";
|
||||||
|
export const SITE_DESCRIPTION =
|
||||||
|
"At Khiyal Architecture Studio, design transcends meaning and becomes space. We craft environments that fulfill functional needs while creating profound and enduring human experiences.";
|
||||||
|
|
||||||
|
export const CONTACT = {
|
||||||
|
email: "Mostafa.eng@live.com",
|
||||||
|
phone: "09122525008",
|
||||||
|
phoneLabel: "0912 252 50 08",
|
||||||
|
whatsapp: "https://api.whatsapp.com/send?phone=09122525008&text=%D8%B3%D9%84%D8%A7%D9%85",
|
||||||
|
telegram: "https://t.me/+6Kw7qDzBJKphOWQ0",
|
||||||
|
bale: "https://ble.ir/join/CLEXk5asgD",
|
||||||
|
instagram:
|
||||||
|
"https://www.instagram.com/mostafa_heydar_hoseini?igsh=MXh5MzRtM3g4bHoycQ==",
|
||||||
|
video: "https://www.aparat.com/v/oiz515o",
|
||||||
|
};
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
{
|
||||||
|
"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"]
|
||||||
|
}
|
||||||