phase3-4 done
Some checks failed
Deploy to VPS / deploy (push) Has been cancelled

This commit is contained in:
Ali Taghavi
2026-04-24 11:28:48 +03:30
parent 95a546df11
commit f08d3b1fde
24 changed files with 1500 additions and 16 deletions

View File

@@ -2,6 +2,10 @@
.github
node_modules
.next
.turbo
.env*.local
.DS_Store
README.md
*.md
.vscode
.idea

View File

@@ -1,15 +1,33 @@
/** @type {import('next-sitemap').IConfig} */
module.exports = {
siteUrl: "https://biztaghavi.com",
siteUrl: process.env.NEXT_PUBLIC_SITE_URL || "https://biztaghavi.com",
generateRobotsTxt: true,
generateIndexSitemap: false,
changefreq: "weekly",
priority: 0.7,
exclude: ["/studio/*", "/api/*"],
exclude: ["/studio/*", "/api/*", "/en/studio/*"],
alternateRefs: [
{ href: "https://biztaghavi.com", hreflang: "fa" },
{ href: "https://biztaghavi.com/en", hreflang: "en" },
{ href: "https://biztaghavi.com", hreflang: "x-default" },
],
robotsTxtOptions: {
policies: [
{ userAgent: "*", allow: "/" },
{ userAgent: "*", disallow: ["/studio", "/api"] },
],
additionalSitemaps: [],
},
transform: async (config, path) => {
// Boost priority for key pages
const highPriority = ["/", "/en", "/about", "/en/about", "/writing", "/en/writing"];
const priority = highPriority.includes(path) ? 1.0 : config.priority;
return {
loc: path,
changefreq: config.changefreq,
priority,
lastmod: config.autoLastmod ? new Date().toISOString() : undefined,
alternateRefs: config.alternateRefs ?? [],
};
},
};

View File

@@ -9,7 +9,30 @@ const nextConfig: NextConfig = {
remotePatterns: [
{ protocol: "https", hostname: "cdn.sanity.io" },
],
formats: ["image/avif", "image/webp"],
deviceSizes: [640, 750, 828, 1080, 1200, 1920],
minimumCacheTTL: 60 * 60 * 24 * 30, // 30 days
},
experimental: {
optimizePackageImports: ["lucide-react", "framer-motion", "@sanity/image-url"],
},
compress: true,
poweredByHeader: false,
headers: async () => [
{
source: "/(.*)",
headers: [
{ key: "X-DNS-Prefetch-Control", value: "on" },
{ key: "X-Content-Type-Options", value: "nosniff" },
{ key: "X-Frame-Options", value: "SAMEORIGIN" },
{ key: "Referrer-Policy", value: "strict-origin-when-cross-origin" },
],
},
{
source: "/_next/static/(.*)",
headers: [{ key: "Cache-Control", value: "public, max-age=31536000, immutable" }],
},
],
};
export default withNextIntl(nextConfig);

View File

