Add contact and about pages with shared CTA buttons.

Wire forms to the Meshkee contact API, tighten page-title spacing, and polish outline button hover so it fades cleanly.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Alireza Hassani
2026-09-18 17:54:34 +03:30
co-authored by Cursor
parent b0ba7b1de8
commit e6404eaaec
17 changed files with 880 additions and 97 deletions
+207
View File
@@ -0,0 +1,207 @@
import type { Metadata } from "next";
import Link from "next/link";
import { Button } from "@/components/Button";
import { ContentPageShell } from "@/components/ContentPageShell";
import { images } from "@/data/assets";
import { services, site } from "@/data/home";
export const metadata: Metadata = {
title: "درباره ما",
description:
"آشنایی با تیم برنامه‌نویسی و تجارت الکترونیک مشکی — طراحی وبسایت، اپلیکیشن، CMS، سئو و گرافیک.",
alternates: { canonical: "/about" },
};
const highlights = [
{
title: "تیم متخصص",
body: "برای هر بخش — از گرافیک و برنامه‌نویسی تا سرور و سئو — متخصص جداگانه‌ای کنار پروژه‌تان می‌ماند.",
},
{
title: "یکپارچگی کامل",
body: "وبسایت، اپلیکیشن و سیستم‌های مدیریتی طوری طراحی می‌شوند که با هم کار کنند، نه جدا از هم.",
},
{
title: "رشد پایدار",
body: "از طراحی اولیه تا سئو و مدیریت محتوا، مسیر رشد برند آنلاین شما را همراهی می‌کنیم.",
},
];
const stats = [
{ value: "112+", label: "وبسایت" },
{ value: "28+", label: "لوگو و هویت بصری" },
{ value: "7+", label: "اپلیکیشن" },
];
export default function AboutPage() {
return (
<ContentPageShell flushTop>
<section>
<div className="page-title-band">
<div className="relative mx-auto max-w-7xl px-5 pb-9 pt-[calc(var(--header-height)+0.15rem)] lg:px-8 lg:pb-12 lg:pt-[calc(var(--header-height)+0.3rem)]">
<p
aria-hidden
className="latin pointer-events-none absolute inset-x-5 top-[calc(var(--header-height)+0.5rem)] z-0 text-[clamp(1.75rem,5.5vw,3.75rem)] font-light uppercase leading-none tracking-[0.05em] text-ink/[0.07] lg:inset-x-8 lg:top-[calc(var(--header-height)+0.65rem)]"
>
About
</p>
<div className="reveal relative z-10 max-w-2xl">
<h1 className="whitespace-normal text-[clamp(1.35rem,3.6vw,2.75rem)] font-normal leading-tight tracking-tight text-ink">
درباره ما
</h1>
<p className="mt-1 text-sm leading-6 text-muted md:text-base">
تیم برنامه‌نویسی و تجارت الکترونیک مشکی — راهکارهای مدرن برای
تجارت شما.
</p>
</div>
</div>
</div>
<div className="bg-white">
<div className="mx-auto grid max-w-7xl items-center gap-12 px-5 py-16 lg:grid-cols-2 lg:gap-16 lg:px-8 lg:py-24">
<div className="reveal min-w-0">
<p className="mb-4 text-sm text-muted">ما کی هستیم؟</p>
<h2 className="mb-6 max-w-[16em] text-[clamp(1.2rem,3vw,2rem)] font-normal leading-snug tracking-tight text-ink">
{site.fullTitle}
</h2>
<p className="text-body mb-5 text-sm leading-8 text-ink-soft md:text-base md:leading-9">
مشکی یک تیم تخصصی در حوزه طراحی و توسعه محصولات دیجیتال است. ما
کنار کسب‌وکارها می‌ایستیم تا وبسایت، اپلیکیشن، سیستم مدیریتی و
هویت بصری‌شان را از ایده تا اجرا پیش ببریم.
</p>
<p className="text-body mb-8 text-sm leading-8 text-muted md:text-base md:leading-9">
دفتر ما در قم است و پروژه‌ها را با تمرکز روی کیفیت فنی، تجربه
کاربری و رشد پایدار جلو می‌بریم — نه فقط تحویل یک محصول، بلکه
ساخت مسیری برای فروش و حضور آنلاین قوی‌تر.
</p>
<div className="flex flex-wrap gap-3">
<Button href="/contact">تماس با ما</Button>
<Button href="/portfolio" variant="outline">
نمونه‌کارها
</Button>
</div>
</div>
<div className="reveal reveal-delay-1 relative mx-auto w-full max-w-md lg:max-w-none">
<div className="pointer-events-none absolute inset-8 rounded-full bg-[radial-gradient(circle,var(--glow),transparent_70%)] blur-2xl" />
<div className="relative aspect-square">
<img
src={images.hero}
alt={site.fullTitle}
className="h-full w-full object-contain drop-shadow-[0_30px_60px_rgba(14,17,22,0.14)]"
/>
</div>
</div>
</div>
<div className="border-y border-line/60 bg-[#FAFAFA]">
<div className="mx-auto grid max-w-7xl grid-cols-1 divide-y divide-line/60 sm:grid-cols-3 sm:divide-x sm:divide-y-0">
{stats.map((stat) => (
<div
key={stat.label}
className="reveal flex items-center gap-4 px-5 py-8 lg:px-8 lg:py-10"
>
<p
className="latin shrink-0 text-[clamp(2rem,4vw,2.75rem)] font-light leading-none tracking-tight text-ink"
dir="ltr"
>
{stat.value}
</p>
<span aria-hidden className="h-10 w-px shrink-0 bg-line/80" />
<p className="text-sm text-ink-soft">{stat.label}</p>
</div>
))}
</div>
</div>
<div className="mx-auto max-w-7xl px-5 py-16 lg:px-8 lg:py-24">
<div className="reveal mb-12 max-w-2xl">
<h2 className="mb-3 text-[clamp(1.2rem,3vw,2rem)] font-normal tracking-tight text-ink">
چطور کار می‌کنیم؟
</h2>
<p className="text-sm leading-8 text-muted md:text-base">
سه اصل ساده که در همه پروژه‌های مشکی ثابت می‌ماند.
</p>
</div>
<div className="grid gap-10 md:grid-cols-3 md:gap-8">
{highlights.map((item, index) => (
<div
key={item.title}
className={`reveal min-w-0 ${
index === 1
? "reveal-delay-1"
: index === 2
? "reveal-delay-2"
: ""
}`}
>
<p className="latin mb-3 text-xs tracking-[0.18em] text-muted">
0{index + 1}
</p>
<h3 className="mb-3 text-lg font-normal text-ink">
{item.title}
</h3>
<p className="text-sm leading-8 text-muted">{item.body}</p>
</div>
))}
</div>
</div>
<div className="border-t border-line/50 bg-[#FAFAFA]">
<div className="mx-auto max-w-7xl px-5 py-16 lg:px-8 lg:py-20">
<div className="reveal mb-10 max-w-2xl">
<h2 className="mb-3 text-[clamp(1.2rem,3vw,2rem)] font-normal tracking-tight text-ink">
خدمات ما
</h2>
<p className="text-sm leading-8 text-muted md:text-base">
از ایده تا اجرا، هر بخشی که برای حضور دیجیتال نیاز دارید.
</p>
</div>
<ul className="grid gap-4 sm:grid-cols-2 lg:grid-cols-3">
{services.map((service, index) => (
<li key={service.href}>
<Link
href={service.href}
className={`reveal group block rounded-2xl border border-line/60 bg-white px-5 py-5 transition hover:border-ink/20 ${
index % 3 === 1
? "reveal-delay-1"
: index % 3 === 2
? "reveal-delay-2"
: ""
}`}
>
<p className="mb-2 text-base text-ink group-hover:text-ink">
{service.title}
</p>
<p className="latin title-en-sm text-muted">
{service.titleEn}
</p>
</Link>
</li>
))}
</ul>
<div className="reveal mt-14 flex flex-wrap items-center justify-between gap-6 border-t border-line/60 pt-10">
<div>
<p className="mb-2 text-lg text-ink">
آماده‌اید پروژه بعدی را شروع کنید؟
</p>
<p className="text-sm text-muted">
از طریق فرم تماس یا شماره{" "}
<span className="latin" dir="ltr">
{site.phone}
</span>{" "}
با ما در ارتباط باشید.
</p>
</div>
<Button href="/contact">برو به تماس با ما</Button>
</div>
</div>
</div>
</div>
</section>
</ContentPageShell>
);
}
+43
View File
@@ -0,0 +1,43 @@
import { NextResponse } from "next/server";
import { SITE_DOMAIN } from "@/lib/api";
const API_BASE_URL = "https://api.meshkee.com/api/v1";
export async function POST(request: Request) {
const body = (await request.json()) as {
title?: string;
name?: string;
email?: string;
cellNumber?: string;
text?: string;
};
if (!body.title?.trim() || !body.name?.trim() || !body.text?.trim()) {
return NextResponse.json(
{ message: "نام، موضوع و متن پیام الزامی است." },
{ status: 400 },
);
}
const response = await fetch(
`${API_BASE_URL}/tenants/${SITE_DOMAIN}/contact-submissions`,
{
method: "POST",
headers: {
Accept: "application/json",
"Content-Type": "application/json",
},
body: JSON.stringify({
title: body.title.trim(),
name: body.name.trim(),
email: body.email?.trim() || undefined,
cellNumber: body.cellNumber?.trim() || undefined,
text: body.text.trim(),
}),
cache: "no-store",
},
);
const data = await response.json().catch(() => ({}));
return NextResponse.json(data, { status: response.status });
}
+2 -2
View File
@@ -84,10 +84,10 @@ export default async function BlogDetailPage({ params }: BlogDetailPageProps) {
<ContentPageShell flushTop> <ContentPageShell flushTop>
<article className="pb-20 lg:pb-28"> <article className="pb-20 lg:pb-28">
<div className="page-title-band mb-8 md:mb-10"> <div className="page-title-band mb-8 md:mb-10">
<div className="relative mx-auto max-w-7xl px-5 pb-4 pt-[calc(var(--header-height)+0.75rem)] lg:px-8 lg:pb-5 lg:pt-[calc(var(--header-height)+1rem)]"> <div className="relative mx-auto max-w-7xl px-5 pb-10 pt-[calc(var(--header-height)+0.15rem)] lg:px-8 lg:pb-12 lg:pt-[calc(var(--header-height)+0.3rem)]">
<p <p
aria-hidden aria-hidden
className="latin pointer-events-none absolute inset-x-5 top-[calc(var(--header-height)+2.4rem)] z-0 text-[clamp(1.75rem,5.5vw,3.75rem)] font-light uppercase leading-none tracking-[0.05em] text-ink/[0.07] lg:inset-x-8 lg:top-[calc(var(--header-height)+2.6rem)]" className="latin pointer-events-none absolute inset-x-5 top-[calc(var(--header-height)+1.75rem)] z-0 text-[clamp(1.75rem,5.5vw,3.75rem)] font-light uppercase leading-none tracking-[0.05em] text-ink/[0.07] lg:inset-x-8 lg:top-[calc(var(--header-height)+1.95rem)]"
> >
{titleEn} {titleEn}
</p> </p>
+147
View File
@@ -0,0 +1,147 @@
import type { Metadata } from "next";
import { ContentPageShell } from "@/components/ContentPageShell";
import { ContactForm } from "@/components/ContactForm";
import { site, socials } from "@/data/home";
export const metadata: Metadata = {
title: "تماس با ما",
description:
"با تیم مشکی در تماس باشید — فرم تماس، تلفن، ایمیل و آدرس دفتر قم.",
alternates: { canonical: "/contact" },
};
export default function ContactPage() {
return (
<ContentPageShell flushTop>
<section>
<div className="page-title-band">
<div className="relative mx-auto max-w-7xl px-5 pb-9 pt-[calc(var(--header-height)+0.15rem)] lg:px-8 lg:pb-12 lg:pt-[calc(var(--header-height)+0.3rem)]">
<p
aria-hidden
className="latin pointer-events-none absolute inset-x-5 top-[calc(var(--header-height)+0.5rem)] z-0 text-[clamp(1.75rem,5.5vw,3.75rem)] font-light uppercase leading-none tracking-[0.05em] text-ink/[0.07] lg:inset-x-8 lg:top-[calc(var(--header-height)+0.65rem)]"
>
Contact
</p>
<div className="reveal relative z-10 max-w-2xl">
<h1 className="whitespace-normal text-[clamp(1.35rem,3.6vw,2.75rem)] font-normal leading-tight tracking-tight text-ink">
تماس با ما
</h1>
<p className="mt-1 text-sm leading-6 text-muted md:text-base">
سوالی دارید یا می‌خواهید پروژه‌تان را شروع کنید؟ با تیم مشکی در
تماس باشید.
</p>
</div>
</div>
</div>
<div className="relative flex min-h-[calc(100svh-var(--header-height)-7.5rem)] items-center overflow-hidden bg-[linear-gradient(180deg,#fafafa_0%,#eef1f5_45%,#f3f5f7_100%)] py-16 lg:py-20">
<div className="pointer-events-none absolute inset-0 bg-[radial-gradient(ellipse_at_80%_20%,rgba(196,77,184,0.1),transparent_50%)]" />
<div className="relative mx-auto grid w-full max-w-7xl items-center gap-14 px-5 lg:grid-cols-[1fr_1.1fr] lg:gap-16 lg:px-8">
<div className="reveal flex min-w-0 flex-col gap-8">
<div className="grid grid-cols-[auto_minmax(0,1fr)] items-baseline gap-x-4 gap-y-6">
<span className="text-sm text-muted">شماره تماس</span>
<a
href={site.phoneHref}
className="latin link-hover justify-self-start text-lg font-medium tracking-tight text-ink md:text-xl"
dir="ltr"
>
{site.phone}
</a>
<span className="text-sm text-muted">ایمیل</span>
<a
href={`mailto:${site.email}`}
className="latin link-hover justify-self-start text-sm text-ink-soft md:text-base"
dir="ltr"
>
{site.email}
</a>
<span className="text-sm text-muted">آدرس</span>
<a
href={site.mapUrl}
target="_blank"
rel="noopener noreferrer"
className="link-hover justify-self-start text-start text-sm leading-7 text-ink-soft md:text-base md:leading-8"
>
{site.address}
</a>
<span className="pt-1 text-sm text-muted">شبکه‌های اجتماعی</span>
<div className="flex flex-wrap items-center justify-start gap-3 pt-1">
{socials.map((social) => (
<a
key={social.label}
href={social.href}
target="_blank"
rel="noopener noreferrer"
aria-label={social.label}
title={social.label}
className="flex h-11 w-11 items-center justify-center rounded-full border border-line/70 bg-white/60 text-ink-soft shadow-[0_8px_20px_rgba(14,17,22,0.04)] backdrop-blur-sm transition hover:border-ink/20 hover:text-ink"
>
<svg
viewBox="0 0 24 24"
fill="currentColor"
className="h-4 w-4"
aria-hidden
>
<path d={social.icon} />
</svg>
</a>
))}
</div>
</div>
<div className="relative overflow-hidden rounded-[1.5rem] border border-line/70 bg-white/50 shadow-[0_16px_40px_rgba(14,17,22,0.05)] backdrop-blur-sm">
<div className="relative aspect-[16/10] w-full overflow-hidden sm:aspect-[2/1]">
<iframe
title="موقعیت دفتر مشکی روی نقشه"
src={`https://maps.google.com/maps?q=${site.mapLat},${site.mapLng}&z=16&hl=fa&output=embed`}
className="absolute inset-0 h-full w-full border-0"
loading="lazy"
referrerPolicy="no-referrer-when-downgrade"
allowFullScreen
/>
</div>
<a
href={site.mapUrl}
target="_blank"
rel="noopener noreferrer"
className="absolute bottom-3 inset-inline-3 inline-flex max-w-max items-center gap-2 rounded-full border border-white/70 bg-white/90 px-3.5 py-2 text-sm text-ink shadow-sm backdrop-blur-md transition hover:bg-white"
>
<svg
viewBox="0 0 24 24"
fill="currentColor"
className="h-4 w-4 shrink-0 text-accent"
aria-hidden
>
<path d="M12 2C8.13 2 5 5.13 5 9c0 5.25 7 13 7 13s7-7.75 7-13c0-3.87-3.13-7-7-7zm0 9.5a2.5 2.5 0 110-5 2.5 2.5 0 010 5z" />
</svg>
مشاهده روی نقشه
</a>
</div>
</div>
<div className="reveal reveal-delay-1 relative min-w-0 overflow-hidden rounded-[1.75rem] border border-white/70 bg-white/45 p-5 shadow-[0_20px_50px_rgba(14,17,22,0.06)] backdrop-blur-xl sm:p-6 md:p-8">
<div
aria-hidden
className="pointer-events-none absolute inset-0 bg-[linear-gradient(145deg,rgba(255,255,255,0.75)_0%,rgba(255,255,255,0.2)_40%,transparent_70%)]"
/>
<div className="relative">
<h2 className="mb-2 text-xl font-normal text-ink md:text-2xl">
فرم تماس
</h2>
<p className="mb-8 text-sm text-muted">
ما همیشه دنبال دوستای جدید هستیم.
</p>
<ContactForm variant="light" />
</div>
</div>
</div>
</div>
</section>
</ContentPageShell>
);
}
+112 -5
View File
@@ -117,20 +117,127 @@ h4 {
} }
/* Gradient hover utilities */ /* Gradient hover utilities */
/* Simple fade for primary CTAs */
.btn-primary { .btn-primary {
position: relative;
isolation: isolate;
background: var(--ink); background: var(--ink);
color: var(--surface); color: var(--surface);
transition: cursor: pointer;
background 0.25s ease, transition: color 0.3s ease;
color 0.25s ease,
box-shadow 0.25s ease;
} }
.btn-primary:hover { .btn-primary::before {
content: "";
position: absolute;
inset: 0;
z-index: -1;
border-radius: inherit;
background: var(--brand-gradient); background: var(--brand-gradient);
opacity: 0;
transition: opacity 0.3s ease;
}
.btn-primary:hover:not(:disabled) {
color: #fff; color: #fff;
} }
.btn-primary:hover:not(:disabled)::before {
opacity: 1;
}
.btn-primary:disabled {
cursor: not-allowed;
}
/* Outline CTA: opacity-only fades (no blink on mouse-out) */
.btn-outline {
position: relative;
display: inline-flex;
align-items: center;
justify-content: center;
isolation: isolate;
border: 1px solid transparent;
background-color: #fff;
color: var(--ink-soft);
cursor: pointer;
}
.btn-outline::before,
.btn-outline::after {
content: "";
position: absolute;
inset: 0;
z-index: 0;
border-radius: inherit;
padding: 1px;
pointer-events: none;
transition: opacity 0.3s ease;
-webkit-mask:
linear-gradient(#fff 0 0) content-box,
linear-gradient(#fff 0 0);
mask:
linear-gradient(#fff 0 0) content-box,
linear-gradient(#fff 0 0);
-webkit-mask-composite: xor;
mask-composite: exclude;
}
.btn-outline::after {
background: var(--line);
opacity: 1;
}
.btn-outline::before {
background: var(--brand-gradient);
opacity: 0;
}
.btn-outline:hover::after {
opacity: 0;
}
.btn-outline:hover::before {
opacity: 1;
}
.btn-outline-label {
position: relative;
z-index: 1;
display: inline-flex;
align-items: center;
gap: 0.375rem;
}
.btn-outline-label-base {
transition: opacity 0.3s ease;
}
.btn-outline-label-grad {
position: absolute;
inset: 0;
display: inline-flex;
align-items: center;
justify-content: inherit;
gap: 0.375rem;
background-image: var(--brand-gradient);
background-clip: text;
-webkit-background-clip: text;
color: transparent;
-webkit-text-fill-color: transparent;
opacity: 0;
pointer-events: none;
transition: opacity 0.3s ease;
}
.btn-outline:hover .btn-outline-label-base {
opacity: 0;
}
.btn-outline:hover .btn-outline-label-grad {
opacity: 1;
}
.link-hover { .link-hover {
display: inline-block; display: inline-block;
width: fit-content; width: fit-content;
+2 -2
View File
@@ -127,11 +127,11 @@ export default async function InstructionDetailPage({
<ContentPageShell flushTop> <ContentPageShell flushTop>
<article className="pb-20 lg:pb-28"> <article className="pb-20 lg:pb-28">
<div className="page-title-band mb-8 md:mb-10"> <div className="page-title-band mb-8 md:mb-10">
<div className="relative mx-auto max-w-7xl px-5 pb-4 pt-[calc(var(--header-height)+0.75rem)] lg:px-8 lg:pb-5 lg:pt-[calc(var(--header-height)+1rem)]"> <div className="relative mx-auto max-w-7xl px-5 pb-10 pt-[calc(var(--header-height)+0.15rem)] lg:px-8 lg:pb-12 lg:pt-[calc(var(--header-height)+0.3rem)]">
{titleEn ? ( {titleEn ? (
<p <p
aria-hidden aria-hidden
className="latin pointer-events-none absolute inset-x-5 top-[calc(var(--header-height)+2.4rem)] z-0 text-[clamp(1.75rem,5.5vw,3.75rem)] font-light uppercase leading-none tracking-[0.05em] text-ink/[0.07] lg:inset-x-8 lg:top-[calc(var(--header-height)+2.6rem)]" className="latin pointer-events-none absolute inset-x-5 top-[calc(var(--header-height)+1.75rem)] z-0 text-[clamp(1.75rem,5.5vw,3.75rem)] font-light uppercase leading-none tracking-[0.05em] text-ink/[0.07] lg:inset-x-8 lg:top-[calc(var(--header-height)+1.95rem)]"
> >
{titleEn} {titleEn}
</p> </p>
+2 -2
View File
@@ -99,10 +99,10 @@ export default async function InstructionListPage({
<ContentPageShell flushTop> <ContentPageShell flushTop>
<section className="pb-20 lg:pb-28"> <section className="pb-20 lg:pb-28">
<div className="page-title-band mb-8 md:mb-10"> <div className="page-title-band mb-8 md:mb-10">
<div className="relative mx-auto max-w-7xl px-5 pb-3 pt-[calc(var(--header-height)+0.75rem)] lg:px-8 lg:pb-4 lg:pt-[calc(var(--header-height)+1rem)]"> <div className="relative mx-auto max-w-7xl px-5 pb-9 pt-[calc(var(--header-height)+0.15rem)] lg:px-8 lg:pb-12 lg:pt-[calc(var(--header-height)+0.3rem)]">
<p <p
aria-hidden aria-hidden
className="latin pointer-events-none absolute inset-x-5 top-[calc(var(--header-height)+1.15rem)] z-0 text-[clamp(1.75rem,5.5vw,3.75rem)] font-light uppercase leading-none tracking-[0.05em] text-ink/[0.07] lg:inset-x-8 lg:top-[calc(var(--header-height)+1.35rem)]" className="latin pointer-events-none absolute inset-x-5 top-[calc(var(--header-height)+0.5rem)] z-0 text-[clamp(1.75rem,5.5vw,3.75rem)] font-light uppercase leading-none tracking-[0.05em] text-ink/[0.07] lg:inset-x-8 lg:top-[calc(var(--header-height)+0.65rem)]"
> >
Instructions Instructions
</p> </p>
+2 -2
View File
@@ -123,11 +123,11 @@ export default async function PortfolioDetailPage({
<ContentPageShell flushTop> <ContentPageShell flushTop>
<article className="pb-20 lg:pb-28"> <article className="pb-20 lg:pb-28">
<div className="page-title-band mb-8 md:mb-10"> <div className="page-title-band mb-8 md:mb-10">
<div className="relative mx-auto max-w-7xl px-5 pb-4 pt-[calc(var(--header-height)+0.75rem)] lg:px-8 lg:pb-5 lg:pt-[calc(var(--header-height)+1rem)]"> <div className="relative mx-auto max-w-7xl px-5 pb-10 pt-[calc(var(--header-height)+0.15rem)] lg:px-8 lg:pb-12 lg:pt-[calc(var(--header-height)+0.3rem)]">
{titleEn ? ( {titleEn ? (
<p <p
aria-hidden aria-hidden
className="latin pointer-events-none absolute inset-x-5 top-[calc(var(--header-height)+2.4rem)] z-0 text-[clamp(1.75rem,5.5vw,3.75rem)] font-light uppercase leading-none tracking-[0.05em] text-ink/[0.07] lg:inset-x-8 lg:top-[calc(var(--header-height)+2.6rem)]" className="latin pointer-events-none absolute inset-x-5 top-[calc(var(--header-height)+1.75rem)] z-0 text-[clamp(1.75rem,5.5vw,3.75rem)] font-light uppercase leading-none tracking-[0.05em] text-ink/[0.07] lg:inset-x-8 lg:top-[calc(var(--header-height)+1.95rem)]"
> >
{titleEn} {titleEn}
</p> </p>
+2 -2
View File
@@ -95,10 +95,10 @@ export default async function PortfolioListPage({
<ContentPageShell flushTop> <ContentPageShell flushTop>
<section className="pb-20 lg:pb-28"> <section className="pb-20 lg:pb-28">
<div className="page-title-band mb-8 md:mb-10"> <div className="page-title-band mb-8 md:mb-10">
<div className="relative mx-auto max-w-7xl px-5 pb-3 pt-[calc(var(--header-height)+0.75rem)] lg:px-8 lg:pb-4 lg:pt-[calc(var(--header-height)+1rem)]"> <div className="relative mx-auto max-w-7xl px-5 pb-9 pt-[calc(var(--header-height)+0.15rem)] lg:px-8 lg:pb-12 lg:pt-[calc(var(--header-height)+0.3rem)]">
<p <p
aria-hidden aria-hidden
className="latin pointer-events-none absolute inset-x-5 top-[calc(var(--header-height)+1.15rem)] z-0 text-[clamp(1.75rem,5.5vw,3.75rem)] font-light uppercase leading-none tracking-[0.05em] text-ink/[0.07] lg:inset-x-8 lg:top-[calc(var(--header-height)+1.35rem)]" className="latin pointer-events-none absolute inset-x-5 top-[calc(var(--header-height)+0.5rem)] z-0 text-[clamp(1.75rem,5.5vw,3.75rem)] font-light uppercase leading-none tracking-[0.05em] text-ink/[0.07] lg:inset-x-8 lg:top-[calc(var(--header-height)+0.65rem)]"
> >
Portfolio Portfolio
</p> </p>
+1 -1
View File
@@ -24,7 +24,7 @@ export function AppSection() {
بیشتر کاربران را به همراه دارد. بیشتر کاربران را به همراه دارد.
</p> </p>
<a <a
href="#contact" href="/contact"
className="btn-primary inline-flex rounded-full px-6 py-3.5 text-sm" className="btn-primary inline-flex rounded-full px-6 py-3.5 text-sm"
> >
با ما تماس بگیرید با ما تماس بگیرید
+108
View File
@@ -0,0 +1,108 @@
import Link from "next/link";
import type { ComponentPropsWithoutRef, ReactNode } from "react";
type ButtonVariant = "primary" | "outline";
type ButtonSize = "sm" | "md";
type CommonProps = {
variant?: ButtonVariant;
size?: ButtonSize;
className?: string;
children: ReactNode;
};
type ButtonAsLink = CommonProps &
Omit<ComponentPropsWithoutRef<typeof Link>, "className" | "children"> & {
href: string;
};
type ButtonAsAnchor = CommonProps &
Omit<ComponentPropsWithoutRef<"a">, "className" | "children" | "href"> & {
href: string;
external?: true;
};
type ButtonAsButton = CommonProps &
Omit<ComponentPropsWithoutRef<"button">, "className" | "children"> & {
href?: undefined;
};
export type ButtonProps = ButtonAsLink | ButtonAsAnchor | ButtonAsButton;
const sizeClass: Record<ButtonSize, string> = {
sm: "px-3.5 py-2 text-xs",
md: "px-6 py-3.5 text-sm",
};
function cx(...parts: Array<string | false | null | undefined>) {
return parts.filter(Boolean).join(" ");
}
function renderLabel(variant: ButtonVariant, children: ReactNode) {
if (variant !== "outline") return children;
return (
<span className="btn-outline-label">
<span className="btn-outline-label-base inline-flex items-center gap-1.5">
{children}
</span>
<span className="btn-outline-label-grad" aria-hidden>
{children}
</span>
</span>
);
}
export function Button(props: ButtonProps) {
const {
variant = "primary",
size = "md",
className,
children,
...rest
} = props;
const classes = cx(
variant === "primary" ? "btn-primary" : "btn-outline",
"inline-flex items-center justify-center gap-1.5 rounded-full font-medium",
sizeClass[size],
className,
);
const content = renderLabel(variant, children);
if ("href" in props && props.href) {
const { href, external, ...linkRest } = rest as Omit<
ButtonAsLink & ButtonAsAnchor,
keyof CommonProps
> & { external?: true };
if (external || href.startsWith("http") || href.startsWith("tel:") || href.startsWith("mailto:")) {
return (
<a
href={href}
className={classes}
{...(external
? { target: "_blank", rel: "noopener noreferrer" }
: {})}
{...(linkRest as ComponentPropsWithoutRef<"a">)}
>
{content}
</a>
);
}
return (
<Link href={href} className={classes} {...(linkRest as Omit<ComponentPropsWithoutRef<typeof Link>, "href" | "className" | "children">)}>
{content}
</Link>
);
}
const buttonRest = rest as Omit<ButtonAsButton, keyof CommonProps>;
return (
<button type={buttonRest.type ?? "button"} className={classes} {...buttonRest}>
{content}
</button>
);
}
+2 -2
View File
@@ -35,13 +35,13 @@ export function CmsSection() {
</p> </p>
<div className="flex flex-wrap gap-4"> <div className="flex flex-wrap gap-4">
<a <a
href="#contact" href="/contact"
className="btn-primary rounded-full px-6 py-3.5 text-sm" className="btn-primary rounded-full px-6 py-3.5 text-sm"
> >
با ما تماس بگیرید با ما تماس بگیرید
</a> </a>
<a <a
href="#contact" href="/contact"
className="link-hover rounded-full px-5 py-3.5 text-sm text-ink-soft" className="link-hover rounded-full px-5 py-3.5 text-sm text-ink-soft"
> >
دوست دارم بیشتر بدونم دوست دارم بیشتر بدونم
+155
View File
@@ -0,0 +1,155 @@
"use client";
import { FormEvent, useState } from "react";
type ContactFormProps = {
variant?: "light" | "dark";
};
export function ContactForm({ variant = "dark" }: ContactFormProps) {
const [error, setError] = useState("");
const [success, setSuccess] = useState("");
const [sending, setSending] = useState(false);
const isLight = variant === "light";
const labelClass = isLight ? "text-ink-soft" : "text-white/70";
const hintClass = isLight ? "text-muted" : "text-white/40";
const inputClass = isLight
? "rounded-xl border border-white/80 bg-white/55 px-4 py-3 text-ink outline-none shadow-[inset_0_1px_0_rgba(255,255,255,0.7)] backdrop-blur-sm transition placeholder:text-muted focus:border-ink/25 focus:bg-white/80"
: "rounded-xl border border-white/10 bg-white/5 px-4 py-3 text-surface outline-none transition placeholder:text-white/30 focus:border-accent";
const errorClass = isLight ? "text-sm text-red-600" : "text-sm text-red-300";
const successClass = isLight
? "text-sm text-emerald-700"
: "text-sm text-emerald-300";
async function submit(event: FormEvent<HTMLFormElement>) {
event.preventDefault();
const formElement = event.currentTarget;
const form = new FormData(formElement);
const name = String(form.get("name") ?? "").trim();
const phone = String(form.get("phone") ?? "").trim();
const email = String(form.get("email") ?? "").trim();
const subject = String(form.get("subject") ?? "").trim();
const message = String(form.get("message") ?? "").trim();
if (!name || !phone || !subject || !message) {
setError("لطفا نام، شماره تماس، موضوع و متن پیام را کامل کنید.");
setSuccess("");
return;
}
setError("");
setSuccess("");
setSending(true);
try {
const response = await fetch("/api/contact", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
title: subject,
name,
cellNumber: phone,
email: email || undefined,
text: message,
}),
});
if (!response.ok) {
throw new Error("Contact submission failed");
}
formElement.reset();
setSuccess(
"پیام شما با موفقیت ثبت شد. همکاران ما به‌زودی با شما تماس می‌گیرند.",
);
} catch {
setError("ارسال پیام انجام نشد. لطفا دوباره تلاش کنید.");
} finally {
setSending(false);
}
}
return (
<form className="grid gap-4" onSubmit={submit} noValidate>
<label className="grid gap-2 text-sm">
<span className={labelClass}>نام</span>
<input
name="name"
type="text"
placeholder="نام شما"
autoComplete="name"
required
className={inputClass}
/>
</label>
<label className="grid gap-2 text-sm">
<span className={labelClass}>شماره تماس</span>
<input
name="phone"
type="tel"
placeholder="۰۹۱۲…"
inputMode="tel"
autoComplete="tel"
required
className={inputClass}
dir="ltr"
/>
</label>
<label className="grid gap-2 text-sm">
<span className={labelClass}>
ایمیل <span className={hintClass}>(اختیاری)</span>
</span>
<input
name="email"
type="email"
placeholder="you@example.com"
autoComplete="email"
className={inputClass}
dir="ltr"
/>
</label>
<label className="grid gap-2 text-sm">
<span className={labelClass}>موضوع</span>
<input
name="subject"
type="text"
placeholder="موضوع پیام"
required
className={inputClass}
/>
</label>
<label className="grid gap-2 text-sm">
<span className={labelClass}>پیام</span>
<textarea
name="message"
rows={4}
placeholder="چطور می‌تونیم کمکتون کنیم؟"
required
className={`resize-none ${inputClass}`}
/>
</label>
{error ? (
<p className={errorClass} role="alert">
{error}
</p>
) : null}
{success ? (
<p className={successClass} role="status">
{success}
</p>
) : null}
<button
type="submit"
disabled={sending}
className="btn-primary mt-2 w-full cursor-pointer rounded-full px-6 py-3.5 text-sm font-medium disabled:cursor-not-allowed disabled:opacity-60"
>
{sending ? "در حال ارسال…" : "منتظر تماس هستم"}
</button>
</form>
);
}
+11 -39
View File
@@ -1,6 +1,5 @@
"use client";
import { site } from "@/data/home"; import { site } from "@/data/home";
import { ContactForm } from "@/components/ContactForm";
export function ContactSection() { export function ContactSection() {
return ( return (
@@ -17,6 +16,14 @@ export function ContactSection() {
> >
{site.phone} {site.phone}
</a> </a>
<p className="mb-2 text-sm text-white/55">ایمیل</p>
<a
href={`mailto:${site.email}`}
className="latin link-hover mb-8 block text-sm text-white/80 md:text-base"
dir="ltr"
>
{site.email}
</a>
<p className="mb-2 text-sm text-white/55">آدرس</p> <p className="mb-2 text-sm text-white/55">آدرس</p>
<p className="max-w-sm text-sm leading-7 text-white/80 md:text-base md:leading-8"> <p className="max-w-sm text-sm leading-7 text-white/80 md:text-base md:leading-8">
{site.address} {site.address}
@@ -28,45 +35,10 @@ export function ContactSection() {
تماس با ما تماس با ما
</h2> </h2>
<p className="mb-8 text-sm text-white/60"> <p className="mb-8 text-sm text-white/60">
ما همیشه دنبال دوستای جدید هستیم — یا ما خدمتتون تماس بگیریم. ما همیشه دنبال دوستای جدید هستیم.
</p> </p>
<form className="grid gap-4" onSubmit={(e) => e.preventDefault()}> <ContactForm />
<label className="grid gap-2 text-sm">
<span className="text-white/70">نام</span>
<input
name="name"
type="text"
placeholder="نام شما"
className="rounded-xl border border-white/10 bg-white/5 px-4 py-3 text-surface outline-none transition placeholder:text-white/30 focus:border-accent"
/>
</label>
<label className="grid gap-2 text-sm">
<span className="text-white/70">شماره تماس</span>
<input
name="phone"
type="tel"
placeholder="۰۹۱۲…"
className="rounded-xl border border-white/10 bg-white/5 px-4 py-3 text-surface outline-none transition placeholder:text-white/30 focus:border-accent"
dir="ltr"
/>
</label>
<label className="grid gap-2 text-sm">
<span className="text-white/70">پیام</span>
<textarea
name="message"
rows={4}
placeholder="چطور می‌تونیم کمکتون کنیم؟"
className="resize-none rounded-xl border border-white/10 bg-white/5 px-4 py-3 text-surface outline-none transition placeholder:text-white/30 focus:border-accent"
/>
</label>
<button
type="submit"
className="btn-primary mt-2 rounded-full px-6 py-3.5 text-sm font-medium"
>
منتظر تماس هستم
</button>
</form>
</div> </div>
</div> </div>
</section> </section>
+14 -6
View File
@@ -101,12 +101,20 @@ export function Footer() {
<p className="mt-3 max-w-[16rem] text-sm leading-7 text-muted"> <p className="mt-3 max-w-[16rem] text-sm leading-7 text-muted">
{site.address} {site.address}
</p> </p>
<Link <div className="mt-5 flex flex-wrap gap-2">
href="#contact" <Link
className="mt-5 inline-flex rounded-full border border-line px-4 py-2 text-xs text-ink-soft transition hover:border-ink/30 hover:text-ink" href="/about"
> className="inline-flex rounded-full border border-line px-4 py-2 text-xs text-ink-soft transition hover:border-ink/30 hover:text-ink"
تماس با ما >
</Link> درباره ما
</Link>
<Link
href="/contact"
className="inline-flex rounded-full border border-line px-4 py-2 text-xs text-ink-soft transition hover:border-ink/30 hover:text-ink"
>
تماس با ما
</Link>
</div>
</div> </div>
</div> </div>
+39 -34
View File
@@ -2,17 +2,10 @@
import Link from "next/link"; import Link from "next/link";
import { useState } from "react"; import { useState } from "react";
import { nav, site, type NavItem } from "@/data/home"; import { Button } from "@/components/Button";
import { nav, site, socials, type NavItem } from "@/data/home";
import { images } from "@/data/assets"; import { images } from "@/data/assets";
const socials = [
{ label: "اینستاگرام", href: "https://instagram.com", icon: "M12 2.163c3.204 0 3.584.012 4.85.07 3.252.148 4.771 1.691 4.919 4.919.058 1.265.069 1.645.069 4.849 0 3.205-.012 3.584-.069 4.849-.149 3.225-1.664 4.771-4.919 4.919-1.266.058-1.644.07-4.85.07-3.204 0-3.584-.012-4.849-.07-3.26-.149-4.771-1.699-4.919-4.92-.058-1.265-.07-1.644-.07-4.849 0-3.204.013-3.583.07-4.849.149-3.227 1.664-4.771 4.919-4.919 1.266-.057 1.645-.069 4.849-.069zM12 0C8.741 0 8.333.014 7.053.072 2.695.272.273 2.69.073 7.052.014 8.333 0 8.741 0 12c0 3.259.014 3.668.072 4.948.2 4.358 2.618 6.78 6.98 6.98C8.333 23.986 8.741 24 12 24c3.259 0 3.668-.014 4.948-.072 4.354-.2 6.782-2.618 6.979-6.98.059-1.28.073-1.689.073-4.948 0-3.259-.014-3.667-.072-4.947-.196-4.354-2.617-6.78-6.979-6.98C15.668.014 15.259 0 12 0zm0 5.838a6.162 6.162 0 100 12.324 6.162 6.162 0 000-12.324zM12 16a4 4 0 110-8 4 4 0 010 8zm6.406-11.845a1.44 1.44 0 100 2.881 1.44 1.44 0 000-2.881z" },
{ label: "یوتیوب", href: "https://youtube.com", icon: "M23.498 6.186a3.016 3.016 0 00-2.122-2.136C19.505 3.545 12 3.545 12 3.545s-7.505 0-9.377.505A3.017 3.017 0 00.502 6.186C0 8.07 0 12 0 12s0 3.93.502 5.814a3.016 3.016 0 002.122 2.136c1.871.505 9.376.505 9.376.505s7.505 0 9.377-.505a3.015 3.015 0 002.122-2.136C24 15.93 24 12 24 12s0-3.93-.502-5.814zM9.545 15.568V8.432L15.818 12l-6.273 3.568z" },
{ label: "لینکدین", href: "https://linkedin.com", icon: "M20.447 20.452h-3.554v-5.569c0-1.328-.027-3.037-1.852-3.037-1.853 0-2.136 1.445-2.136 2.939v5.667H9.351V9h3.414v1.561h.046c.477-.9 1.637-1.85 3.37-1.85 3.601 0 4.267 2.37 4.267 5.455v6.286zM5.337 7.433a2.062 2.062 0 01-2.063-2.065 2.064 2.064 0 112.063 2.065zm1.782 13.019H3.555V9h3.564v11.452zM22.225 0H1.771C.792 0 0 .774 0 1.729v20.542C0 23.227.792 24 1.771 24h20.451C23.2 24 24 23.227 24 22.271V1.729C24 .774 23.2 0 22.222 0h.003z" },
{ label: "فیسبوک", href: "https://facebook.com", icon: "M24 12.073c0-6.627-5.373-12-12-12s-12 5.373-12 12c0 5.99 4.388 10.954 10.125 11.854v-8.385H7.078v-3.47h3.047V9.43c0-3.007 1.792-4.669 4.533-4.669 1.312 0 2.686.235 2.686.235v2.953H15.83c-1.491 0-1.956.925-1.956 1.874v2.25h3.328l-.532 3.47h-2.796v8.385C19.612 23.027 24 18.062 24 12.073z" },
{ label: "آپارات", href: "https://aparat.com", icon: "M12 0C5.373 0 0 5.373 0 12s5.373 12 12 12 12-5.373 12-12S18.627 0 12 0zm0 4a8 8 0 110 16A8 8 0 0112 4zm0 2a6 6 0 100 12A6 6 0 0012 6zm-1 3l5 3-5 3V9z" },
];
function DesktopNavItem({ item }: { item: NavItem }) { function DesktopNavItem({ item }: { item: NavItem }) {
if (!item.children?.length) { if (!item.children?.length) {
return ( return (
@@ -101,7 +94,7 @@ export function Header() {
<Link href="/about" className="link-hover text-xs text-muted"> <Link href="/about" className="link-hover text-xs text-muted">
درباره ما درباره ما
</Link> </Link>
<Link href="#contact" className="link-hover text-xs text-muted"> <Link href="/contact" className="link-hover text-xs text-muted">
تماس با ما تماس با ما
</Link> </Link>
<a <a
@@ -139,22 +132,23 @@ export function Header() {
{/* Login + CTA on the left */} {/* Login + CTA on the left */}
<div className="hidden items-center gap-2 md:flex"> <div className="hidden items-center gap-2 md:flex">
<a <Button href="/login" variant="outline" size="sm">
href="/login" <svg
className="flex items-center gap-1.5 rounded-full border border-line px-3.5 py-2 text-xs text-ink-soft transition hover:border-ink hover:text-ink" viewBox="0 0 24 24"
> fill="none"
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.8" className="h-3.5 w-3.5" aria-hidden> stroke="currentColor"
<circle cx="12" cy="8" r="4"/> strokeWidth="1.8"
<path d="M4 20c0-4 3.582-7 8-7s8 3 8 7"/> className="h-3.5 w-3.5"
aria-hidden
>
<circle cx="12" cy="8" r="4" />
<path d="M4 20c0-4 3.582-7 8-7s8 3 8 7" />
</svg> </svg>
ورود ورود
</a> </Button>
<a <Button href="/contact" size="sm" className="px-4">
href="#contact"
className="btn-primary rounded-full px-4 py-2 text-sm"
>
تماس با ما تماس با ما
</a> </Button>
</div> </div>
<button <button
@@ -238,21 +232,32 @@ export function Header() {
)} )}
</div> </div>
))} ))}
<div className="mt-3 flex gap-2"> <div className="mt-3 flex flex-col gap-2">
<a <div className="flex gap-2">
<Button
href="/about"
variant="outline"
className="flex-1 py-3"
onClick={() => setOpen(false)}
>
درباره ما
</Button>
<Button
href="/contact"
className="flex-1 py-3"
onClick={() => setOpen(false)}
>
تماس با ما
</Button>
</div>
<Button
href="/login" href="/login"
className="flex flex-1 items-center justify-center gap-1.5 rounded-full border border-line py-3 text-sm text-ink-soft" variant="outline"
className="w-full py-3"
onClick={() => setOpen(false)} onClick={() => setOpen(false)}
> >
ورود ورود
</a> </Button>
<a
href="#contact"
className="btn-primary flex-1 rounded-full py-3 text-center text-sm"
onClick={() => setOpen(false)}
>
تماس با ما
</a>
</div> </div>
</nav> </nav>
</div> </div>
+31
View File
@@ -18,8 +18,39 @@ export const site = {
phoneHref: "tel:+989024200240", phoneHref: "tel:+989024200240",
email: "info@meshkee.com", email: "info@meshkee.com",
address: "قم، نبش کریمی ۳۴، ساختمان ظفر، طبقهٔ چهارم", address: "قم، نبش کریمی ۳۴، ساختمان ظفر، طبقهٔ چهارم",
mapUrl: "https://maps.app.goo.gl/GdDdMfMFzjobcggv5",
mapLat: 34.598146,
mapLng: 50.87917,
}; };
export const socials = [
{
label: "اینستاگرام",
href: "https://instagram.com",
icon: "M12 2.163c3.204 0 3.584.012 4.85.07 3.252.148 4.771 1.691 4.919 4.919.058 1.265.069 1.645.069 4.849 0 3.205-.012 3.584-.069 4.849-.149 3.225-1.664 4.771-4.919 4.919-1.266.058-1.644.07-4.85.07-3.204 0-3.584-.012-4.849-.07-3.26-.149-4.771-1.699-4.919-4.92-.058-1.265-.07-1.644-.07-4.849 0-3.204.013-3.583.07-4.849.149-3.227 1.664-4.771 4.919-4.919 1.266-.057 1.645-.069 4.849-.069zM12 0C8.741 0 8.333.014 7.053.072 2.695.272.273 2.69.073 7.052.014 8.333 0 8.741 0 12c0 3.259.014 3.668.072 4.948.2 4.358 2.618 6.78 6.98 6.98C8.333 23.986 8.741 24 12 24c3.259 0 3.668-.014 4.948-.072 4.354-.2 6.782-2.618 6.979-6.98.059-1.28.073-1.689.073-4.948 0-3.259-.014-3.667-.072-4.947-.196-4.354-2.617-6.78-6.979-6.98C15.668.014 15.259 0 12 0zm0 5.838a6.162 6.162 0 100 12.324 6.162 6.162 0 000-12.324zM12 16a4 4 0 110-8 4 4 0 010 8zm6.406-11.845a1.44 1.44 0 100 2.881 1.44 1.44 0 000-2.881z",
},
{
label: "یوتیوب",
href: "https://youtube.com",
icon: "M23.498 6.186a3.016 3.016 0 00-2.122-2.136C19.505 3.545 12 3.545 12 3.545s-7.505 0-9.377.505A3.017 3.017 0 00.502 6.186C0 8.07 0 12 0 12s0 3.93.502 5.814a3.016 3.016 0 002.122 2.136c1.871.505 9.376.505 9.376.505s7.505 0 9.377-.505a3.015 3.015 0 002.122-2.136C24 15.93 24 12 24 12s0-3.93-.502-5.814zM9.545 15.568V8.432L15.818 12l-6.273 3.568z",
},
{
label: "لینکدین",
href: "https://linkedin.com",
icon: "M20.447 20.452h-3.554v-5.569c0-1.328-.027-3.037-1.852-3.037-1.853 0-2.136 1.445-2.136 2.939v5.667H9.351V9h3.414v1.561h.046c.477-.9 1.637-1.85 3.37-1.85 3.601 0 4.267 2.37 4.267 5.455v6.286zM5.337 7.433a2.062 2.062 0 01-2.063-2.065 2.064 2.064 0 112.063 2.065zm1.782 13.019H3.555V9h3.564v11.452zM22.225 0H1.771C.792 0 0 .774 0 1.729v20.542C0 23.227.792 24 1.771 24h20.451C23.2 24 24 23.227 24 22.271V1.729C24 .774 23.2 0 22.222 0h.003z",
},
{
label: "فیسبوک",
href: "https://facebook.com",
icon: "M24 12.073c0-6.627-5.373-12-12-12s-12 5.373-12 12c0 5.99 4.388 10.954 10.125 11.854v-8.385H7.078v-3.47h3.047V9.43c0-3.007 1.792-4.669 4.533-4.669 1.312 0 2.686.235 2.686.235v2.953H15.83c-1.491 0-1.956.925-1.956 1.874v2.25h3.328l-.532 3.47h-2.796v8.385C19.612 23.027 24 18.062 24 12.073z",
},
{
label: "آپارات",
href: "https://aparat.com",
icon: "M12 0C5.373 0 0 5.373 0 12s5.373 12 12 12 12-5.373 12-12S18.627 0 12 0zm0 4a8 8 0 110 16A8 8 0 0112 4zm0 2a6 6 0 100 12A6 6 0 0012 6zm-1 3l5 3-5 3V9z",
},
];
export type NavItem = { export type NavItem = {
label: string; label: string;
href: string; href: string;