diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..ebcd2ca --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,75 @@ +# Changelog + +All notable changes to this project are documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), +and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + +## [0.3.0] - 2026-07-02 + +### Added + +- Version History drawer opened from the footer version label. +- Categorized release notes UI with Added, Improved, and Fixed sections. +- Current version indicator badge on the latest release card. +- Hardcoded release data (`lib/changelog-releases.ts`) for the drawer. + +### Changed + +- Refined drawer layout, card typography, and spacing between release sections. +- Improved release date formatting and left-aligned changelog text. + +### Fixed + +- Empty version descriptions caused by client-side changelog file parsing. +- Scrolling issues inside the version history drawer. +- Server-only `fs` import errors by loading changelog data in the root layout. + +## [0.2.0] - 2026-07-02 + +### Added + +- Introduced a site-wide typography scale (`type.h1`, `type.body`, `type.nav`, etc.) so headings, paragraphs, and navigation look consistent across all pages. +- Built new blog blockquote templates with yellow, green, and gray color variants — side accent bar, quote icon, soft background, and decorative shapes. +- Added pullquote styling for centered highlight quotes with a larger type size and icon above the text. +- Shipped `BlogTable` with two visual themes (green and yellow): glass-style header, zebra rows, row hover, bold first column, and horizontal scroll on small screens. +- Added centralized `.blog-content` styles for article prose — heading sizes, list spacing, link colors, `my-6` image margins, rounded image shadows, and `scroll-mt-24` for TOC anchor jumps. + +### Changed + +- Replaced home page hero images with updated `1.jpg` / `1dark.jpg` assets; synced horizontal gutters and max-width with the header bar. +- Swapped all four features-accordion preview images (integration, panels, social, automation) plus their dark-mode variants. +- Replaced the ecosystem section image with `intelligent-workflow-control-center` light/dark JPEGs and a fixed `aspect-[3/2]` frame. +- Refactored the single blog post page: removed long inline Tailwind classes from the article element and moved prose rules into `globals.css`. +- Wired pullquote and WordPress table blocks into the post content renderer — they were previously ignored. +- Redesigned the site header: centered desktop navigation, mobile drawer next to the theme toggle, and width aligned with the hero container. +- Updated hero CTA and integrations links from placeholder `#` anchors to `/solutions`. + +### Fixed + +- WordPress pullquotes did not render on single post pages. +- Blockquote HTML from WordPress could contain unsafe markup — content is now sanitized before display. +- Background page kept scrolling while the mobile navigation menu was open. +- Header bar width and padding did not match page content at several breakpoints. +- Related posts section crashed the entire post page when the WordPress API returned an error. +- Featured image aspect ratio and top spacing were wrong on mobile blog posts. +- SEO metadata, Open Graph tags, 404 layout, and crawler-facing text on key pages. +- Placeholder links on the home page integrations strip pointed nowhere. + +### Removed + +- Deleted obsolete home page image files: `Main_pic.jpg`, `Main_pic-min.jpg`, `Untitled-1 copy-min.jpg`, `main-pic.jpg`. + +## [0.1.0] - 2026-06-01 + +### Added + +- Launched the Feep marketing site with Persian RTL layout and core pages: home, about, solutions, consulting, and blog. +- Connected headless WordPress: post listing, single article view, featured images, and related posts. +- Built blog reading tools: table of contents, reading time estimate, scroll-to-top button, and scroll progress. +- Implemented custom WordPress block rendering: accordion (collapse), blockquote, and tip components. +- Added a featured posts grid on the blog index and Docker standalone build configuration. + +[0.3.0]: https://github.com/feep/next-ctrlp/compare/v0.2.0...v0.3.0 +[0.2.0]: https://github.com/feep/next-ctrlp/compare/v0.1.0...v0.2.0 +[0.1.0]: https://github.com/feep/next-ctrlp/releases/tag/v0.1.0 diff --git a/app/about/page.tsx b/app/about/page.tsx index 0102172..0ae987d 100644 --- a/app/about/page.tsx +++ b/app/about/page.tsx @@ -5,6 +5,8 @@ export const metadata = { }; import { Button } from "@/components/ui/button"; import Link from "next/link"; +import { type, typeSpacing } from "@/lib/typography"; +import { cn } from "@/lib/utils"; export default function AboutPage() { return ( @@ -12,11 +14,11 @@ export default function AboutPage() { {/* Hero Section */}
-
-

+
+

درباره ما

-

+

ما یک گروه متخصص هستیم که در حوزه اینتگریشن و روش‌های نوین یکپارچه‌سازی فعالیت می‌کنیم. هدف ما ارائه راهکارهای هوشمند و کارآمد برای اتصال سیستم‌ها، داده‌ها و فرآیندهای کسب‌وکار است.