@@ -28,7 +28,10 @@
"@sanity/image-url": "^1.1.0",
"@portabletext/react": "^3.1.0",
"sugar-high": "^0.9.0",
"next-sitemap": "^4.2.3"
"next-sitemap": "^4.2.3",
"resend": "^4.0.0",
"@radix-ui/react-accordion": "^1.2.0",
"@radix-ui/react-tabs": "^1.1.0"
},
"pnpm": {
"onlyBuiltDependencies": ["@parcel/watcher", "@swc/core"]

226
pnpm-lock.yaml generated
View File

@@ -14,6 +14,9 @@ importers:
'@portabletext/react':
specifier: ^3.1.0
version: 3.2.4(react@19.2.4)
'@radix-ui/react-accordion':
specifier: ^1.2.0
version: 1.2.12(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
'@radix-ui/react-dialog':
specifier: ^1.1.0
version: 1.1.15(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
@@ -26,6 +29,9 @@ importers:
'@radix-ui/react-slot':
specifier: ^1.2.0
version: 1.2.4(@types/react@19.2.14)(react@19.2.4)
'@radix-ui/react-tabs':
specifier: ^1.1.0
version: 1.1.13(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
'@sanity/image-url':
specifier: ^1.1.0
version: 1.2.0
@@ -59,6 +65,9 @@ importers:
react-dom:
specifier: 19.2.4
version: 19.2.4(react@19.2.4)
resend:
specifier: ^4.0.0
version: 4.8.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
sanity:
specifier: ^3.0.0
version: 3.99.0(@emotion/is-prop-valid@1.4.0)(@types/node@20.19.39)(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(immer@10.2.0)(jiti@2.6.1)(lightningcss@1.32.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(styled-components@6.4.1(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(typescript@5.9.3)(yaml@2.8.3)
@@ -1717,6 +1726,32 @@ packages:
'@radix-ui/primitive@1.1.3':
resolution: {integrity: sha512-JTF99U/6XIjCBo0wqkU5sK10glYe27MRRsfwoiq5zzOEZLHU3A3KCMa5X/azekYRCJ0HlwI0crAXS/5dEHTzDg==}
'@radix-ui/react-accordion@1.2.12':
resolution: {integrity: sha512-T4nygeh9YE9dLRPhAHSeOZi7HBXo+0kYIPJXayZfvWOWA0+n3dESrZbjfDPUABkUNym6Hd+f2IR113To8D2GPA==}
peerDependencies:
'@types/react': '*'
'@types/react-dom': '*'
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
peerDependenciesMeta:
'@types/react':
optional: true
'@types/react-dom':
optional: true
'@radix-ui/react-collapsible@1.1.12':
resolution: {integrity: sha512-Uu+mSh4agx2ib1uIGPP4/CKNULyajb3p92LsVXmH2EHVMTfZWpll88XJ0j4W0z3f8NK1eYl1+Mf/szHPmcHzyA==}
peerDependencies:
'@types/react': '*'
'@types/react-dom': '*'
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
peerDependenciesMeta:
'@types/react':
optional: true
'@types/react-dom':
optional: true
'@radix-ui/react-collection@1.1.7':
resolution: {integrity: sha512-Fh9rGN0MoI4ZFUNyfFVNU4y9LUz93u9/0K+yLgA2bwRojxM8JU1DyvvMBabnZPBgMWREAJvU2jjVzq+LrFUglw==}
peerDependencies:
@@ -1879,6 +1914,19 @@ packages:
'@types/react-dom':
optional: true
'@radix-ui/react-roving-focus@1.1.11':
resolution: {integrity: sha512-7A6S9jSgm/S+7MdtNDSb+IU859vQqJ/QAtcYQcfFC6W8RS4IxIZDldLR0xqCFZ6DCyrQLjLPsxtTNch5jVA4lA==}
peerDependencies:
'@types/react': '*'
'@types/react-dom': '*'
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
peerDependenciesMeta:
'@types/react':
optional: true
'@types/react-dom':
optional: true
'@radix-ui/react-separator@1.1.8':
resolution: {integrity: sha512-sDvqVY4itsKwwSMEe0jtKgfTh+72Sy3gPmQpjqcQneqQ4PFmr/1I0YA+2/puilhggCe2gJcx5EBAYFkWkdpa5g==}
peerDependencies:
@@ -1910,6 +1958,19 @@ packages:
'@types/react':
optional: true
'@radix-ui/react-tabs@1.1.13':
resolution: {integrity: sha512-7xdcatg7/U+7+Udyoj2zodtI9H/IIopqo+YOIcZOq1nJwXWBZ9p8xiu5llXlekDbZkca79a/fozEYQXIA4sW6A==}
peerDependencies:
'@types/react': '*'
'@types/react-dom': '*'
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
peerDependenciesMeta:
'@types/react':
optional: true
'@types/react-dom':
optional: true
'@radix-ui/react-use-callback-ref@1.1.1':
resolution: {integrity: sha512-FkBMwD+qbGQeMu1cOHnuGB6x4yzPjho8ap5WtbEJ26umhgqVXbhekKUQO+hZEL1vU92a3wHwdp0HAcqAUF5iDg==}
peerDependencies:
@@ -1977,6 +2038,13 @@ packages:
'@types/react-dom':
optional: true
'@react-email/render@1.1.2':
resolution: {integrity: sha512-RnRehYN3v9gVlNMehHPHhyp2RQo7+pSkHDtXPvg3s0GbzM9SQMW4Qrf8GRNvtpLC4gsI+Wt0VatNRUFqjvevbw==}
engines: {node: '>=18.0.0'}
peerDependencies:
react: ^18.0 || ^19.0 || ^19.0.0-rc
react-dom: ^18.0 || ^19.0 || ^19.0.0-rc
'@rexxars/react-json-inspector@9.0.1':
resolution: {integrity: sha512-4uZ4RnrVoOGOShIKKcPoF+qhwDCZJsPPqyoEoW/8HRdzNknN9Q2yhlbEgTX1lMZunF1fv7iHzAs+n1vgIgfg/g==}
peerDependencies:
@@ -2379,6 +2447,9 @@ packages:
'@schummar/icu-type-parser@1.21.5':
resolution: {integrity: sha512-bXHSaW5jRTmke9Vd0h5P7BtWZG9Znqb8gSDxZnxaGSJnGwPLDPfS+3g0BKzeWqzgZPsIVZkM7m2tbo18cm5HBw==}
'@selderee/plugin-htmlparser2@0.11.0':
resolution: {integrity: sha512-P33hHGdldxGabLFjPPpaTxVolMrzrcegejx+0GxjrIb9Zv48D8yAIA/QTDR2dFl7Uz7urX8aX6+5bCZslr+gWQ==}
'@sentry-internal/browser-utils@8.55.1':
resolution: {integrity: sha512-SipXiwVhJrxzy3/4kf+YIFmpYlLKtGSRD+er7SBCcuSBtv31Fee8IXMDvk+bq24gRXxyjOLUmT//GGXjy2LL6w==}
engines: {node: '>=14.18'}
@@ -3524,6 +3595,10 @@ packages:
deep-is@0.1.4:
resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==}
deepmerge@4.3.1:
resolution: {integrity: sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==}
engines: {node: '>=0.10.0'}
defaults@1.0.4:
resolution: {integrity: sha512-eFuaLoy/Rxalv2kr+lqMlUnrDWV+3j4pljOIJgLIhI058IQfWJ7vXhyEIHu+HtC738klGALYxOKDO0bQP3tg8A==}
@@ -3863,6 +3938,9 @@ packages:
fast-content-type-parse@3.0.0:
resolution: {integrity: sha512-ZvLdcY8P+N8mGQJahJV5G4U88CSvT1rP8ApL6uETe88MBXrBHAkZlSEySdUlyztF7ccb+Znos3TFqaepHxdhBg==}
fast-deep-equal@2.0.1:
resolution: {integrity: sha512-bCK/2Z4zLidyB4ReuIsvALH6w31YfAQDmXMqMx6FyfHqvBxtjC0eRumeSu4Bs3XtXwpyIywtSTrVT99BxY1f9w==}
fast-deep-equal@3.1.3:
resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==}
@@ -4221,6 +4299,13 @@ packages:
html-parse-stringify@3.0.1:
resolution: {integrity: sha512-KknJ50kTInJ7qIScF3jeaFRpMpE8/lfiTdzf/twXyPBLAGrLRTmkz3AdTnKeh40X8k9L2fdYwEp/42WGXIRGcg==}
html-to-text@9.0.5:
resolution: {integrity: sha512-qY60FjREgVZL03vJU6IfMV4GDjGBIoOyvuFdpBDIX9yTlDw0TjxVBQp+P8NvpdIXNJvfWBTNul7fsAQJq2FNpg==}
engines: {node: '>=14'}
htmlparser2@8.0.2:
resolution: {integrity: sha512-GYdjWKDkbRLkZ5geuHs5NY1puJ+PXwP7+fHPRz06Eirsb9ugf6d8kkXav6ADhcODhFFPMIXyxkxSuMf3D6NCFA==}
http-proxy-agent@7.0.2:
resolution: {integrity: sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==}
engines: {node: '>= 14'}
@@ -4638,6 +4723,9 @@ packages:
resolution: {integrity: sha512-b94GiNHQNy6JNTrt5w6zNyffMrNkXZb3KTkCZJb2V1xaEGCk093vkZ2jk3tpaeP33/OiXC+WvK9AxUebnf5nbw==}
engines: {node: '>= 0.6.3'}
leac@0.6.0:
resolution: {integrity: sha512-y+SqErxb8h7nE/fiEX07jsbuhrpO9lL8eca7/Y1nuWV2moNlXhyd59iDGcRf6moVyDMbmTNzL40SUyrFU/yDpg==}
leven@3.1.0:
resolution: {integrity: sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==}
engines: {node: '>=6'}
@@ -5236,6 +5324,9 @@ packages:
parse5@8.0.1:
resolution: {integrity: sha512-z1e/HMG90obSGeidlli3hj7cbocou0/wa5HacvI3ASx34PecNjNQeaHNo5WIZpWofN9kgkqV1q5YvXe3F0FoPw==}
parseley@0.12.1:
resolution: {integrity: sha512-e6qHKe3a9HWr0oMRVDTRhKce+bRO8VGQR3NyVwcjwrbhMmFCX9KszEV35+rn4AdilFAq9VPxP/Fe1wC9Qjd2lw==}
path-exists@3.0.0:
resolution: {integrity: sha512-bpC7GYwiDYQ4wYLe+FA8lhRjhQCMcQGuSgGGqDkg/QerRWw9CmGRT0iSOVRSZJ29NMLZgIzqaljJ63oaL4NIJQ==}
engines: {node: '>=4'}
@@ -5273,6 +5364,9 @@ packages:
resolution: {integrity: sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==}
engines: {node: '>=8'}
peberminta@0.9.0:
resolution: {integrity: sha512-XIxfHpEuSJbITd1H3EeQwpcZbTLHc+VVr8ANI9t5sit565tsI4/xK3KWTUFE2e6QiangUkh3B0jihzmGnNrRsQ==}
peek-stream@1.1.3:
resolution: {integrity: sha512-FhJ+YbOSBb9/rIl2ZeE/QHEsWn7PqNYt8ARAY3kIgNGOk13g9FGyIY6JIl/xB/3TFRVoTv5as0l11weORrTekA==}
@@ -5476,6 +5570,9 @@ packages:
react-is@18.3.1:
resolution: {integrity: sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==}
react-promise-suspense@0.3.4:
resolution: {integrity: sha512-I42jl7L3Ze6kZaq+7zXWSunBa3b1on5yfvUW6Eo/3fFOj6dZ5Bqmcd264nJbTK/gn1HjjILAjSwnZbV4RpSaNQ==}
react-refractor@2.2.0:
resolution: {integrity: sha512-UvWkBVqH/2b9nkkkt4UNFtU3aY1orQfd4plPjx5rxbefy6vGajNHU9n+tv8CbykFyVirr3vEBfN2JTxyK0d36g==}
peerDependencies:
@@ -5604,6 +5701,10 @@ packages:
reselect@5.1.1:
resolution: {integrity: sha512-K/BG6eIky/SBpzfHZv/dd+9JBFiS4SWV7FIujVyJRux6e45+73RaUHXLmIR1f7WOMaQ0U1km6qwklRQxpJJY0w==}
resend@4.8.0:
resolution: {integrity: sha512-R8eBOFQDO6dzRTDmaMEdpqrkmgSjPpVXt4nGfWsZdYOet0kqra0xgbvTES6HmCriZEXbmGk3e0DiGIaLFTFSHA==}
engines: {node: '>=18'}
resolve-from@4.0.0:
resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==}
engines: {node: '>=4'}
@@ -5735,6 +5836,9 @@ packages:
resolution: {integrity: sha512-e1QtP3YL5tWww8uKaOCQ18UxIT2laNBXHjV/S2WYCiK4udiv8lkG89KRIoCjUagnAmCBurjF4zEVX2ByBbnCjQ==}
hasBin: true
selderee@0.11.0:
resolution: {integrity: sha512-5TF+l7p4+OsnP8BCCvSyZiSPc4x4//p5uPwK8TCnVPJYRmU2aYKMpOXvw8zM5a5JvuuCGN1jmsMwuU2W02ukfA==}
semver@5.7.2:
resolution: {integrity: sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==}
hasBin: true
@@ -8319,6 +8423,39 @@ snapshots:
'@radix-ui/primitive@1.1.3': {}
'@radix-ui/react-accordion@1.2.12(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)':
dependencies:
'@radix-ui/primitive': 1.1.3
'@radix-ui/react-collapsible': 1.1.12(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
'@radix-ui/react-collection': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
'@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.2.4)
'@radix-ui/react-context': 1.1.2(@types/react@19.2.14)(react@19.2.4)
'@radix-ui/react-direction': 1.1.1(@types/react@19.2.14)(react@19.2.4)
'@radix-ui/react-id': 1.1.1(@types/react@19.2.14)(react@19.2.4)
'@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
'@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.14)(react@19.2.4)
react: 19.2.4
react-dom: 19.2.4(react@19.2.4)
optionalDependencies:
'@types/react': 19.2.14
'@types/react-dom': 19.2.3(@types/react@19.2.14)
'@radix-ui/react-collapsible@1.1.12(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)':
dependencies:
'@radix-ui/primitive': 1.1.3
'@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.2.4)
'@radix-ui/react-context': 1.1.2(@types/react@19.2.14)(react@19.2.4)
'@radix-ui/react-id': 1.1.1(@types/react@19.2.14)(react@19.2.4)
'@radix-ui/react-presence': 1.1.5(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
'@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
'@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.14)(react@19.2.4)
'@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.14)(react@19.2.4)
react: 19.2.4
react-dom: 19.2.4(react@19.2.4)
optionalDependencies:
'@types/react': 19.2.14
'@types/react-dom': 19.2.3(@types/react@19.2.14)
'@radix-ui/react-collection@1.1.7(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)':
dependencies:
'@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.2.4)
@@ -8468,6 +8605,23 @@ snapshots:
'@types/react': 19.2.14
'@types/react-dom': 19.2.3(@types/react@19.2.14)
'@radix-ui/react-roving-focus@1.1.11(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)':
dependencies:
'@radix-ui/primitive': 1.1.3
'@radix-ui/react-collection': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
'@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.2.4)
'@radix-ui/react-context': 1.1.2(@types/react@19.2.14)(react@19.2.4)
'@radix-ui/react-direction': 1.1.1(@types/react@19.2.14)(react@19.2.4)
'@radix-ui/react-id': 1.1.1(@types/react@19.2.14)(react@19.2.4)
'@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
'@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.2.14)(react@19.2.4)
'@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.14)(react@19.2.4)
react: 19.2.4
react-dom: 19.2.4(react@19.2.4)
optionalDependencies:
'@types/react': 19.2.14
'@types/react-dom': 19.2.3(@types/react@19.2.14)
'@radix-ui/react-separator@1.1.8(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)':
dependencies:
'@radix-ui/react-primitive': 2.1.4(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
@@ -8491,6 +8645,22 @@ snapshots:
optionalDependencies:
'@types/react': 19.2.14
'@radix-ui/react-tabs@1.1.13(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)':
dependencies:
'@radix-ui/primitive': 1.1.3
'@radix-ui/react-context': 1.1.2(@types/react@19.2.14)(react@19.2.4)
'@radix-ui/react-direction': 1.1.1(@types/react@19.2.14)(react@19.2.4)
'@radix-ui/react-id': 1.1.1(@types/react@19.2.14)(react@19.2.4)
'@radix-ui/react-presence': 1.1.5(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
'@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
'@radix-ui/react-roving-focus': 1.1.11(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
'@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.14)(react@19.2.4)
react: 19.2.4
react-dom: 19.2.4(react@19.2.4)
optionalDependencies:
'@types/react': 19.2.14
'@types/react-dom': 19.2.3(@types/react@19.2.14)
'@radix-ui/react-use-callback-ref@1.1.1(@types/react@19.2.14)(react@19.2.4)':
dependencies:
react: 19.2.4
@@ -8540,6 +8710,14 @@ snapshots:
'@types/react': 19.2.14
'@types/react-dom': 19.2.3(@types/react@19.2.14)
'@react-email/render@1.1.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4)':
dependencies:
html-to-text: 9.0.5
prettier: 3.8.3
react: 19.2.4
react-dom: 19.2.4(react@19.2.4)
react-promise-suspense: 0.3.4
'@rexxars/react-json-inspector@9.0.1(react@19.2.4)':
dependencies:
debounce: 1.2.1
@@ -9102,6 +9280,11 @@ snapshots:
'@schummar/icu-type-parser@1.21.5': {}
'@selderee/plugin-htmlparser2@0.11.0':
dependencies:
domhandler: 5.0.3
selderee: 0.11.0
'@sentry-internal/browser-utils@8.55.1':
dependencies:
'@sentry/core': 8.55.1
@@ -10222,6 +10405,8 @@ snapshots:
deep-is@0.1.4: {}
deepmerge@4.3.1: {}
defaults@1.0.4:
dependencies:
clone: 1.0.4
@@ -10731,6 +10916,8 @@ snapshots:
fast-content-type-parse@3.0.0: {}
fast-deep-equal@2.0.1: {}
fast-deep-equal@3.1.3: {}
fast-fifo@1.3.2: {}
@@ -11113,6 +11300,21 @@ snapshots:
dependencies:
void-elements: 3.1.0
html-to-text@9.0.5:
dependencies:
'@selderee/plugin-htmlparser2': 0.11.0
deepmerge: 4.3.1
dom-serializer: 2.0.0
htmlparser2: 8.0.2
selderee: 0.11.0
htmlparser2@8.0.2:
dependencies:
domelementtype: 2.3.0
domhandler: 5.0.3
domutils: 3.2.2
entities: 4.5.0
http-proxy-agent@7.0.2:
dependencies:
agent-base: 7.1.4
@@ -11529,6 +11731,8 @@ snapshots:
dependencies:
readable-stream: 2.3.8
leac@0.6.0: {}
leven@3.1.0: {}
levn@0.4.1:
@@ -12142,6 +12346,11 @@ snapshots:
dependencies:
entities: 8.0.0
parseley@0.12.1:
dependencies:
leac: 0.6.0
peberminta: 0.9.0
path-exists@3.0.0: {}
path-exists@4.0.0: {}
@@ -12168,6 +12377,8 @@ snapshots:
path-type@4.0.0: {}
peberminta@0.9.0: {}
peek-stream@1.1.3:
dependencies:
buffer-from: 1.1.2
@@ -12353,6 +12564,10 @@ snapshots:
react-is@18.3.1: {}
react-promise-suspense@0.3.4:
dependencies:
fast-deep-equal: 2.0.1
react-refractor@2.2.0(react@19.2.4):
dependencies:
react: 19.2.4
@@ -12515,6 +12730,13 @@ snapshots:
reselect@5.1.1: {}
resend@4.8.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4):
dependencies:
'@react-email/render': 1.1.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
transitivePeerDependencies:
- react
- react-dom
resolve-from@4.0.0: {}
resolve-from@5.0.0: {}
@@ -12830,6 +13052,10 @@ snapshots:
dependencies:
commander: 2.20.3
selderee@0.11.0:
dependencies:
parseley: 0.12.1
semver@5.7.2: {}
semver@6.3.1: {}

View File

@@ -1,9 +1,115 @@
export default function AboutPage() {
import type { Metadata } from "next";
import { getTranslations } from "next-intl/server";
import { Github, Linkedin, Send, Twitter } from "lucide-react";
import { SectionHeader } from "@/components/shared/SectionHeader";
import { AnimatedSection } from "@/components/shared/AnimatedSection";
import { Timeline } from "@/components/about/Timeline";
import { Values } from "@/components/about/Values";
import { FocusAreas } from "@/components/about/FocusAreas";
import { sanityFetch } from "@/lib/sanity/client";
import { siteSettingsQuery } from "@/lib/sanity/queries";
import type { SiteSettings } from "@/lib/sanity/types";
import { groq } from "next-sanity";
interface Props {
params: Promise<{ locale: string }>;
}
export async function generateMetadata({ params }: Props): Promise<Metadata> {
const { locale } = await params;
const t = await getTranslations({ locale, namespace: "about" });
return { title: t("title"), description: t("subtitle") };
}
const timelineQuery = groq`
*[_type == "timelineEvent"] | order(date desc) {
title, date, description, icon, category
}
`;
const socialLinks = [
{ icon: Github, href: "https://github.com/alitaghavi", label: "GitHub" },
{ icon: Linkedin, href: "https://linkedin.com/in/alitaghavi", label: "LinkedIn" },
{ icon: Twitter, href: "https://twitter.com/alitaghavi", label: "Twitter/X" },
{ icon: Send, href: "https://t.me/alitaghavi", label: "Telegram" },
];
export default async function AboutPage({ params }: Props) {
const { locale } = await params;
const [t, settings, timelineEvents] = await Promise.all([
getTranslations({ locale, namespace: "about" }),
sanityFetch<SiteSettings>(siteSettingsQuery),
sanityFetch<unknown[]>(timelineQuery),
]);
const values = t.raw("values") as { title: string; desc: string }[];
const focusAreas = t.raw("focus_areas") as string[];
return (
<div className="mx-auto max-w-6xl px-6 py-32">
<span className="font-mono text-xs text-accent">// about</span>
<h1 className="mt-4 text-4xl font-bold text-text-primary">درباره علی تقوی</h1>
<p className="mt-4 text-text-secondary">در حال آمادهسازی Phase 3</p>
<div className="relative z-10 mx-auto max-w-6xl px-6 py-24 pt-32">
<SectionHeader label="about" />
{/* Hero */}
<AnimatedSection className="mb-20">
<div className="grid gap-12 lg:grid-cols-[1fr_auto]">
<div>
<h1 className="mb-6 text-4xl font-bold text-text-primary md:text-5xl">{t("title")}</h1>
<div className="space-y-4 max-w-2xl">
{t("bio").split("\n\n").map((paragraph, i) => (
<p key={i} className="text-base text-text-secondary leading-relaxed">
{paragraph}
</p>
))}
</div>
<div className="mt-8 flex items-center gap-3">
{socialLinks.map(({ icon: Icon, href, label }) => (
<a
key={href}
href={href}
target="_blank"
rel="noopener noreferrer"
aria-label={label}
className="flex h-10 w-10 items-center justify-center rounded-full border border-border text-text-secondary transition-all hover:border-accent/40 hover:text-accent hover:bg-accent-muted"
>
<Icon size={16} />
</a>
))}
</div>
</div>
{/* Current status card */}
{settings?.currentStatus && (
<div className="lg:w-72">
<div className="rounded-xl border border-accent/20 bg-surface p-6">
<div className="mb-3 font-mono text-xs text-accent">// {t("working_on")}</div>
<p className="text-text-primary leading-relaxed">{settings.currentStatus}</p>
<div className="mt-4 flex items-center gap-2">
<span className="h-2 w-2 animate-pulse rounded-full bg-success" />
<span className="font-mono text-xs text-success">active</span>
</div>
</div>
</div>
)}
</div>
</AnimatedSection>
{/* Values */}
<AnimatedSection className="mb-20">
<SectionHeader label={t("values_heading")} />
<Values values={values} />
</AnimatedSection>
{/* Focus Areas */}
<AnimatedSection className="mb-20">
<SectionHeader label={t("focus_heading")} />
<FocusAreas areas={focusAreas} />
</AnimatedSection>
{/* Timeline */}
<AnimatedSection>
<SectionHeader label={t("journey")} />
<Timeline events={timelineEvents as Parameters<typeof Timeline>[0]["events"]} />
</AnimatedSection>
</div>
);
}

View File

@@ -1,9 +1,106 @@
export default function ContactPage() {
import type { Metadata } from "next";
import { getTranslations } from "next-intl/server";
import { Github, Linkedin, Send, Twitter, Instagram, MapPin } from "lucide-react";
import { SectionHeader } from "@/components/shared/SectionHeader";
import { AnimatedSection } from "@/components/shared/AnimatedSection";
import { ContactForm } from "@/components/shared/ContactForm";
import { Button } from "@/components/ui/button";
interface Props {
params: Promise<{ locale: string }>;
}
export async function generateMetadata({ params }: Props): Promise<Metadata> {
const { locale } = await params;
const t = await getTranslations({ locale, namespace: "contact" });
return { title: t("title"), description: t("subtitle") };
}
const socialLinks = [
{ icon: Github, href: "https://github.com/alitaghavi", label: "GitHub" },
{ icon: Linkedin, href: "https://linkedin.com/in/alitaghavi", label: "LinkedIn" },
{ icon: Twitter, href: "https://twitter.com/alitaghavi", label: "Twitter/X" },
{ icon: Send, href: "https://t.me/alitaghavi", label: "Telegram" },
{ icon: Instagram, href: "https://instagram.com/alitaghavi", label: "Instagram" },
];
export default async function ContactPage({ params }: Props) {
const { locale } = await params;
const t = await getTranslations({ locale, namespace: "contact" });
const openToItems = t.raw("open_to_items") as string[];
return (
<div className="mx-auto max-w-6xl px-6 py-32">
<span className="font-mono text-xs text-accent">// contact</span>
<h1 className="mt-4 text-4xl font-bold text-text-primary">تماس</h1>
<p className="mt-4 text-text-secondary">در حال آمادهسازی Phase 3</p>
<div className="relative z-10 mx-auto max-w-6xl px-6 py-24 pt-32">
<SectionHeader label="contact" />
<div className="grid gap-12 lg:grid-cols-[1fr_380px]">
{/* Form */}
<AnimatedSection>
<h1 className="mb-2 text-4xl font-bold text-text-primary md:text-5xl">{t("title")}</h1>
<p className="mb-8 text-text-secondary">{t("subtitle")}</p>
<ContactForm />
</AnimatedSection>
{/* Sidebar info */}
<AnimatedSection delay={0.1} className="space-y-8">
{/* Open to */}
<div className="rounded-xl border border-border bg-surface p-6">
<div className="mb-4 font-mono text-xs text-accent">// {t("open_to")}</div>
<ul className="space-y-2">
{openToItems.map((item, i) => (
<li key={i} className="flex items-center gap-2 text-sm text-text-secondary">
<span className="h-1.5 w-1.5 rounded-full bg-accent shrink-0" />
{item}
</li>
))}
</ul>
</div>
{/* Location */}
<div className="flex items-center gap-2 font-mono text-sm text-text-secondary">
<MapPin size={14} className="text-accent shrink-0" />
{t("based_in")}
</div>
{/* Social */}
<div>
<div className="mb-4 font-mono text-xs text-accent">// social</div>
<div className="flex flex-wrap gap-3">
{socialLinks.map(({ icon: Icon, href, label }) => (
<a
key={href}
href={href}
target="_blank"
rel="noopener noreferrer"
aria-label={label}
className="flex h-10 w-10 items-center justify-center rounded-full border border-border text-text-secondary transition-all hover:border-accent/40 hover:text-accent hover:bg-accent-muted"
>
<Icon size={16} />
</a>
))}
</div>
</div>
{/* Telegram CTA */}
<div className="relative overflow-hidden rounded-xl border border-accent/20 bg-surface p-6">
<div className="absolute -right-8 -top-8 h-24 w-24 rounded-full bg-accent/5 blur-2xl" />
<div className="mb-3 flex h-10 w-10 items-center justify-center rounded-full bg-accent-muted">
<Send size={18} className="text-accent" />
</div>
<p className="mb-4 text-sm text-text-secondary leading-relaxed">
{locale === "fa"
? "برای یادداشت‌های روزانه، ایده‌ها و آپدیت‌ها، کانال تلگرامم را دنبال کن"
: "For daily notes, ideas, and updates, follow my Telegram channel"}
</p>
<Button asChild size="sm" variant="outline">
<a href="https://t.me/alitaghavi_channel" target="_blank" rel="noopener noreferrer">
<Send size={13} />
{locale === "fa" ? "عضو کانال" : "Join Channel"}
</a>
</Button>
</div>
</AnimatedSection>
</div>
</div>
);
}

View File

@@ -0,0 +1,48 @@
"use client";
import { useEffect } from "react";
import { useTranslations } from "next-intl";
import { Link } from "@/lib/i18n/navigation";
import { RefreshCw, Home } from "lucide-react";
import { Button } from "@/components/ui/button";
import { AnimatedSection } from "@/components/shared/AnimatedSection";
export default function Error({
error,
reset,
}: {
error: Error & { digest?: string };
reset: () => void;
}) {
const t = useTranslations("errors");
useEffect(() => {
console.error(error);
}, [error]);
return (
<div className="relative z-10 flex min-h-[70vh] flex-col items-center justify-center px-6 py-24 text-center">
<AnimatedSection>
<div className="mb-6 font-mono text-8xl font-bold text-danger/20 select-none">500</div>
<div className="mb-3 font-mono text-sm text-danger">// {t("error.label")}</div>
<h1 className="mb-4 text-3xl font-bold text-text-primary md:text-4xl">{t("error.title")}</h1>
<p className="mb-8 max-w-md text-text-secondary">{t("error.description")}</p>
<div className="flex flex-wrap items-center justify-center gap-3">
<Button onClick={reset}>
<RefreshCw size={15} />
{t("error.retry")}
</Button>
<Button asChild variant="outline">
<Link href="/">
<Home size={15} />
{t("error.home")}
</Link>
</Button>
</div>
{error.digest && (
<p className="mt-6 font-mono text-xs text-text-secondary/50">digest: {error.digest}</p>
)}
</AnimatedSection>
</div>
);
}

View File

@@ -85,9 +85,15 @@ export default async function LocaleLayout({
<link rel="alternate" type="application/rss+xml" title="علی تقوی — نوشته‌ها" href="/api/rss" />
</head>
<body className="relative flex min-h-full flex-col bg-background text-text-primary">
<a
href="#main-content"
className="sr-only focus:not-sr-only focus:fixed focus:left-4 focus:top-4 focus:z-100 focus:rounded-md focus:bg-accent focus:px-4 focus:py-2 focus:text-sm focus:font-medium focus:text-background focus:outline-none"
>
{dir === "rtl" ? "رفتن به محتوای اصلی" : "Skip to main content"}
</a>
<NextIntlClientProvider messages={messages}>
<Header />
<main className="flex-1 relative z-10">{children}</main>
<main id="main-content" className="flex-1 relative z-10">{children}</main>
<Footer />
</NextIntlClientProvider>
</body>

View File

@@ -0,0 +1,36 @@
"use client";
import { useTranslations } from "next-intl";
import { Link } from "@/lib/i18n/navigation";
import { Home, ArrowRight } from "lucide-react";
import { Button } from "@/components/ui/button";
import { AnimatedSection } from "@/components/shared/AnimatedSection";
export default function NotFound() {
const t = useTranslations("errors");
return (
<div className="relative z-10 flex min-h-[70vh] flex-col items-center justify-center px-6 py-24 text-center">
<AnimatedSection>
<div className="mb-6 font-mono text-8xl font-bold text-accent/20 select-none">404</div>
<div className="mb-3 font-mono text-sm text-accent">// {t("notFound.label")}</div>
<h1 className="mb-4 text-3xl font-bold text-text-primary md:text-4xl">{t("notFound.title")}</h1>
<p className="mb-8 max-w-md text-text-secondary">{t("notFound.description")}</p>
<div className="flex flex-wrap items-center justify-center gap-3">
<Button asChild>
<Link href="/">
<Home size={15} />
{t("notFound.home")}
</Link>
</Button>
<Button asChild variant="outline">
<Link href="/writing">
<ArrowRight size={15} />
{t("notFound.writing")}
</Link>
</Button>
</div>
</AnimatedSection>
</div>
);
}

View File

@@ -0,0 +1,187 @@
import type { Metadata } from "next";
import { getTranslations } from "next-intl/server";
import { Download, Calendar, MapPin } from "lucide-react";
import { Button } from "@/components/ui/button";
import { Badge } from "@/components/ui/badge";
import { SectionHeader } from "@/components/shared/SectionHeader";
import { AnimatedSection } from "@/components/shared/AnimatedSection";
interface Props {
params: Promise<{ locale: string }>;
}
export async function generateMetadata({ params }: Props): Promise<Metadata> {
const { locale } = await params;
const t = await getTranslations({ locale, namespace: "resume" });
return { title: t("title") };
}
const experience = [
{
role: "مدیرعامل (CEO)",
roleEn: "CEO & Founder",
company: "NODE-Group",
period: "۱۴۰۱",
periodEn: "2022",
presentKey: true,
location: "تهران، ایران",
locationEn: "Tehran, Iran",
bullets: [
"راه‌اندازی و رهبری شرکت فناوری با محصولات HyperAccount، Khanehban و NODE-AUTH",
"طراحی استراتژی محصول، برند و بازاریابی دیجیتال",
"مدیریت تیم توسعه و عملیات",
],
bulletsEn: [
"Founded and lead technology company with products HyperAccount, Khanehban, and NODE-AUTH",
"Designed product, brand, and digital marketing strategy",
"Managed development and operations teams",
],
},
];
const education = [
{
degree: "کارشناسی ارشد مدیریت کسب‌وکار (MBA)",
degreeEn: "Master of Business Administration (MBA)",
school: "دانشگاه",
schoolEn: "University",
year: "۱۴۰۰",
yearEn: "2021",
},
];
const skillGroups = [
{
category: "فناوری",
categoryEn: "Technology",
skills: ["Next.js", "TypeScript", "React", "Node.js", "Docker", "PostgreSQL", "Sanity CMS"],
},
{
category: "بازاریابی و برند",
categoryEn: "Marketing & Brand",
skills: ["دیجیتال مارکتینگ", "برندینگ", "SEO", "محتوا مارکتینگ", "استراتژی رسانه‌های اجتماعی"],
},
{
category: "استراتژی محصول",
categoryEn: "Product Strategy",
skills: ["Product Management", "UX Research", "A/B Testing", "Roadmapping", "Customer Discovery"],
},
{
category: "ابزارها",
categoryEn: "Tools",
skills: ["Figma", "Linear", "Notion", "VS Code", "GitHub Actions"],
},
];
export default async function ResumePage({ params }: Props) {
const { locale } = await params;
const t = await getTranslations({ locale, namespace: "resume" });
const isRtl = locale === "fa";
return (
<div className="relative z-10 mx-auto max-w-4xl px-6 py-24 pt-32">
{/* Header */}
<AnimatedSection>
<div className="mb-12 flex items-start justify-between gap-4">
<div>
<SectionHeader label="resume" />
<h1 className="text-4xl font-bold text-text-primary">
{isRtl ? "علی تقوی" : "Ali Taghavi"}
</h1>
<p className="mt-2 text-text-secondary">
{isRtl ? "مدیرعامل NODE-Group · سازنده · استراتژیست" : "CEO @ NODE-Group · Builder · Strategist"}
</p>
<div className="mt-3 flex flex-wrap items-center gap-3 text-sm text-text-secondary">
<span className="flex items-center gap-1"><MapPin size={13} /> {isRtl ? "تهران، ایران" : "Tehran, Iran"}</span>
<span className="font-mono">biztaghavi.com</span>
</div>
</div>
<Button asChild variant="secondary" size="sm" className="shrink-0">
<a href="/resume.pdf" download>
<Download size={14} />
{t("download_pdf")}
</a>
</Button>
</div>
</AnimatedSection>
{/* Experience */}
<AnimatedSection className="mb-12">
<SectionHeader label={t("experience")} />
<div className="space-y-8">
{experience.map((job, i) => (
<div key={i} className="rounded-xl border border-border bg-surface p-6">
<div className="mb-4 flex flex-col gap-1 sm:flex-row sm:items-start sm:justify-between">
<div>
<h3 className="font-semibold text-text-primary">
{isRtl ? job.role : job.roleEn}
</h3>
<p className="font-mono text-sm text-accent">{job.company}</p>
</div>
<div className="flex flex-col items-start gap-1 sm:items-end shrink-0">
<div className="flex items-center gap-1 font-mono text-xs text-text-secondary">
<Calendar size={11} />
{isRtl ? job.period : job.periodEn} {t("present")}
</div>
<span className="font-mono text-xs text-text-secondary">
<MapPin size={11} className="inline me-1" />
{isRtl ? job.location : job.locationEn}
</span>
</div>
</div>
<ul className="space-y-2">
{(isRtl ? job.bullets : job.bulletsEn).map((b, j) => (
<li key={j} className="flex items-start gap-2 text-sm text-text-secondary">
<span className="mt-1.5 h-1.5 w-1.5 shrink-0 rounded-full bg-accent" />
{b}
</li>
))}
</ul>
</div>
))}
</div>
</AnimatedSection>
{/* Education */}
<AnimatedSection className="mb-12">
<SectionHeader label={t("education")} />
<div className="space-y-4">
{education.map((edu, i) => (
<div key={i} className="rounded-xl border border-border bg-surface p-6">
<div className="flex flex-col gap-1 sm:flex-row sm:items-start sm:justify-between">
<div>
<h3 className="font-semibold text-text-primary">
{isRtl ? edu.degree : edu.degreeEn}
</h3>
<p className="text-sm text-text-secondary">{isRtl ? edu.school : edu.schoolEn}</p>
</div>
<span className="font-mono text-xs text-text-secondary shrink-0">
{isRtl ? edu.year : edu.yearEn}
</span>
</div>
</div>
))}
</div>
</AnimatedSection>
{/* Skills */}
<AnimatedSection>
<SectionHeader label={t("skills")} />
<div className="grid gap-6 sm:grid-cols-2">
{skillGroups.map((group, i) => (
<div key={i} className="rounded-xl border border-border bg-surface p-5">
<h3 className="mb-3 font-mono text-xs text-accent">
// {isRtl ? group.category : group.categoryEn}
</h3>
<div className="flex flex-wrap gap-2">
{group.skills.map((skill) => (
<Badge key={skill} variant="secondary">{skill}</Badge>
))}
</div>
</div>
))}
</div>
</AnimatedSection>
</div>
);
}

View File

@@ -0,0 +1,154 @@
import type { Metadata } from "next";
import Image from "next/image";
import { getTranslations } from "next-intl/server";
import { ExternalLink } from "lucide-react";
import { SectionHeader } from "@/components/shared/SectionHeader";
import { AnimatedSection } from "@/components/shared/AnimatedSection";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import { sanityFetch } from "@/lib/sanity/client";
import { allProductsQuery } from "@/lib/sanity/queries";
import { urlFor } from "@/lib/sanity/image";
import type { SanityImage } from "@/lib/sanity/types";
interface Props {
params: Promise<{ locale: string }>;
}
interface Product {
_id: string;
title: string;
slug: { current: string };
description?: string;
price?: number;
currency?: string;
productType?: string;
purchaseUrl?: string;
featured?: boolean;
coverImage?: SanityImage;
}
export async function generateMetadata({ params }: Props): Promise<Metadata> {
const { locale } = await params;
const t = await getTranslations({ locale, namespace: "shop" });
return { title: t("title"), description: t("subtitle") };
}
// Placeholder products shown when Sanity has no data yet
const placeholders: Product[] = [
{
_id: "p1",
title: "دوره بازاریابی دیجیتال برای استارتاپ‌های ایرانی",
slug: { current: "digital-marketing-course" },
description: "آموزش جامع استراتژی بازاریابی دیجیتال متناسب با بازار ایران — از صفر تا اجرا",
price: 490000,
currency: "IRR",
productType: "digital",
},
{
_id: "p2",
title: "قالب استراتژی برند",
slug: { current: "brand-strategy-template" },
description: "قالب آماده برای طراحی هویت برند و استراتژی بصری کسب‌وکار",
price: 0,
currency: "IRR",
productType: "digital",
},
{
_id: "p3",
title: "HyperAccount",
slug: { current: "hyperaccount" },
description: "زیرساخت هویت و حساب کاربری برای محصولات دیجیتال ایرانی",
productType: "node-product",
purchaseUrl: "https://hyperaccount.ir",
},
];
export default async function ShopPage({ params }: Props) {
const { locale } = await params;
const t = await getTranslations({ locale, namespace: "shop" });
const isRtl = locale === "fa";
const sanityProducts = await sanityFetch<Product[]>(allProductsQuery, { locale });
const products = sanityProducts && sanityProducts.length > 0 ? sanityProducts : placeholders;
function formatPrice(price: number, currency: string) {
if (price === 0) return isRtl ? t("free") : "Free";
if (currency === "IRR") {
return new Intl.NumberFormat("fa-IR").format(price) + " تومان";
}
return `$${price}`;
}
return (
<div className="relative z-10 mx-auto max-w-6xl px-6 py-24 pt-32">
<SectionHeader label="shop" />
<AnimatedSection className="mb-12">
<h1 className="text-4xl font-bold text-text-primary md:text-5xl">{t("title")}</h1>
<p className="mt-4 max-w-2xl text-text-secondary">{t("subtitle")}</p>
</AnimatedSection>
<div className="grid gap-6 sm:grid-cols-2 lg:grid-cols-3">
{products.map((product, i) => {
const imageUrl = product.coverImage?.asset
? urlFor(product.coverImage).width(600).height(340).url()
: null;
return (
<AnimatedSection key={product._id} delay={i * 0.07}>
<div className="flex h-full flex-col overflow-hidden rounded-xl border border-border bg-surface transition-all hover:border-accent/20 hover:bg-surface-hover">
{/* Image or placeholder */}
{imageUrl ? (
<div className="relative h-44 overflow-hidden">
<Image src={imageUrl} alt={product.title} fill className="object-cover" />
</div>
) : (
<div className="flex h-44 items-center justify-center bg-surface-hover">
<span className="font-mono text-3xl text-accent opacity-20">//</span>
</div>
)}
<div className="flex flex-1 flex-col p-5">
<div className="mb-3 flex items-center gap-2">
<Badge variant={product.productType === "node-product" ? "default" : "secondary"}>
{product.productType === "node-product" ? t("node_product") : t("digital")}
</Badge>
{product.featured && (
<span className="font-mono text-xs text-accent"></span>
)}
</div>
<h3 className="mb-2 font-semibold text-text-primary">{product.title}</h3>
{product.description && (
<p className="mb-4 flex-1 text-sm text-text-secondary leading-relaxed line-clamp-3">
{product.description}
</p>
)}
<div className="mt-auto flex items-center justify-between gap-3">
{product.price !== undefined && (
<span className="font-mono text-sm font-semibold text-accent">
{formatPrice(product.price, product.currency ?? "IRR")}
</span>
)}
{product.purchaseUrl ? (
<Button asChild size="sm" className="ms-auto">
<a href={product.purchaseUrl} target="_blank" rel="noopener noreferrer">
<ExternalLink size={13} />
{t("buy")}
</a>
</Button>
) : (
<Button size="sm" className="ms-auto">{t("buy")}</Button>
)}
</div>
</div>
</div>
</AnimatedSection>
);
})}
</div>
</div>
);
}

View File

@@ -0,0 +1,104 @@
import type { Metadata } from "next";
import { getTranslations } from "next-intl/server";
import { ExternalLink } from "lucide-react";
import { SectionHeader } from "@/components/shared/SectionHeader";
import { AnimatedSection } from "@/components/shared/AnimatedSection";
import { sanityFetch } from "@/lib/sanity/client";
import { allUsesItemsQuery } from "@/lib/sanity/queries";
interface Props {
params: Promise<{ locale: string }>;
}
type UsesCategory = "development" | "design" | "marketing" | "productivity" | "hardware";
interface UsesItem {
_id: string;
title: string;
description?: string;
category: UsesCategory;
url?: string;
}
export async function generateMetadata({ params }: Props): Promise<Metadata> {
const { locale } = await params;
const t = await getTranslations({ locale, namespace: "uses" });
return { title: t("title"), description: t("subtitle") };
}
// Static fallback — shown until Sanity has data
const fallbackItems: UsesItem[] = [
{ _id: "1", title: "VS Code", description: "اصلی‌ترین ابزار کدنویسی‌ام", category: "development", url: "https://code.visualstudio.com" },
{ _id: "2", title: "Next.js", description: "فریم‌ورک React برای وب اپلیکیشن‌ها", category: "development", url: "https://nextjs.org" },
{ _id: "3", title: "Sanity", description: "هدلس CMS برای مدیریت محتوا", category: "development", url: "https://sanity.io" },
{ _id: "4", title: "Docker", description: "کانتینرایزیشن برای دپلوی", category: "development" },
{ _id: "5", title: "Figma", description: "طراحی UI/UX و هویت بصری", category: "design", url: "https://figma.com" },
{ _id: "6", title: "Linear", description: "مدیریت پروژه و تسک‌ها", category: "productivity", url: "https://linear.app" },
{ _id: "7", title: "Notion", description: "مستندسازی و یادداشت‌برداری", category: "productivity", url: "https://notion.so" },
{ _id: "8", title: "macOS", description: "سیستم‌عامل اصلی", category: "hardware" },
{ _id: "9", title: "Google Analytics + Umami", description: "آنالیتیکس سایت — هر دو برای مقایسه", category: "marketing" },
];
const categoryOrder: UsesCategory[] = ["development", "design", "marketing", "productivity", "hardware"];
export default async function UsesPage({ params }: Props) {
const { locale } = await params;
const t = await getTranslations({ locale, namespace: "uses" });
const tCat = await getTranslations({ locale, namespace: "uses.categories" });
const sanityItems = await sanityFetch<UsesItem[]>(allUsesItemsQuery);
const items = sanityItems && sanityItems.length > 0 ? sanityItems : fallbackItems;
const grouped = categoryOrder.reduce<Record<string, UsesItem[]>>((acc, cat) => {
const catItems = items.filter((i) => i.category === cat);
if (catItems.length > 0) acc[cat] = catItems;
return acc;
}, {});
return (
<div className="relative z-10 mx-auto max-w-4xl px-6 py-24 pt-32">
<SectionHeader label="uses" />
<AnimatedSection className="mb-12">
<h1 className="text-4xl font-bold text-text-primary md:text-5xl">{t("title")}</h1>
<p className="mt-4 text-text-secondary">{t("subtitle")}</p>
</AnimatedSection>
<div className="space-y-14">
{Object.entries(grouped).map(([cat, catItems], groupIdx) => (
<AnimatedSection key={cat} delay={groupIdx * 0.05}>
<h2 className="mb-6 flex items-center gap-3">
<span className="font-mono text-xs text-accent">// {tCat(cat as UsesCategory)}</span>
<span className="h-px flex-1 bg-border" />
</h2>
<div className="grid gap-3 sm:grid-cols-2">
{catItems.map((item) => (
<div
key={item._id}
className="group flex items-start justify-between gap-3 rounded-lg border border-border bg-surface p-4 transition-all hover:border-accent/20"
>
<div className="flex-1 min-w-0">
<div className="font-medium text-text-primary">{item.title}</div>
{item.description && (
<p className="mt-0.5 text-sm text-text-secondary">{item.description}</p>
)}
</div>
{item.url && (
<a
href={item.url}
target="_blank"
rel="noopener noreferrer"
className="mt-0.5 shrink-0 text-text-secondary opacity-0 transition-opacity group-hover:opacity-100 hover:text-accent"
aria-label={`Visit ${item.title}`}
>
<ExternalLink size={14} />
</a>
)}
</div>
))}
</div>
</AnimatedSection>
))}
</div>
</div>
);
}

View File

@@ -0,0 +1,36 @@
import { Resend } from "resend";
import type { NextRequest } from "next/server";
const resend = new Resend(process.env.RESEND_API_KEY);
export async function POST(req: NextRequest) {
try {
const { name, email, subject, message } = await req.json();
if (!name || !email || !message) {
return Response.json({ error: "Missing required fields" }, { status: 400 });
}
await resend.emails.send({
from: "biztaghavi.com <noreply@biztaghavi.com>",
to: [process.env.CONTACT_EMAIL ?? "ali@biztaghavi.com"],
replyTo: email,
subject: subject ? `[biztaghavi.com] ${subject}` : `[biztaghavi.com] پیام از ${name}`,
text: `نام: ${name}\nایمیل: ${email}\n\n${message}`,
html: `
<div style="font-family: sans-serif; max-width: 600px;">
<p><strong>نام:</strong> ${name}</p>
<p><strong>ایمیل:</strong> ${email}</p>
${subject ? `<p><strong>موضوع:</strong> ${subject}</p>` : ""}
<hr />
<p style="white-space: pre-wrap;">${message}</p>
</div>
`,
});
return Response.json({ success: true });
} catch (err) {
console.error("[contact]", err);
return Response.json({ error: "Failed to send" }, { status: 500 });
}
}

View File

@@ -1,4 +1,5 @@
import type { Metadata } from "next";
import Script from "next/script";
import "./globals.css";
export const metadata: Metadata = {
@@ -10,10 +11,26 @@ export const metadata: Metadata = {
metadataBase: new URL("https://biztaghavi.com"),
};
const umamiSrc = process.env.NEXT_PUBLIC_UMAMI_URL;
const umamiId = process.env.NEXT_PUBLIC_UMAMI_WEBSITE_ID;
export default function RootLayout({
children,
}: Readonly<{
children: React.ReactNode;
}>) {
return children;
return (
<>
{umamiSrc && umamiId && (
<Script
async
defer
src={umamiSrc}
data-website-id={umamiId}
strategy="afterInteractive"
/>
)}
{children}
</>
);
}

View File

@@ -0,0 +1,38 @@
"use client";
import { motion } from "framer-motion";
const icons: Record<string, string> = {
"فناوری و توسعه نرم‌افزار": "⚡",
"بازاریابی دیجیتال": "📢",
"برندینگ و هویت بصری": "🎨",
"استراتژی محصول": "🧭",
"محتوا و تولید محتوا": "✍️",
"متن‌باز": "🔓",
"Technology & Software Development": "⚡",
"Digital Marketing": "📢",
"Branding & Visual Identity": "🎨",
"Product Strategy": "🧭",
"Content & Content Creation": "✍️",
"Open Source": "🔓",
};
export function FocusAreas({ areas }: { areas: string[] }) {
return (
<div className="flex flex-wrap gap-3">
{areas.map((area, i) => (
<motion.div
key={i}
initial={{ opacity: 0, scale: 0.9 }}
whileInView={{ opacity: 1, scale: 1 }}
viewport={{ once: true }}
transition={{ duration: 0.3, delay: i * 0.06 }}
className="flex items-center gap-2 rounded-full border border-border bg-surface px-4 py-2 text-sm text-text-secondary hover:border-accent/30 hover:text-text-primary transition-colors"
>
<span>{icons[area] ?? "●"}</span>
{area}
</motion.div>
))}
</div>
);
}

View File

@@ -0,0 +1,74 @@
"use client";
import { motion } from "framer-motion";
import { cn } from "@/lib/utils";
interface TimelineEvent {
title: string;
date: string;
description?: string;
icon?: string;
category?: "career" | "product" | "personal";
}
const categoryColor: Record<string, string> = {
career: "text-accent border-accent/40 bg-accent-muted",
product: "text-blue-400 border-blue-400/40 bg-blue-400/10",
personal: "text-purple-400 border-purple-400/40 bg-purple-400/10",
};
// Static fallback timeline — will be overridden by Sanity data when available
const fallbackEvents: TimelineEvent[] = [
{ title: "تأسیس NODE-Group", date: "۱۴۰۱", icon: "🏢", category: "career", description: "راه‌اندازی شرکت فناوری NODE-Group برای ساختن محصولات نوآورانه" },
{ title: "راه‌اندازی HyperAccount", date: "۱۴۰۲", icon: "⚡", category: "product", description: "عرضه زیرساخت هویت و حساب کاربری برای وب نسل بعدی" },
{ title: "ساخت Khanehban", date: "۱۴۰۲", icon: "🏠", category: "product", description: "مدیریت هوشمند املاک برای موجران و مستأجران ایرانی" },
{ title: "NODE-AUTH (Dezhban)", date: "۱۴۰۳", icon: "🔐", category: "product", description: "لایه احراز هویت و مجوزدهی برای محصولات ایرانی" },
{ title: "MBA", date: "۱۴۰۰", icon: "🎓", category: "personal", description: "فارغ‌التحصیلی از دوره MBA" },
];
interface TimelineProps {
events?: TimelineEvent[];
}
export function Timeline({ events }: TimelineProps) {
const items = events && events.length > 0 ? events : fallbackEvents;
return (
<div className="relative">
{/* Vertical line */}
<div className="absolute start-5 top-0 bottom-0 w-px bg-border" />
<div className="space-y-8">
{items.map((event, i) => (
<motion.div
key={i}
initial={{ opacity: 0, x: -16 }}
whileInView={{ opacity: 1, x: 0 }}
viewport={{ once: true, margin: "-40px" }}
transition={{ duration: 0.4, delay: i * 0.05 }}
className="relative flex gap-6 ps-14"
>
{/* Icon dot */}
<div className="absolute start-0 flex h-10 w-10 items-center justify-center rounded-full border border-border bg-surface text-lg">
{event.icon ?? "●"}
</div>
<div className="flex-1 pb-2">
<div className="mb-1 flex flex-wrap items-center gap-2">
<h3 className="font-semibold text-text-primary">{event.title}</h3>
{event.category && (
<span className={cn("rounded-full border px-2 py-0.5 font-mono text-xs", categoryColor[event.category])}>
{event.date}
</span>
)}
</div>
{event.description && (
<p className="text-sm text-text-secondary leading-relaxed">{event.description}</p>
)}
</div>
</motion.div>
))}
</div>
</div>
);
}

View File

@@ -0,0 +1,28 @@
"use client";
import { motion } from "framer-motion";
interface Value {
title: string;
desc: string;
}
export function Values({ values }: { values: Value[] }) {
return (
<div className="grid gap-4 sm:grid-cols-2">
{values.map((v, i) => (
<motion.div
key={i}
initial={{ opacity: 0, y: 16 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true, margin: "-40px" }}
transition={{ duration: 0.4, delay: i * 0.07 }}
className="rounded-lg border border-border bg-surface p-5 hover:border-accent/20 transition-colors"
>
<div className="mb-1 font-semibold text-text-primary">{v.title}</div>
<p className="text-sm text-text-secondary leading-relaxed">{v.desc}</p>
</motion.div>
))}
</div>
);
}

View File

@@ -12,6 +12,9 @@ const navLinks = [
{ href: "/writing", labelKey: "writing" },
{ href: "/work", labelKey: "work" },
{ href: "/about", labelKey: "about" },
{ href: "/resume", labelKey: "resume" },
{ href: "/shop", labelKey: "shop" },
{ href: "/uses", labelKey: "uses" },
{ href: "/contact", labelKey: "contact" },
] as const;

View File

@@ -7,12 +7,15 @@ import { Link } from "@/lib/i18n/navigation";
import { cn } from "@/lib/utils";
import { LanguageSwitcher } from "./LanguageSwitcher";
const navKeys = ["home", "writing", "work", "about", "contact"] as const;
const navKeys = ["home", "writing", "work", "about", "shop", "uses", "resume", "contact"] as const;
const navHrefs: Record<string, string> = {
home: "/",
writing: "/writing",
work: "/work",
about: "/about",
shop: "/shop",
uses: "/uses",
resume: "/resume",
contact: "/contact",
};

View File

@@ -0,0 +1,90 @@
"use client";
import { useState } from "react";
import { useTranslations, useLocale } from "next-intl";
import { Send, Check, AlertCircle } from "lucide-react";
import { Button } from "@/components/ui/button";
type Status = "idle" | "loading" | "success" | "error";
export function ContactForm() {
const t = useTranslations("contact");
const locale = useLocale();
const isRtl = locale === "fa";
const [status, setStatus] = useState<Status>("idle");
const [form, setForm] = useState({ name: "", email: "", subject: "", message: "" });
function set(field: keyof typeof form) {
return (e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) =>
setForm((prev) => ({ ...prev, [field]: e.target.value }));
}
async function handleSubmit(e: React.FormEvent) {
e.preventDefault();
setStatus("loading");
try {
const res = await fetch("/api/contact", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(form),
});
setStatus(res.ok ? "success" : "error");
if (res.ok) setForm({ name: "", email: "", subject: "", message: "" });
} catch {
setStatus("error");
}
}
const inputClass =
"w-full rounded-lg border border-border bg-surface px-4 py-3 text-sm text-text-primary placeholder:text-text-secondary focus:border-accent/50 focus:outline-none transition-colors";
return (
<form onSubmit={handleSubmit} className="space-y-4">
<div className="grid gap-4 sm:grid-cols-2">
<div>
<label className="mb-1.5 block font-mono text-xs text-text-secondary">{t("name")}</label>
<input required value={form.name} onChange={set("name")} className={inputClass} placeholder={isRtl ? "علی تقوی" : "Your name"} />
</div>
<div>
<label className="mb-1.5 block font-mono text-xs text-text-secondary">{t("email")}</label>
<input required type="email" value={form.email} onChange={set("email")} className={inputClass} placeholder="hello@example.com" />
</div>
</div>
<div>
<label className="mb-1.5 block font-mono text-xs text-text-secondary">{t("subject")}</label>
<input value={form.subject} onChange={set("subject")} className={inputClass} placeholder={isRtl ? "موضوع پیام (اختیاری)" : "Subject (optional)"} />
</div>
<div>
<label className="mb-1.5 block font-mono text-xs text-text-secondary">{t("message")}</label>
<textarea
required
rows={6}
value={form.message}
onChange={set("message")}
className={`${inputClass} resize-none`}
placeholder={isRtl ? "پیامت را اینجا بنویس..." : "Your message..."}
/>
</div>
{status === "success" && (
<div className="flex items-center gap-2 rounded-lg border border-success/30 bg-success/10 px-4 py-3 text-sm text-success">
<Check size={15} />
{t("success")}
</div>
)}
{status === "error" && (
<div className="flex items-center gap-2 rounded-lg border border-danger/30 bg-danger/10 px-4 py-3 text-sm text-danger">
<AlertCircle size={15} />
{t("error")}
</div>
)}
<Button type="submit" size="lg" disabled={status === "loading"} className="w-full sm:w-auto">
<Send size={15} />
{status === "loading" ? t("sending") : t("send")}
</Button>
</form>
);
}

View File

@@ -89,6 +89,23 @@ export const siteSettingsQuery = groq`
}
`;
// ── Products ───────────────────────────────────────────────────────────────
export const allProductsQuery = groq`
*[_type == "product" && locale == $locale] | order(_createdAt desc) {
_id, title, slug, description, price, currency, productType, purchaseUrl, featured,
"coverImage": coverImage { asset->, alt }
}
`;
// ── Uses items ─────────────────────────────────────────────────────────────
export const allUsesItemsQuery = groq`
*[_type == "usesItem"] | order(category asc, title asc) {
_id, title, description, category, url, "image": image { asset-> }
}
`;
// ── RSS (all fa posts) ─────────────────────────────────────────────────────
export const rssPostsQuery = groq`

View File

@@ -79,5 +79,88 @@
"outcome": "Outcome",
"tech_stack": "Tech Stack",
"tools": "Tools Used"
},
"about": {
"title": "About Ali Taghavi",
"subtitle": "A builder from Tehran operating at the intersection of technology, branding, and product",
"bio_heading": "Who I Am",
"bio": "I'm Ali Taghavi — CEO of NODE-Group and a builder who creates from Tehran for the world. For over a decade I've been building products, brands, and systems — from software to marketing strategy and visual identity.\n\nMy work sits at the intersection of technology, digital marketing, and branding. I believe the best products come from combining systematic thinking with deep human understanding.",
"working_on": "What I'm Working On",
"journey": "Journey",
"values_heading": "What I Believe In",
"focus_heading": "Areas of Focus",
"values": [
{ "title": "Transparency", "desc": "In work and communication, I speak clearly and honestly" },
{ "title": "Systems Thinking", "desc": "I see problems as systems, not isolated events" },
{ "title": "Building for Iran", "desc": "I build products truly designed for Persian-speaking users" },
{ "title": "Long-term Quality", "desc": "I prefer to build less but build it right and lasting" }
],
"focus_areas": [
"Technology & Software Development",
"Digital Marketing",
"Branding & Visual Identity",
"Product Strategy",
"Content & Content Creation",
"Open Source"
]
},
"resume": {
"title": "Resume",
"download_pdf": "Download PDF",
"experience": "Experience",
"education": "Education",
"skills": "Skills",
"certifications": "Certifications",
"present": "Present"
},
"shop": {
"title": "Shop",
"subtitle": "Courses, templates, and resources I've built",
"buy": "Buy",
"free": "Free",
"digital": "Digital",
"node_product": "NODE Product"
},
"contact": {
"title": "Contact",
"subtitle": "For collaboration, consulting, or any question",
"name": "Name",
"email": "Email",
"subject": "Subject",
"message": "Message",
"send": "Send Message",
"sending": "Sending...",
"success": "Message received! I'll get back to you soon.",
"error": "Something went wrong. Please try again.",
"open_to": "Open to",
"open_to_items": ["Consulting", "Project Collaboration", "Speaking"],
"based_in": "Based in Tehran, Iran"
},
"uses": {
"title": "Uses",
"subtitle": "Tools, software, and hardware I use daily",
"categories": {
"development": "Development",
"design": "Design",
"marketing": "Marketing",
"productivity": "Productivity",
"hardware": "Hardware"
}
},
"errors": {
"notFound": {
"label": "not-found",
"title": "Page not found",
"description": "The page you're looking for doesn't exist or has been moved.",
"home": "Back to home",
"writing": "Browse writing"
},
"error": {
"label": "server-error",
"title": "Something went wrong",
"description": "An error occurred on the server. Please try again.",
"retry": "Try again",
"home": "Back to home"
}
}
}

View File

@@ -79,5 +79,88 @@
"outcome": "نتیجه",
"tech_stack": "تکنولوژی‌ها",
"tools": "ابزارها"
},
"about": {
"title": "درباره علی تقوی",
"subtitle": "سازنده‌ای از تهران که در تقاطع فناوری، برندینگ و محصول کار می‌کند",
"bio_heading": "چه کسی هستم",
"bio": "علی تقوی هستم — مدیرعامل NODE-Group و یک سازنده که از تهران برای دنیا می‌سازد. بیش از یک دهه است که محصول، برند و سیستم می‌سازم؛ از نرم‌افزار گرفته تا استراتژی بازاریابی و هویت بصری.\n\nکارم در تقاطع فناوری، بازاریابی دیجیتال و برندینگ قرار دارد. باور دارم که بهترین محصولات از ترکیب تفکر سیستماتیک با درک عمیق انسانی به وجود می‌آیند.",
"working_on": "الان روی چه کار می‌کنم",
"journey": "مسیر",
"values_heading": "چه چیزهایی برایم مهم است",
"focus_heading": "حوزه‌های تمرکز",
"values": [
{ "title": "شفافیت", "desc": "در کار و ارتباط، صادقانه و روشن حرف می‌زنم" },
{ "title": "تفکر سیستمی", "desc": "مسائل را در قالب سیستم‌ها می‌بینم، نه رویدادهای مجزا" },
{ "title": "ساختن برای ایران", "desc": "محصولاتی می‌سازم که واقعاً برای کاربر فارسی‌زبان طراحی شده‌اند" },
{ "title": "کیفیت بلند‌مدت", "desc": "ترجیح می‌دهم کمتر بسازم اما درست‌تر و ماندگارتر" }
],
"focus_areas": [
"فناوری و توسعه نرم‌افزار",
"بازاریابی دیجیتال",
"برندینگ و هویت بصری",
"استراتژی محصول",
"محتوا و تولید محتوا",
"متن‌باز"
]
},
"resume": {
"title": "رزومه",
"download_pdf": "دانلود PDF",
"experience": "تجربه کاری",
"education": "تحصیلات",
"skills": "مهارت‌ها",
"certifications": "گواهی‌نامه‌ها",
"present": "اکنون"
},
"shop": {
"title": "فروشگاه",
"subtitle": "دوره‌ها، قالب‌ها و منابعی که ساخته‌ام",
"buy": "خرید",
"free": "رایگان",
"digital": "دیجیتال",
"node_product": "محصول NODE"
},
"contact": {
"title": "تماس",
"subtitle": "برای همکاری، مشاوره یا هر سوالی پیام بده",
"name": "نام",
"email": "ایمیل",
"subject": "موضوع",
"message": "پیام",
"send": "ارسال پیام",
"sending": "در حال ارسال...",
"success": "پیامت دریافت شد! به‌زودی جواب می‌دم.",
"error": "مشکلی پیش آمد. دوباره تلاش کن.",
"open_to": "آماده همکاری در",
"open_to_items": ["مشاوره", "همکاری در پروژه", "سخنرانی"],
"based_in": "مستقر در تهران، ایران"
},
"uses": {
"title": "ابزارها",
"subtitle": "ابزارها، نرم‌افزارها و سخت‌افزارهایی که ازشان استفاده می‌کنم",
"categories": {
"development": "توسعه",
"design": "طراحی",
"marketing": "بازاریابی",
"productivity": "بهره‌وری",
"hardware": "سخت‌افزار"
}
},
"errors": {
"notFound": {
"label": "not-found",
"title": "صفحه پیدا نشد",
"description": "صفحه‌ای که دنبالش می‌گردی وجود ندارد یا منتقل شده.",
"home": "برگشت به خانه",
"writing": "مشاهده نوشته‌ها"
},
"error": {
"label": "server-error",
"title": "مشکلی پیش آمد",
"description": "خطایی در سرور رخ داد. لطفاً دوباره تلاش کن.",
"retry": "تلاش دوباره",
"home": "برگشت به خانه"
}
}
}