diff --git a/.dockerignore b/.dockerignore index fd43adf2..e9bad375 100644 --- a/.dockerignore +++ b/.dockerignore @@ -2,6 +2,10 @@ .github node_modules .next +.turbo .env*.local .DS_Store README.md +*.md +.vscode +.idea diff --git a/next-sitemap.config.js b/next-sitemap.config.js index facfde7d..b0750306 100644 --- a/next-sitemap.config.js +++ b/next-sitemap.config.js @@ -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 ?? [], + }; }, }; diff --git a/next.config.ts b/next.config.ts index 005d5da1..42d9b042 100644 --- a/next.config.ts +++ b/next.config.ts @@ -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); diff --git a/package.json b/package.json index b5887af7..0cdf66b6 100644 --- a/package.json +++ b/package.json @@ -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"] diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index b92c3e91..a91f7c68 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -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: {} diff --git a/src/app/[locale]/about/page.tsx b/src/app/[locale]/about/page.tsx index 8c0c6753..ae7087c4 100644 --- a/src/app/[locale]/about/page.tsx +++ b/src/app/[locale]/about/page.tsx @@ -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 { + 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(siteSettingsQuery), + sanityFetch(timelineQuery), + ]); + + const values = t.raw("values") as { title: string; desc: string }[]; + const focusAreas = t.raw("focus_areas") as string[]; + return ( -
- // about -

درباره علی تقوی

-

در حال آماده‌سازی — Phase 3

+
+ + + {/* Hero */} + +
+
+

{t("title")}

+
+ {t("bio").split("\n\n").map((paragraph, i) => ( +

+ {paragraph} +

+ ))} +
+
+ {socialLinks.map(({ icon: Icon, href, label }) => ( + + + + ))} +
+
+ + {/* Current status card */} + {settings?.currentStatus && ( +
+
+
// {t("working_on")}
+

{settings.currentStatus}

+
+ + active +
+
+
+ )} +
+
+ + {/* Values */} + + + + + + {/* Focus Areas */} + + + + + + {/* Timeline */} + + + [0]["events"]} /> +
); } diff --git a/src/app/[locale]/contact/page.tsx b/src/app/[locale]/contact/page.tsx index b35d9f50..d47ef403 100644 --- a/src/app/[locale]/contact/page.tsx +++ b/src/app/[locale]/contact/page.tsx @@ -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 { + 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 ( -
- // contact -

تماس

-

در حال آماده‌سازی — Phase 3

+
+ + +
+ {/* Form */} + +

{t("title")}

+

{t("subtitle")}

+ +
+ + {/* Sidebar info */} + + {/* Open to */} +
+
// {t("open_to")}
+
    + {openToItems.map((item, i) => ( +
  • + + {item} +
  • + ))} +
+
+ + {/* Location */} +
+ + {t("based_in")} +
+ + {/* Social */} +
+
// social
+
+ {socialLinks.map(({ icon: Icon, href, label }) => ( + + + + ))} +
+
+ + {/* Telegram CTA */} +
+
+
+ +
+

+ {locale === "fa" + ? "برای یادداشت‌های روزانه، ایده‌ها و آپدیت‌ها، کانال تلگرامم را دنبال کن" + : "For daily notes, ideas, and updates, follow my Telegram channel"} +

+ +
+ +
); } diff --git a/src/app/[locale]/error.tsx b/src/app/[locale]/error.tsx new file mode 100644 index 00000000..52b35abb --- /dev/null +++ b/src/app/[locale]/error.tsx @@ -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 ( +
+ +
500
+
// {t("error.label")}
+

{t("error.title")}

+

{t("error.description")}

+
+ + +
+ {error.digest && ( +

digest: {error.digest}

+ )} +
+
+ ); +} diff --git a/src/app/[locale]/layout.tsx b/src/app/[locale]/layout.tsx index 8c12982f..1e9b3f0f 100644 --- a/src/app/[locale]/layout.tsx +++ b/src/app/[locale]/layout.tsx @@ -85,9 +85,15 @@ export default async function LocaleLayout({ + + {dir === "rtl" ? "رفتن به محتوای اصلی" : "Skip to main content"} +
-
{children}
+
{children}