@@ -29,16 +31,16 @@ export default function AboutPage() {
-

+

داستان ما

-
-

+

+

فیپ یک گروه متخصص است که در حوزه اینتگریشن و روش‌های نوین یکپارچه‌سازی فعالیت می‌کند. ما با استفاده از تکنولوژی‌های پیشرفته و رویکردهای نوآورانه، به کسب‌وکارها کمک می‌کنیم تا سیستم‌های خود را به شکلی کارآمد و قابل‌اعتماد بهم متصل کنند.

-

+

ما در زمینه یکپارچه‌سازی سیستم‌ها، اتوماسیون فرآیندها، ارتباطات چندکاناله و مدیریت داده‌ها تخصص داریم. با استفاده از روش‌های نوین و ابزارهای پیشرفته، راهکارهای سفارشی برای هر کسب‌وکار طراحی و پیاده‌سازی می‌کنیم. @@ -47,8 +49,8 @@ export default function AboutPage() {

-
۱۰+
-
سال تجربه
+
۱۰+
+
سال تجربه
@@ -61,10 +63,10 @@ export default function AboutPage() {
-

+

ماموریت ما

-

+

ماموریت ما این است که با استفاده از روش‌های نوین اینتگریشن، به کسب‌وکارها کمک کنیم تا بدون پیچیدگی‌های فنی، سیستم‌های خود را بهم متصل کنند و از قدرت اتوماسیون و ارتباطات هوشمند بهره‌مند شوند. @@ -73,10 +75,10 @@ export default function AboutPage() {

-

+

چشم‌انداز ما

-

+

ما می‌خواهیم به گروه پیشرو در حوزه اینتگریشن و روش‌های نوین یکپارچه‌سازی تبدیل شویم. چشم‌انداز ما این است که هر کسب‌وکاری، از کوچک تا بزرگ، بتواند به راحتی از خدمات ما استفاده کند و به اهداف خود برسد. @@ -92,35 +94,35 @@ export default function AboutPage() {

-

+

ارزش‌های ما

-

+

ارزش‌هایی که در تمام کارهای ما رعایت می‌شود

-

کیفیت

-

+

کیفیت

+

تعهد به کیفیت و نوآوری در تمام پروژه‌ها و خدمات

-

شفافیت

-

+

شفافیت

+

شفافیت کامل در ارتباطات، قیمت‌گذاری و فرآیندها

-

مشتری‌محوری

-

+

مشتری‌محوری

+

تمرکز بر نیازهای مشتری و رضایت کامل آن‌ها

-

نوآوری

-

+

نوآوری

+

استفاده از آخرین تکنولوژی‌ها و روش‌های نوین

@@ -134,10 +136,10 @@ export default function AboutPage() {
-

+

چرا فیپ را انتخاب کنید؟

-

+

مزایای رقابتی که ما را از دیگران متمایز می‌کند

@@ -150,8 +152,8 @@ export default function AboutPage() {
-

سادگی

-

+

سادگی

+

رابط کاربری ساده و قابل فهم که نیاز به تخصص فنی ندارد

@@ -163,8 +165,8 @@ export default function AboutPage() {
-

قابلیت اطمینان

-

+

قابلیت اطمینان

+

زیرساخت قدرتمند و پایدار که همیشه در دسترس است

@@ -176,8 +178,8 @@ export default function AboutPage() {
-

مقیاس‌پذیری

-

+

مقیاس‌پذیری

+

قابلیت رشد همراه با نیازهای کسب‌وکار شما

@@ -191,10 +193,10 @@ export default function AboutPage() {
-

+

تیم ما

-

+

تیم فیپ متشکل از متخصصان با تجربه در حوزه‌های اینتگریشن، فناوری اطلاعات، توسعه نرم‌افزار و مدیریت کسب‌وکار است. ما با همکاری یکدیگر و استفاده از روش‌های نوین، راهکارهای نوآورانه و کاربردی برای چالش‌های یکپارچه‌سازی ارائه می‌دهیم. @@ -204,33 +206,33 @@ export default function AboutPage() {

- ت + ت
-

تخصص

-

+

تخصص

+

متخصصان با تجربه در حوزه اینتگریشن و یکپارچه‌سازی

- ن + ن
-

نوآوری

-

+

نوآوری

+

استفاده از آخرین تکنولوژی‌ها و روش‌های نوین

- پ + پ
-

پشتیبانی

-

+

پشتیبانی

+

پشتیبانی کامل و مستمر در تمام مراحل پروژه

@@ -243,10 +245,10 @@ export default function AboutPage() {
-

+

آماده شروع هستید؟

-

+

با ما تماس بگیرید تا درباره نیازهای شما صحبت کنیم و بهترین راهکار را پیشنهاد دهیم

@@ -254,7 +256,7 @@ export default function AboutPage() { مشاهده قیمت‌ها
diff --git a/app/blog/[slug]/page.tsx b/app/blog/[slug]/page.tsx index 5f9575d..4a1972e 100644 --- a/app/blog/[slug]/page.tsx +++ b/app/blog/[slug]/page.tsx @@ -5,16 +5,19 @@ import { notFound } from "next/navigation"; import { getPostBySlug, getFeaturedImageUrl, getRelatedPosts } from "@/lib/wp-api"; import { formatDateShamsi } from "@/lib/format-date"; import { ChevronRight } from "lucide-react"; -import type { CollapseBlockPayload, BlockquoteBlockPayload, TipBlockPayload } from "@/lib/blog-blocks"; +import type { CollapseBlockPayload, BlockquoteBlockPayload, TipBlockPayload, TableBlockPayload } from "@/lib/blog-blocks"; import { parseContentHeadings, getReadingTimeMinutes, formatAuthorName } from "@/lib/blog-utils"; import { BlogToc } from "@/components/blog-toc"; import { BlogPostMeta } from "@/components/blog-post-meta"; import { BlogCollapseSection } from "@/components/blog-collapse-section"; import { BlogBlockquote } from "@/components/blog-blockquote"; import { BlogTip } from "@/components/blog-tip"; +import { BlogTable } from "@/components/blog-table"; import { BlogPullquote } from "@/components/blog-pullquote"; import { ScrollToTop } from "@/components/scroll-to-top"; import { BlogPostCard } from "@/components/blog-post-card"; +import { type, typeSpacing } from "@/lib/typography"; +import { cn } from "@/lib/utils"; interface BlogPostPageProps { params: Promise<{ slug: string }>; @@ -111,14 +114,14 @@ export default async function BlogPostPage({ params }: BlogPostPageProps) {

-
+
{segments.map((seg, i) => seg.type === "html" ? (
) : seg.blockType === "blockquote" ? ( + ) : seg.blockType === "pullquote" ? ( + ) : seg.blockType === "tip" ? ( + ) : seg.blockType === "table" ? ( + ) : ( null ) @@ -163,7 +170,7 @@ export default async function BlogPostPage({ params }: BlogPostPageProps) { {relatedPosts.length > 0 && (
-

+

مقالات مرتبط

diff --git a/app/blog/page.tsx b/app/blog/page.tsx index 46984a9..87f8de1 100644 --- a/app/blog/page.tsx +++ b/app/blog/page.tsx @@ -5,6 +5,8 @@ import { BlogPostCard } from "@/components/blog-post-card"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { ChevronRight, ChevronLeft, Search, X } from "lucide-react"; +import { type, typeSpacing } from "@/lib/typography"; +import { cn } from "@/lib/utils"; const PER_PAGE = 9; @@ -44,8 +46,8 @@ export default async function BlogPage({ searchParams }: BlogPageProps) { return (
-

بلاگ

-

+

بلاگ

+

در حال حاضر امکان بارگذاری مطالب وجود ندارد. لطفاً بعداً تلاش کنید.

@@ -72,10 +74,10 @@ export default async function BlogPage({ searchParams }: BlogPageProps) {
-

+

دانشنامه

-

+

مقالات و مطالب فیپ در زمینه یکپارچه‌سازی، اتوماسیون و بهره‌وری سازمانی.

diff --git a/app/consulting/page.tsx b/app/consulting/page.tsx index cd89985..ba75896 100644 --- a/app/consulting/page.tsx +++ b/app/consulting/page.tsx @@ -20,6 +20,8 @@ import { Quote, } from 'lucide-react' import Link from 'next/link' +import { type, typeSpacing } from '@/lib/typography' +import { cn } from '@/lib/utils' const consultants = [ { @@ -114,16 +116,16 @@ export default function ConsultingPage() {
-
-

+
+

مشاوره سازمانی

-

+

مشاوره سازمانی به معنای همراهی تخصصی با سازمان شما برای شناسایی چالش‌ها، انتخاب راهکارهای فنی مناسب و برنامه‌ریزی برای پیاده‌سازی است. ما در حوزه‌های اینتگریشن، هوشمندسازی، RAG و بهینه‌سازی عملکرد در کنار شما هستیم.

-

+

شرایط همکاری: پس از ثبت درخواست، تیم ما با شما تماس می‌گیرد؛ جلسه اول برای شناخت نیازها و تعیین دامنه کار است و در ادامه پیشنهاد و برنامه اجرا ارائه می‌شود. @@ -158,8 +160,8 @@ export default function ConsultingPage() {

-

در چه حوزه‌هایی مشاوره می‌دهیم؟

-

+

در چه حوزه‌هایی مشاوره می‌دهیم؟

+

از اینتگریشن و هوشمندسازی تا RAG و معماری نرم‌افزار

@@ -174,8 +176,8 @@ export default function ConsultingPage() {
-

{domain.title}

-

+

{domain.title}

+

{domain.description}

@@ -192,8 +194,8 @@ export default function ConsultingPage() {
-

چه کسانی مشاوره می‌دهند؟

-

+

چه کسانی مشاوره می‌دهند؟

+

تیم مشاوره ما متشکل از متخصصان با تجربه در حوزه‌های مختلف فناوری است.

@@ -202,21 +204,21 @@ export default function ConsultingPage() { -
+
{person.description}
- + {person.fallback}
-

{person.name}

-
+

{person.name}

+
{person.expertise.map((e) => ( - + {e} {e !== person.expertise[person.expertise.length - 1] ? ' · ' : ''} @@ -235,8 +237,8 @@ export default function ConsultingPage() {
-

سوالات متداول

-

+

سوالات متداول

+

پاسخ سوالات رایج درباره مشاوره سازمانی

@@ -247,11 +249,11 @@ export default function ConsultingPage() { value={item.id} className="rounded-xl border-none px-4 data-[state=open]:bg-muted/50 data-[state=open]:shadow-sm" > - + {item.question} -

{item.answer}

+

{item.answer}

))} @@ -264,8 +266,8 @@ export default function ConsultingPage() {
-

درخواست مشاوره

-

+

درخواست مشاوره

+

فرم زیر را پر کنید؛ در سریع‌ترین زمان ممکن با شما تماس می‌گیریم.

diff --git a/app/error.tsx b/app/error.tsx index ade1eb9..ed9a8bf 100644 --- a/app/error.tsx +++ b/app/error.tsx @@ -3,6 +3,8 @@ import { useEffect } from 'react' import Link from 'next/link' import { Button } from '@/components/ui/button' +import { type } from '@/lib/typography' +import { cn } from '@/lib/utils' export default function Error({ error, @@ -17,8 +19,8 @@ export default function Error({ return (
-

خطایی رخ داد

-

+

خطایی رخ داد

+

لطفاً دوباره تلاش کنید یا به صفحه اصلی برگردید.

diff --git a/app/global-not-found.tsx b/app/global-not-found.tsx index 3c19747..269fd17 100644 --- a/app/global-not-found.tsx +++ b/app/global-not-found.tsx @@ -1,5 +1,7 @@ import './globals.css' import Link from 'next/link' +import { type } from '@/lib/typography' +import { cn } from '@/lib/utils' export const metadata = { title: '۴۰۴ | صفحه یافت نشد', @@ -13,17 +15,17 @@ export default function GlobalNotFound() {
۴۰۴
-

+

صفحه پیدا نشد

-

+

به نظر می‌رسد این آدرس وجود ندارد یا منتقل شده است.

diff --git a/app/globals.css b/app/globals.css index 7e75f14..0285df3 100644 --- a/app/globals.css +++ b/app/globals.css @@ -80,8 +80,10 @@ @theme inline { --color-background: var(--background); --color-foreground: var(--foreground); - --font-sans: var(--font-geist-sans); - --font-mono: var(--font-geist-mono); + --font-sans: "IRANSansX", "system-ui", "Avenir", "Helvetica", "Arial", + "sans-serif"; + --font-mono: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, + monospace; --font-iran-sans: "IRANSansX", "system-ui", "Avenir", "Helvetica", "Arial", "sans-serif"; --color-sidebar-ring: var(--sidebar-ring); @@ -117,11 +119,13 @@ --radius-md: calc(var(--radius) - 2px); --radius-lg: var(--radius); --radius-xl: calc(var(--radius) + 4px); + --radius-blog: var(--radius-blog); } :root { - --font-iran-sans: "IRANSansX", var(--font-geist-sans); + --font-iran-sans: "IRANSansX", system-ui, sans-serif; --radius: 0.625rem; + --radius-blog: 0.9375rem; --card: oklch(1 0.006 175); --card-foreground: oklch(0.145 0.02 175); --popover: oklch(1 0.006 175); @@ -233,46 +237,98 @@ } -/* WordPress table block (figure.wp-block-table > table) */ -.wp-block-table { - margin: 1.5rem 0; - overflow-x: auto; - border-radius: var(--radius); +/* Blog table blocks (figure.blog-table) */ +.blog-table { + position: relative; + width: 100%; + margin-inline: 0; + padding: 0; } -.wp-block-table table { +.blog-table__scroll { + overflow-x: auto; + -webkit-overflow-scrolling: touch; + margin: 0; + padding: 0; +} + +.blog-table__grid { width: 100%; border-collapse: collapse; font-size: 0.9375rem; - + line-height: 1.6; } -.wp-block-table table.has-fixed-layout { - table-layout: fixed; -} - -.wp-block-table th, -.wp-block-table td { - padding: 0.75rem 1rem; - border: 1px solid var(--border); +.blog-table__grid th, +.blog-table__grid td { + padding: 0.875rem 1rem; text-align: right; - min-width: 850px; + vertical-align: top; } -.wp-block-table tbody tr:first-child th, -.wp-block-table tbody tr:first-child td { - background: var(--muted); - font-weight: 600; +.blog-table__grid th p, +.blog-table__grid td p { + margin: 0; +} + +.blog-table__caption { + margin-top: 0.75rem; + text-align: center; + font-size: 0.8125rem; + color: var(--muted-foreground); +} + +.blog-table__grid a { + color: var(--primary); + text-decoration: underline; +} + +.blog-table__grid a:hover { + text-decoration: none; +} + +/* Variant one (green) & two (yellow) — glass layout aligned with HeroHeader */ +.blog-table--one { + --table-tone: var(--primary); +} + +.blog-table--two { + --table-tone: var(--accent); +} + +.blog-table--one .blog-table__grid thead th, +.blog-table--two .blog-table__grid thead th { + background: color-mix(in srgb, var(--table-tone) 35%, transparent); + backdrop-filter: blur(12px); + -webkit-backdrop-filter: blur(12px); color: var(--foreground); -} - -.wp-block-table tbody tr:not(:first-child):hover td { - background: var(--muted); -} - -.wp-block-table thead th { - background: var(--muted); font-weight: 600; + border-bottom: 1px solid color-mix(in srgb, var(--border) 10%, transparent); +} + +.blog-table--one .blog-table__grid tbody tr, +.blog-table--two .blog-table__grid tbody tr { + transition: background-color 0.3s ease; +} + +.blog-table--one .blog-table__grid tbody tr:nth-child(even) td, +.blog-table--two .blog-table__grid tbody tr:nth-child(even) td { + background: color-mix(in srgb, var(--background) 20%, transparent); +} + +.blog-table--one .blog-table__grid tbody tr:hover td, +.blog-table--two .blog-table__grid tbody tr:hover td { + background: color-mix(in srgb, var(--table-tone) 12%, transparent); +} + +.blog-table--one .blog-table__grid tbody td, +.blog-table--two .blog-table__grid tbody td { + border-top: 1px solid color-mix(in srgb, var(--border) 10%, transparent); +} + +.blog-table--one .blog-table__grid tbody td:first-child, +.blog-table--two .blog-table__grid tbody td:first-child { + font-weight: 500; color: var(--foreground); } @@ -294,10 +350,217 @@ } .wp-block-image { - @apply rounded-md overflow-hidden shadow-sm; + @apply rounded-blog overflow-hidden shadow-sm; +} + +.blog-content .wp-block-image { + @apply my-6; } .blog-content img { @apply w-full h-auto; image-rendering: auto; +} + +/* Blog prose typography — single source of truth */ +.blog-content { + @apply space-y-6 text-foreground; +} + +.blog-content h2 { + @apply mt-8 mb-4 text-2xl font-semibold leading-snug tracking-normal scroll-mt-24; +} + +.blog-content h3 { + @apply mt-6 mb-3 text-xl font-semibold leading-snug tracking-normal scroll-mt-24; +} + +.blog-content h4 { + @apply mt-4 mb-2 text-lg font-semibold leading-snug tracking-normal scroll-mt-24; +} + +.blog-content p { + @apply leading-relaxed tracking-normal text-muted-foreground; +} + +.blog-content a { + @apply text-primary no-underline hover:underline; +} + +.blog-content ul { + @apply list-disc pr-6; +} + +.blog-content ol { + @apply list-decimal pr-6; +} + +.blog-content li { + @apply my-1; +} + +/* Blog blockquote variants */ +.blog-blockquote { + position: relative; + overflow: hidden; + border-radius: var(--radius-blog); + border: 1px solid; + padding-block: 1.5rem; + padding-inline-end: 1.75rem; + padding-inline-start: 4rem; + box-shadow: 0 1px 2px rgb(0 0 0 / 0.05); +} + +.blog-blockquote::before { + content: ""; + position: absolute; + inset-block: 1rem; + inset-inline-start: 0; + width: 4px; + border-radius: 9999px; +} + +.blog-blockquote--yellow { + border-color: color-mix(in srgb, var(--accent) 45%, var(--border)); + background: color-mix(in srgb, var(--accent) 18%, var(--background)); + box-shadow: 0 2px 8px color-mix(in srgb, var(--accent) 14%, transparent); +} + +.blog-blockquote--yellow::before { + background: var(--accent); +} + +.blog-blockquote--yellow .blog-blockquote__icon { + color: color-mix(in srgb, var(--accent) 55%, transparent); +} + +.blog-blockquote--yellow .blog-blockquote__footer { + border-top-color: color-mix(in srgb, var(--accent) 28%, var(--border)); +} + +.blog-blockquote--green { + border-color: color-mix(in srgb, var(--primary) 45%, var(--border)); + background: color-mix(in srgb, var(--primary) 12%, var(--background)); + box-shadow: 0 2px 8px color-mix(in srgb, var(--primary) 12%, transparent); +} + +.blog-blockquote--green::before { + background: var(--primary); +} + +.blog-blockquote--green .blog-blockquote__icon { + color: color-mix(in srgb, var(--primary) 50%, transparent); +} + +.blog-blockquote--green .blog-blockquote__footer { + border-top-color: color-mix(in srgb, var(--primary) 28%, var(--border)); +} + +.blog-blockquote--gray { + border-color: var(--border); + background: color-mix(in srgb, var(--muted) 65%, var(--background)); +} + +.blog-blockquote--gray::before { + background: color-mix(in srgb, var(--muted-foreground) 38%, transparent); +} + +.blog-blockquote--gray .blog-blockquote__icon { + color: color-mix(in srgb, var(--muted-foreground) 42%, transparent); +} + +.blog-blockquote--gray .blog-blockquote__footer { + border-top-color: color-mix(in srgb, var(--border) 80%, transparent); +} + +.blog-blockquote__icon { + position: absolute; + inset-inline-start: 1.25rem; + top: 1.5rem; + width: 2rem; + height: 2rem; + flex-shrink: 0; + z-index: 1; +} + +.blog-blockquote__decorations { + position: absolute; + inset: 0; + width: 100%; + height: 100%; + pointer-events: none; + z-index: 0; + opacity: 0.15; + /* Keep decorative shapes out of the quote-icon corner (inline-start). */ + clip-path: polygon( + 0 0, + calc(100% - 4.75rem) 0, + calc(100% - 4.75rem) 4.75rem, + 100% 4.75rem, + 100% 100%, + 0 100% + ); +} + +:dir(ltr) .blog-blockquote__decorations { + clip-path: polygon( + 4.75rem 0, + 100% 0, + 100% 100%, + 0 100%, + 0 4.75rem, + 4.75rem 4.75rem + ); +} + +.blog-pullquote .blog-blockquote__decorations { + clip-path: none; +} + +.blog-blockquote__content { + position: relative; + z-index: 1; + isolation: isolate; + font-size: 1.125rem; + line-height: 1.75; + text-align: justify; +} + +.blog-blockquote__content p { + margin-block: 0.5rem; +} + +.blog-blockquote__content a { + color: var(--accent); + text-decoration: underline; +} + +.blog-blockquote__content a:hover { + text-decoration: none; +} + +.blog-blockquote__footer { + position: relative; + z-index: 1; + margin-top: 1rem; + padding-top: 1rem; + border-top: 1px solid; +} + +.blog-pullquote { + text-align: center; + padding-inline: 1.5rem; +} + +.blog-pullquote .blog-blockquote__icon { + position: static; + margin-inline: auto; + margin-bottom: 0.75rem; + width: 1.75rem; + height: 1.75rem; +} + +.blog-pullquote .blog-blockquote__content { + font-size: 1.25rem; + font-weight: 500; } \ No newline at end of file diff --git a/app/layout.tsx b/app/layout.tsx index 17f4de1..eea32a7 100644 --- a/app/layout.tsx +++ b/app/layout.tsx @@ -3,6 +3,7 @@ import "./globals.css"; import { ConditionalLayout } from "@/components/conditional-layout"; import { ThemeProvider } from "@/components/theme-provider"; import { TopProgressBar } from "@/components/top-progress-bar"; +import { getChangelog } from "@/lib/changelog.server"; const SITE_NAME = "فیپ"; @@ -25,6 +26,8 @@ export default function RootLayout({ }: Readonly<{ children: React.ReactNode; }>) { + const changelogReleases = getChangelog(); + return ( - {children} + + {children} + diff --git a/app/not-found.tsx b/app/not-found.tsx index 25ca37e..5cc18cb 100644 --- a/app/not-found.tsx +++ b/app/not-found.tsx @@ -1,6 +1,8 @@ import Link from 'next/link' import { Button } from '@/components/ui/button' import { Home } from 'lucide-react' +import { type, typeSpacing } from '@/lib/typography' +import { cn } from '@/lib/utils' export const metadata = { title: '۴۰۴', @@ -13,17 +15,17 @@ export default function NotFound() {
۴۰۴
-

+

صفحه پیدا نشد

-

+

به نظر می‌رسد این آدرس وجود ندارد یا منتقل شده است.

diff --git a/app/playground/page.tsx b/app/playground/page.tsx new file mode 100644 index 0000000..b69d6d0 --- /dev/null +++ b/app/playground/page.tsx @@ -0,0 +1,36 @@ +import type { Metadata } from "next"; +import { PlaygroundGuide } from "@/components/playground/playground-guide"; +import { PlaygroundWorkspace } from "@/components/playground/playground-workspace"; +import { type } from "@/lib/typography"; +import { cn } from "@/lib/utils"; + +export const metadata: Metadata = { + title: "آزمایشگاه فیپ", + description: + "آزمایشگاه فیپ برای تست سناریوهای تعاملی، پرامپت‌ها و تجربه‌های مبتنی بر هوش مصنوعی.", +}; + +export default function PlaygroundPage() { + return ( + <> +
+
+

+ آزمایشگاه فیپ +

+

+ آزمایش تعامل با دستیار، پرامپت‌ها و سناریوهای هوش مصنوعی +

+
+
+ +
+
+ +
+
+ + + + ); +} diff --git a/app/solutions/page.tsx b/app/solutions/page.tsx index cf54cde..23b5ae9 100644 --- a/app/solutions/page.tsx +++ b/app/solutions/page.tsx @@ -2,6 +2,8 @@ import type { Metadata } from "next"; import { Button } from "@/components/ui/button"; import { Link as LinkIcon, BarChart3, Share2, Zap, TrendingUp, Workflow } from "lucide-react"; import Link from "next/link"; +import { type, typeSpacing } from "@/lib/typography"; +import { cn } from "@/lib/utils"; export const metadata: Metadata = { title: "راه حل‌ها", @@ -90,14 +92,14 @@ export default function SolutionsPage() { {/* Hero Section */}
-
-

+
+

راه‌حل‌های ما

-

+

در دنیای امروز، کسب‌و‌کارهای موفق دیگر نمی‌توانند سیستم‌ها، ابزارها و داده‌هایشان را جدا از هم مدیریت کنند. حجم اطلاعات، تعداد ابزارهای مختلف و نیاز به واکنش سریع باعث شده تا یکپارچه‌سازی واقعی سیستم‌ها دیگر تنها یک انتخاب نباشد — بلکه یک ضرورت باشد.

-

+

راهکارهایی که اینجا معرفی می‌شود، فراتر از تکنولوژی‌های معمول است. هدف اصلی این نیست که فقط چند سرویس را به هم وصل کنیم، بلکه این است که فرایندهای کسب‌و‌کار شما را هوشمند، کارا و قابل کنترل‌تر کنیم.

@@ -119,18 +121,18 @@ export default function SolutionsPage() {
-

+

{index + 1}. {solution.title}

-

+

{solution.description}

    {solution.features.map((feature, featureIndex) => (
  • {feature} @@ -138,7 +140,7 @@ export default function SolutionsPage() { ))}
{solution.result && ( -

+

{solution.result}

)} @@ -153,11 +155,11 @@ export default function SolutionsPage() {
-

نتیجه‌گیری

-

+

نتیجه‌گیری

+

این راهکارها کمک می‌کنند تا کسب‌و‌کار شما از حالت «جزیره‌ای» بیرون بیاید و به یک ساختار هوشمند، به‌هم‌پیوسته و قابل رشد تبدیل شود.

-

+

دیگر لازم نیست زمان زیادی صرف انتقال داده، گزارش‌گیری دستی یا تکرار فعالیت‌ها بکنید؛ بلکه می‌توانید به‌طور هوشمندانه‌تر روی اهداف استراتژیک خود تمرکز کنید و رشد سریع‌تر را تجربه کنید.

@@ -168,10 +170,10 @@ export default function SolutionsPage() {
-

+

آماده شروع هستید؟

-

+

همین حالا شروع کنید و از قدرت یکپارچه‌سازی سیستم‌ها، اتوماسیون فرآیندها و تحلیل داده‌ها بهره‌مند شوید

diff --git a/components/blog-blockquote-decorations.tsx b/components/blog-blockquote-decorations.tsx new file mode 100644 index 0000000..2430aa1 --- /dev/null +++ b/components/blog-blockquote-decorations.tsx @@ -0,0 +1,248 @@ +import type { BlockquoteVariant } from "@/lib/blog-blocks"; +import type { ReactNode } from "react"; + +const STROKE = 0.9; +const BASE = 12; + +type ShapeType = + | "cross" + | "triangle" + | "dot9" + | "square" + | "diamond" + | "dot6" + | "circle"; + +type LayoutEntry = { + x: number; + y: number; + color: string; + type: ShapeType; + /** 0.2 = کوچک … 0.55 = بزرگ‌تر */ + scale: number; +}; + +type DecorShape = { + x: number; + y: number; + color: string; + node: ReactNode; +}; + +function unit(scale: number) { + return BASE * scale; +} + +function hollowCircle(r: number) { + return ( + + ); +} + +function triangle(size: number) { + return ( + + ); +} + +function cross(size: number) { + return ( + <> + + + + ); +} + +function square(size: number) { + return ( + + ); +} + +function diamond(size: number) { + return ( + + ); +} + +function dotPattern(cols: number, rows: number, gap: number, dotR: number) { + const dots: ReactNode[] = []; + const offsetX = ((cols - 1) * gap) / 2; + const offsetY = ((rows - 1) * gap) / 2; + + for (let row = 0; row < rows; row++) { + for (let col = 0; col < cols; col++) { + dots.push( + + ); + } + } + + return <>{dots}; +} + +function buildShape(type: ShapeType, scale: number): ReactNode { + const u = unit(scale); + + switch (type) { + case "circle": + return hollowCircle(u * 0.85); + case "cross": + return cross(u * 0.78); + case "triangle": + return triangle(u * 0.82); + case "square": + return square(u * 0.72); + case "diamond": + return diamond(u * 0.68); + case "dot9": + return dotPattern(3, 3, u * 0.26, u * 0.075); + case "dot6": + return dotPattern(3, 2, u * 0.3, u * 0.085); + } +} + +function toDecorShapes(entries: LayoutEntry[]): DecorShape[] { + return entries.map((entry) => ({ + x: entry.x, + y: entry.y, + color: entry.color, + node: buildShape(entry.type, entry.scale), + })); +} + +const SHAPE_ORDER: ShapeType[] = [ + "cross", + "triangle", + "dot9", + "square", + "diamond", + "dot6", + "circle", +]; + +function swapTypes(entries: LayoutEntry[], offset: number): LayoutEntry[] { + return entries.map((entry) => { + const index = SHAPE_ORDER.indexOf(entry.type); + return { + ...entry, + type: SHAPE_ORDER[(index + offset) % SHAPE_ORDER.length], + }; + }); +} + +/** چیدمان پایه (#2 سبز) — موقعیت، اندازه و رنگ */ +const layoutGreenBase: LayoutEntry[] = [ + { x: 42, y: 11, color: "var(--primary)", type: "diamond", scale: 0.38 }, + { x: 44, y: 12, color: "var(--accent)", type: "circle", scale: 0.3 }, + { x: 72, y: 10, color: "var(--secondary)", type: "dot6", scale: 0.4 }, + { x: 104, y: 13, color: "var(--muted-foreground)", type: "cross", scale: 0.32 }, + { x: 149, y: 11, color: "var(--primary)", type: "dot9", scale: 0.36 }, + { x: 186, y: 12, color: "var(--accent)", type: "triangle", scale: 0.42 }, + { x: 198, y: 28, color: "var(--secondary)", type: "square", scale: 0.34 }, + + { x: 40, y: 27, color: "var(--accent)", type: "triangle", scale: 0.36 }, + { x: 38, y: 47, color: "var(--muted-foreground)", type: "dot9", scale: 0.32 }, + { x: 41, y: 66, color: "var(--primary)", type: "circle", scale: 0.28 }, + { x: 39, y: 87, color: "var(--secondary)", type: "cross", scale: 0.38 }, + { x: 42, y: 105, color: "var(--accent)", type: "diamond", scale: 0.33 }, + + { x: 200, y: 48, color: "var(--primary)", type: "square", scale: 0.36 }, + { x: 203, y: 62, color: "var(--secondary)", type: "dot6", scale: 0.42 }, + { x: 198, y: 78, color: "var(--muted-foreground)", type: "cross", scale: 0.26 }, + { x: 201, y: 92, color: "var(--accent)", type: "dot9", scale: 0.35 }, + + { x: 28, y: 109, color: "var(--secondary)", type: "cross", scale: 0.3 }, + { x: 68, y: 108, color: "var(--primary)", type: "square", scale: 0.38 }, + { x: 128, y: 109, color: "var(--accent)", type: "dot6", scale: 0.34 }, + { x: 183, y: 107, color: "var(--muted-foreground)", type: "triangle", scale: 0.28 }, + { x: 205, y: 109, color: "var(--primary)", type: "circle", scale: 0.4 }, + + { x: 81, y: 41, color: "var(--primary)", type: "circle", scale: 0.22 }, + { x: 167, y: 78, color: "var(--secondary)", type: "triangle", scale: 0.26 }, +]; + +const layoutGreen = toDecorShapes(layoutGreenBase); +const layoutYellow = toDecorShapes(swapTypes(layoutGreenBase, 2)); +const layoutGray = toDecorShapes(swapTypes(layoutGreenBase, 4)); + +const DECORATION_LAYOUTS: Record = { + yellow: layoutYellow, + green: layoutGreen, + gray: layoutGray, +}; + +interface BlogBlockquoteDecorationsProps { + variant: BlockquoteVariant; +} + +export function BlogBlockquoteDecorations({ + variant, +}: BlogBlockquoteDecorationsProps) { + const shapes = DECORATION_LAYOUTS[variant]; + + return ( + + {shapes.map((shape, index) => ( + + {shape.node} + + ))} + + ); +} diff --git a/components/blog-blockquote-styles.ts b/components/blog-blockquote-styles.ts new file mode 100644 index 0000000..d65d733 --- /dev/null +++ b/components/blog-blockquote-styles.ts @@ -0,0 +1,7 @@ +import type { BlockquoteVariant } from "@/lib/blog-blocks"; + +export const blockquoteVariantClass: Record = { + yellow: "blog-blockquote--yellow", + green: "blog-blockquote--green", + gray: "blog-blockquote--gray", +}; diff --git a/components/blog-blockquote.tsx b/components/blog-blockquote.tsx index c16eb62..f3351e4 100644 --- a/components/blog-blockquote.tsx +++ b/components/blog-blockquote.tsx @@ -1,6 +1,8 @@ -"use client"; - import type { BlockquoteBlockPayload } from "@/lib/blog-blocks"; +import { sanitizeBlockquoteContent } from "@/lib/blockquote-content"; +import { blockquoteVariantClass } from "@/components/blog-blockquote-styles"; +import { BlogBlockquoteDecorations } from "@/components/blog-blockquote-decorations"; +import { cn } from "@/lib/utils"; import { Quote } from "lucide-react"; interface BlogBlockquoteProps { @@ -8,22 +10,23 @@ interface BlogBlockquoteProps { } export function BlogBlockquote({ payload }: BlogBlockquoteProps) { - const { content, citation } = payload; - if (!content.trim()) return null; + const { content, citation, variant = "gray" } = payload; + const cleanContent = sanitizeBlockquoteContent(content); + if (!cleanContent.trim()) return null; return (
-
- +
+ +
{citation ? ( -