This commit is contained in:
alirez.br
2026-06-17 21:35:24 +03:30
27 changed files with 2649 additions and 196 deletions
+1
View File
@@ -0,0 +1 @@
{}
+165
View File
@@ -0,0 +1,165 @@
import type { Metadata } from "next";
import Link from "next/link";
import Image from "next/image";
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 { 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 { BlogPullquote } from "@/components/blog-pullquote";
import { ScrollToTop } from "@/components/scroll-to-top";
import { BlogPostCard } from "@/components/blog-post-card";
interface BlogPostPageProps {
params: Promise<{ slug: string }>;
}
export async function generateMetadata({
params,
}: BlogPostPageProps): Promise<Metadata> {
const { slug } = await params;
const post = await getPostBySlug(slug);
if (!post) return { title: "پست یافت نشد" };
const title = post.title?.rendered ?? "پست";
const excerpt = post.excerpt?.rendered?.replace(/<[^>]*>/g, "").trim().slice(0, 160) ?? "";
return {
title,
description: excerpt,
};
}
export default async function BlogPostPage({ params }: BlogPostPageProps) {
const { slug } = await params;
let post;
try {
post = await getPostBySlug(slug);
} catch {
notFound();
}
if (!post) notFound();
let relatedPosts: Awaited<ReturnType<typeof getRelatedPosts>> = [];
try {
relatedPosts = await getRelatedPosts(post.id, post.categories ?? [], 3);
} catch {
// show no related section on API error
}
const date = post.date ? formatDateShamsi(post.date) : null;
const modifiedDate = post.modified
? formatDateShamsi(post.modified)
: null;
const authorName = formatAuthorName(post._embedded?.author?.[0]?.name);
const thumbnailUrl = getFeaturedImageUrl(post, "full");
const media = post._embedded?.["wp:featuredmedia"]?.[0];
const altText = media?.alt_text || post.title?.rendered?.replace(/<[^>]*>/g, "") || "";
const { toc, segments } = parseContentHeadings(post.content.rendered);
const readingTimeMinutes = getReadingTimeMinutes(post.content.rendered);
const postTitle = post.title?.rendered?.replace(/<[^>]*>/g, "") ?? "";
const postPath = `/blog/${slug}`;
return (
<>
{/* تصویر شاخص تمام‌عرض بالای صفحه */}
{thumbnailUrl && (
<div className="pt-13 md:pt-24">
<div className="relative container mx-auto max-w-5xl md:rounded-xl overflow-hidden shadow-sm w-full aspect-[5/2] min-h-[10rem] md:min-h-[10rem] bg-muted">
<Image
src={thumbnailUrl}
alt={altText}
fill
className="object-cover"
quality={100}
priority
sizes="(max-width: 768px) 100vw, 1024px"
/>
</div>
</div>
)}
<section className="bg-background pt-8 pb-20 md:pt-12 md:pb-28">
<div className="mx-auto max-w-6xl px-6">
{/* Single grid so sticky TOC stays for whole page scroll */}
<div className="lg:grid lg:grid-cols-[16rem_1fr] lg:gap-10">
<BlogToc items={toc} className="mb-6 lg:mb-0" />
<div className="min-w-0 space-y-0">
<div className="pb-8 md:pb-12">
<nav className="mb-6" aria-label="مسیر">
<Link
href="/blog"
className="text-muted-foreground hover:text-foreground text-sm inline-flex items-center gap-1 transition-colors"
>
<ChevronRight className="size-4" aria-hidden />
بلاگ
</Link>
</nav>
<h1
className="text-2xl font-bold md:text-3xl leading-tight tracking-normal"
dangerouslySetInnerHTML={{ __html: post.title.rendered }}
/>
<BlogPostMeta
url={postPath}
title={postTitle}
readingTimeMinutes={readingTimeMinutes}
date={date}
dateTime={post.date}
modifiedDate={modifiedDate}
authorName={authorName}
className="mt-4"
/>
</div>
<article className="blog-content text-justify space-y-6 text-foreground [&_h2]:text-2xl [&_h2]:font-bold [&_h2]:mt-8 [&_h2]:mb-4 [&_h3]:text-xl [&_h3]:font-bold [&_h3]:mt-6 [&_h3]:mb-3 [&_p]:leading-relaxed [&_p]:text-muted-foreground [&_a]:text-primary [&_a]:no-underline hover:[&_a]:underline [&_ul]:list-disc [&_ul]:pr-6 [&_ol]:list-decimal [&_ol]:pr-6 [&_li]:my-1 [&_h2]:scroll-mt-24 [&_h3]:scroll-mt-24 [&_h4]:scroll-mt-24">
{segments.map((seg, i) =>
seg.type === "html" ? (
<div
key={i}
dangerouslySetInnerHTML={{ __html: seg.content }}
/>
) : seg.blockType === "collapse" ? (
<BlogCollapseSection
key={i}
section={seg.payload as CollapseBlockPayload}
idPrefix={`blog-${slug}-${i}`}
/>
) : seg.blockType === "blockquote" ? (
<BlogBlockquote key={i} payload={seg.payload as BlockquoteBlockPayload} />
) : seg.blockType === "tip" ? (
<BlogTip key={i} payload={seg.payload as TipBlockPayload} />
) : (
null
)
)}
</article>
</div>
</div>
</div>
</section>
{relatedPosts.length > 0 && (
<section className="bg-background border-t pb-16 md:pb-20">
<div className="mx-auto max-w-6xl px-6 pt-10 md:pt-14">
<h2 className="text-xl md:text-2xl text-center mb-8 md:mb-10">
مقالات مرتبط
</h2>
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-6 min-w-0">
{relatedPosts.map((p) => (
<BlogPostCard key={p.id} post={p} variant="related" />
))}
</div>
</div>
</section>
)}
<ScrollToTop />
</>
);
}
+204
View File
@@ -0,0 +1,204 @@
import type { Metadata } from "next";
import Link from "next/link";
import { getPosts, getCategories } from "@/lib/wp-api";
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";
const PER_PAGE = 9;
export const metadata: Metadata = {
title: "بلاگ",
description: "مقالات و مطالب فیپ در زمینه یکپارچه‌سازی، اتوماسیون و بهره‌وری سازمانی.",
};
interface BlogPageProps {
searchParams: Promise<{ search?: string; category?: string; page?: string }>;
}
export default async function BlogPage({ searchParams }: BlogPageProps) {
const params = await searchParams;
const search = params.search ?? "";
const categoryId = params.category ? parseInt(params.category, 10) : undefined;
const page = Math.max(1, parseInt(params.page ?? "1", 10) || 1);
let postsResult;
let categories: Awaited<ReturnType<typeof getCategories>> = [];
try {
[postsResult, categories] = await Promise.all([
getPosts({
page,
perPage: PER_PAGE,
search: search || undefined,
categoryId: Number.isFinite(categoryId) ? categoryId : undefined,
}),
getCategories(),
]);
} catch (e) {
return (
<section className="bg-background pt-32 pb-20 md:pt-40 md:pb-28">
<div className="mx-auto max-w-4xl px-6 text-center">
<h1 className="text-4xl font-bold md:text-5xl">بلاگ</h1>
<p className="mt-4 text-muted-foreground">
در حال حاضر امکان بارگذاری مطالب وجود ندارد. لطفاً بعداً تلاش کنید.
</p>
</div>
</section>
);
}
const { posts, total, totalPages } = postsResult;
const hasFilters = search || categoryId;
function buildQuery(updates: { search?: string; category?: string; page?: number }) {
const q = new URLSearchParams();
if (updates.search !== undefined && updates.search !== "") q.set("search", updates.search);
if (updates.category !== undefined && updates.category !== "") q.set("category", updates.category);
if (updates.page !== undefined && updates.page > 1) q.set("page", String(updates.page));
const s = q.toString();
return s ? `?${s}` : "";
}
return (
<>
{/* Hero + search & filters, compact */}
<section className="bg-background pt-24 pb-6 md:pt-28 md:pb-8">
<div className="mx-auto max-w-3xl px-4 sm:px-6">
<div className="text-center space-y-5">
<div className="space-y-1.5">
<h1 className="text-3xl font-bold md:text-4xl tracking-tight">
بلاگ
</h1>
<p className="text-sm md:text-base text-muted-foreground max-w-xl mx-auto">
مقالات و مطالب فیپ در زمینه یکپارچه‌سازی، اتوماسیون و بهره‌وری سازمانی.
</p>
</div>
<div className="flex flex-col gap-4">
<form
method="get"
action="/blog"
className="flex gap-2 w-full"
>
<input type="hidden" name="category" value={categoryId ?? ""} />
<div className="relative flex-1 flex items-center">
<Search className="absolute right-3 size-4 text-muted-foreground pointer-events-none" aria-hidden />
<Input
type="search"
name="search"
placeholder="جستجو در مطالب..."
defaultValue={search}
className="w-full pr-10 pl-3 h-9 text-sm"
aria-label="جستجو در مطالب"
/>
</div>
<Button type="submit" className="shrink-0">
جستجو
</Button>
</form>
{hasFilters && (
<Link
href="/blog"
className="inline-flex items-center gap-1.5 text-xs text-muted-foreground hover:text-foreground transition-colors"
>
<X className="size-3.5" />
پاک کردن فیلتر
</Link>
)}
{categories.length > 0 && (
<div className="flex flex-wrap items-center justify-center gap-1.5">
<Link
href="/blog"
className={`text-xs rounded-full px-3 py-1.5 transition-colors ${!categoryId
? "bg-primary text-primary-foreground shadow-sm"
: "bg-muted/80 hover:bg-muted text-muted-foreground hover:text-foreground"
}`}
>
همه
</Link>
{categories.map((cat) => (
<Link
key={cat.id}
href={`/blog${buildQuery({ search, category: String(cat.id), page: 1 })}`}
className={`text-xs rounded-full px-3 py-1.5 transition-colors ${categoryId === cat.id
? "bg-primary text-primary-foreground shadow-sm"
: "bg-muted/80 hover:bg-muted text-muted-foreground hover:text-foreground"
}`}
>
{cat.name}
</Link>
))}
</div>
)}
</div>
</div>
</div>
</section>
<section className="bg-background pb-20 md:pb-28">
<div className="mx-auto max-w-6xl px-6 pt-2">
{posts.length === 0 ? (
<div className="text-center py-16 text-muted-foreground">
<p>مطلبی یافت نشد.</p>
<Link href="/blog" className="mt-4 inline-block">
<Button variant="outline" size="sm">
نمایش همه مطالب
</Button>
</Link>
</div>
) : (
<>
<ul className="grid gap-6 sm:grid-cols-2 lg:grid-cols-3">
{posts.map((post) => (
<li key={post.id}>
<BlogPostCard post={post} />
</li>
))}
</ul>
{totalPages > 1 && (
<nav
className="mt-12 flex flex-wrap items-center justify-center gap-2"
aria-label="صفحه‌بندی"
>
{page > 1 && (
<Link
href={`/blog${buildQuery({
search,
category: categoryId ? String(categoryId) : undefined,
page: page - 1,
})}`}
>
<Button variant="outline" size="icon" aria-label="صفحه قبل">
<ChevronRight className="size-4" />
</Button>
</Link>
)}
<span className="text-muted-foreground text-sm px-2">
صفحه {page} از {totalPages} ({total} مطلب)
</span>
{page < totalPages && (
<Link
href={`/blog${buildQuery({
search,
category: categoryId ? String(categoryId) : undefined,
page: page + 1,
})}`}
>
<Button variant="outline" size="icon" aria-label="صفحه بعد">
<ChevronLeft className="size-4" />
</Button>
</Link>
)}
</nav>
)}
</>
)}
</div>
</section>
</>
);
}
BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 25 KiB

After

Width:  |  Height:  |  Size: 4.2 KiB

+120 -52
View File
@@ -122,74 +122,77 @@
:root {
--font-iran-sans: "IRANSansX", var(--font-geist-sans);
--radius: 0.625rem;
--card: oklch(1 0 0);
--card-foreground: oklch(0.145 0 0);
--popover: oklch(1 0 0);
--popover-foreground: oklch(0.145 0 0);
--primary: #001BB7;
--card: oklch(1 0.006 175);
--card-foreground: oklch(0.145 0.02 175);
--popover: oklch(1 0.006 175);
--popover-foreground: oklch(0.145 0.02 175);
--primary: #01796F;
--primary-foreground: oklch(0.985 0 0);
--secondary: #0046FF;
--secondary: #0D9488;
--secondary-foreground: oklch(0.985 0 0);
--muted: oklch(0.97 0.01 280);
--muted-foreground: oklch(0.35 0.05 250);
--muted: oklch(0.97 0.012 175);
--muted-foreground: oklch(0.35 0.05 175);
--accent: #FCC61D;
--accent-foreground: oklch(0.25 0.05 50);
--destructive: oklch(0.577 0.245 27.325);
--border: oklch(0.9 0.01 280);
--input: oklch(0.95 0.01 280);
--ring: oklch(0.5 0.15 220);
--chart-1: oklch(0.646 0.222 41.116);
--chart-2: oklch(0.6 0.118 184.704);
--chart-3: oklch(0.398 0.07 227.392);
--chart-4: oklch(0.828 0.189 84.429);
--chart-5: oklch(0.769 0.188 70.08);
--sidebar: oklch(0.985 0 0);
--sidebar-foreground: oklch(0.145 0 0);
--sidebar-primary: oklch(0.205 0 0);
--border: oklch(0.9 0.012 175);
--input: oklch(0.95 0.01 175);
--ring: oklch(0.45 0.1 175);
--chart-1: oklch(0.45 0.1 175);
--chart-2: oklch(0.6 0.12 162);
--chart-3: oklch(0.55 0.08 175);
--chart-4: oklch(0.7 0.1 170);
--chart-5: oklch(0.5 0.09 180);
--sidebar: oklch(0.985 0.008 175);
--sidebar-foreground: oklch(0.145 0.02 175);
--sidebar-primary: #01796F;
--sidebar-primary-foreground: oklch(0.985 0 0);
--sidebar-accent: oklch(0.97 0 0);
--sidebar-accent-foreground: oklch(0.205 0 0);
--sidebar-border: oklch(0.922 0 0);
--sidebar-ring: oklch(0.708 0 0);
--background: oklch(0.99 0.005 280);
--foreground: oklch(0.25 0.05 250);
--sidebar-accent: oklch(0.97 0.015 175);
--sidebar-accent-foreground: oklch(0.205 0.02 175);
--sidebar-border: oklch(0.922 0.01 175);
--sidebar-ring: oklch(0.45 0.1 175);
--background: oklch(0.99 0.008 175);
--foreground: oklch(0.25 0.03 175);
}
.dark {
--background: oklch(0.145 0.01 280);
--background: oklch(0.145 0.02 175);
--foreground: oklch(0.95 0.01 280);
--card: oklch(0.22 0.02 280);
--card: oklch(0.22 0.025 175);
--card-foreground: oklch(0.95 0.01 280);
--popover: oklch(0.22 0.02 280);
--popover: oklch(0.22 0.025 175);
--popover-foreground: oklch(0.95 0.01 280);
--primary: #001BB7;
--primary: #01796F;
--primary-foreground: oklch(0.985 0 0);
--secondary: #0046FF;
--secondary-foreground: oklch(0.95 0.01 280);
--muted: oklch(0.28 0.02 280);
--muted-foreground: oklch(0.75 0.02 280);
--secondary: #14B8A6;
--secondary-foreground: oklch(0.985 0 0);
--muted: oklch(0.28 0.025 175);
--muted-foreground: oklch(0.75 0.02 175);
--accent: #FCC61D;
--accent-foreground: oklch(0.2 0.05 50);
--destructive: oklch(0.704 0.191 22.216);
--border: oklch(0.3 0.02 280);
--input: oklch(0.3 0.02 280);
--ring: oklch(0.5 0.15 220);
--chart-1: oklch(0.488 0.243 264.376);
--chart-2: oklch(0.696 0.17 162.48);
--chart-3: oklch(0.769 0.188 70.08);
--chart-4: oklch(0.627 0.265 303.9);
--chart-5: oklch(0.645 0.246 16.439);
--sidebar: oklch(0.22 0.02 280);
--border: oklch(0.3 0.025 175);
--input: oklch(0.3 0.025 175);
--ring: oklch(0.5 0.12 175);
--chart-1: oklch(0.5 0.12 175);
--chart-2: oklch(0.6 0.14 162);
--chart-3: oklch(0.55 0.1 175);
--chart-4: oklch(0.65 0.11 170);
--chart-5: oklch(0.45 0.09 180);
--sidebar: oklch(0.22 0.025 175);
--sidebar-foreground: oklch(0.95 0.01 280);
--sidebar-primary: oklch(0.488 0.243 264.376);
--sidebar-primary: #01796F;
--sidebar-primary-foreground: oklch(0.985 0 0);
--sidebar-accent: oklch(0.3 0.02 280);
--sidebar-accent: oklch(0.3 0.03 175);
--sidebar-accent-foreground: oklch(0.95 0.01 280);
--sidebar-border: oklch(0.3 0.02 280);
--sidebar-ring: oklch(0.5 0.15 220);
--sidebar-border: oklch(0.3 0.025 175);
--sidebar-ring: oklch(0.5 0.12 175);
}
@layer base {
html {
scroll-behavior: smooth;
}
* {
@apply border-border outline-ring/50;
}
@@ -199,17 +202,17 @@
letter-spacing: normal;
line-height: 1.6;
background-image:
radial-gradient(at 0% 0%, oklch(0.98 0.01 280 / 0.3) 0px, transparent 50%),
radial-gradient(at 100% 100%, oklch(0.95 0.02 220 / 0.2) 0px, transparent 50%),
radial-gradient(at 50% 50%, oklch(0.97 0.01 300 / 0.15) 0px, transparent 50%);
radial-gradient(at 0% 0%, oklch(0.98 0.012 175 / 0.3) 0px, transparent 50%),
radial-gradient(at 100% 100%, oklch(0.95 0.02 175 / 0.2) 0px, transparent 50%),
radial-gradient(at 50% 50%, oklch(0.97 0.01 170 / 0.15) 0px, transparent 50%);
background-attachment: fixed;
transition: background-color 0.3s ease;
}
.dark body {
background-image:
radial-gradient(at 0% 0%, oklch(0.2 0.02 280 / 0.3) 0px, transparent 50%),
radial-gradient(at 100% 100%, oklch(0.18 0.02 220 / 0.2) 0px, transparent 50%),
radial-gradient(at 50% 50%, oklch(0.22 0.02 300 / 0.15) 0px, transparent 50%);
radial-gradient(at 0% 0%, oklch(0.2 0.025 175 / 0.3) 0px, transparent 50%),
radial-gradient(at 100% 100%, oklch(0.18 0.02 175 / 0.2) 0px, transparent 50%),
radial-gradient(at 50% 50%, oklch(0.22 0.025 170 / 0.15) 0px, transparent 50%);
}
h1, h2, h3, h4, h5, h6 {
letter-spacing: normal;
@@ -228,3 +231,68 @@
transition: all 0.3s ease;
}
}
/* WordPress table block (figure.wp-block-table > table) */
.wp-block-table {
margin: 1.5rem 0;
overflow-x: auto;
border-radius: var(--radius);
}
.wp-block-table table {
width: 100%;
border-collapse: collapse;
font-size: 0.9375rem;
}
.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);
text-align: right;
min-width: 850px;
}
.wp-block-table tbody tr:first-child th,
.wp-block-table tbody tr:first-child td {
background: var(--muted);
font-weight: 600;
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;
color: var(--foreground);
}
.wp-block-button > .wp-element-button {
@apply inline-flex bg-primary text-primary-foreground px-4 py-2 rounded-md hover:bg-primary/80 hover:text-primary-foreground/80 cursor-pointer no-underline;
}
.wp-block-button {
@apply inline-flex;
}
.wp-block-button.is-style-outline > .wp-element-button {
@apply bg-transparent text-foreground border border-2 border-primary hover:text-primary cursor-pointer no-underline;
}
.cta-block {
@apply flex flex-col gap-4 border rounded-md border-primary/50 p-4 items-center my-4 shadow-sm bg-primary/5 text-primary;
}
.wp-block-image {
@apply rounded-md overflow-hidden shadow-sm;
}
+5
View File
@@ -2,12 +2,16 @@ import type { Metadata } from "next";
import "./globals.css";
import { ConditionalLayout } from "@/components/conditional-layout";
import { ThemeProvider } from "@/components/theme-provider";
import { TopProgressBar } from "@/components/top-progress-bar";
export const metadata: Metadata = {
title: "فیپ",
description:
"فیپ یک سامانه برای ایجاد و طراحی راهکارهای مناسب برای کسب و کار شما است.",
icons: {
icon: "/assets/feep-Logo.svg",
},
};
export default function RootLayout({
@@ -21,6 +25,7 @@ export default function RootLayout({
className={`antialiased`}
>
<ThemeProvider>
<TopProgressBar />
<ConditionalLayout>{children}</ConditionalLayout>
</ThemeProvider>
</body>
+35
View File
@@ -0,0 +1,35 @@
"use client";
import type { BlockquoteBlockPayload } from "@/lib/blog-blocks";
import { Quote } from "lucide-react";
interface BlogBlockquoteProps {
payload: BlockquoteBlockPayload;
}
export function BlogBlockquote({ payload }: BlogBlockquoteProps) {
const { content, citation } = payload;
if (!content.trim()) return null;
return (
<figure className="my-8 not-prose">
<blockquote className="relative rounded-xl border border-border bg-muted/40 px-6 py-2 pr-14 text-foreground shadow-sm">
<Quote
className="absolute right-4 top-4 size-8 text-muted-foreground/40"
aria-hidden
/>
<div
className="blog-blockquote-content text-lg leading-relaxed text-justify [&_p]:my-2 [&_a]:text-primary [&_a]:underline hover:[&_a]:no-underline"
dangerouslySetInnerHTML={{ __html: content }}
/>
{citation ? (
<footer className="mt-4 border-t border-border/60 pt-4">
<cite className="text-sm text-muted-foreground not-italic">
— {citation}
</cite>
</footer>
) : null}
</blockquote>
</figure>
);
}
+74
View File
@@ -0,0 +1,74 @@
import Link from "next/link";
import Image from "next/image";
import { Calendar, Clock, ArrowLeft } from "lucide-react";
import { Card, CardContent, CardFooter, CardHeader } from "@/components/ui/card";
interface BlogCardProps {
slug: string;
title: string;
excerpt: string;
date: string;
readTime: string;
category: string;
image: string;
}
export function BlogCard({
slug,
title,
excerpt,
date,
readTime,
category,
image,
}: BlogCardProps) {
return (
<Link href={`/blog/${slug}`} className="group block h-full">
<Card className="h-full flex flex-col overflow-hidden transition-all duration-300 hover:shadow-lg hover:-translate-y-1 dark:border-transparent">
{/* Image with 3:2 aspect ratio */}
<div className="relative aspect-[5/3] w-full overflow-hidden bg-muted">
<Image
src={image}
alt={title}
fill
className="object-cover transition-transform duration-300 group-hover:scale-105"
quality={100}
sizes="(max-width: 768px) 100vw, (max-width: 1024px) 50vw, 400px"
/>
{/* Category Badge */}
<div className="absolute top-4 right-4">
<span className="inline-flex items-center px-3 py-1 rounded-full text-xs font-medium bg-primary/90 text-primary-foreground backdrop-blur-sm">
{category}
</span>
</div>
</div>
<CardHeader className="flex-1 flex flex-col">
<h3 className="text-base py-2 font-bold leading-tight tracking-normal group-hover:text-primary transition-colors line-clamp-2">
{title}
</h3>
</CardHeader>
<CardContent className="flex-1">
<p className="text-muted-foreground leading-relaxed tracking-normal line-clamp-3 text-sm">
{excerpt}
</p>
</CardContent>
<CardFooter className="flex items-center justify-between gap-4 text-xs text-muted-foreground border-t pt-4">
<div className="flex items-center gap-2">
<Calendar className="size-4" />
<span>{date}</span>
</div>
<div className="flex items-center gap-2">
<Clock className="size-4" />
<span>{readTime} دقیقه</span>
</div>
</CardFooter>
</Card>
</Link>
);
}
+60
View File
@@ -0,0 +1,60 @@
"use client";
import {
Accordion,
AccordionContent,
AccordionItem,
AccordionTrigger,
} from "@/components/ui/accordion";
import type { CollapseBlockPayload } from "@/lib/blog-blocks";
import { Icon } from "@iconify/react";
interface BlogCollapseSectionProps {
section: CollapseBlockPayload;
/** Optional prefix for accordion item ids (e.g. for multiple sections on one page). */
idPrefix?: string;
}
export function BlogCollapseSection({
section,
idPrefix = "collapse",
}: BlogCollapseSectionProps) {
if (!section.items.length) return null;
return (
<div className="my-8 space-y-4 border rounded-xl p-6">
{section.title ? (
<h2 className="text-2xl font-bold !mt-0 mb-4 scroll-mt-24">
{section.title}
</h2>
) : null}
<Accordion type="single" collapsible className="w-full gap-1">
{section.items.map((item, i) => (
<AccordionItem
key={i}
value={`${idPrefix}-${i}`}
className="border-none !m-0 hover:bg-muted has-[[data-state=open]]:bg-muted px-4 rounded-md"
>
<AccordionTrigger className="group/trigger text-right hover:no-underline cursor-pointer py-4 [&>svg]:shrink-0">
<span className="flex gap-1 items-center">
<Icon
icon="lucide:circle-small"
className="size-5 group-data-[state=open]/trigger:hidden"
/>
<Icon
icon="lucide:dot"
className="size-5 hidden group-data-[state=open]/trigger:inline-block"
/>
{item.question}
</span>
</AccordionTrigger>
<AccordionContent className="text-muted-foreground leading-relaxed text-justify">
<div dangerouslySetInnerHTML={{ __html: item.answer }} />
</AccordionContent>
</AccordionItem>
))}
</Accordion>
</div>
);
}
+122
View File
@@ -0,0 +1,122 @@
import Link from "next/link";
import Image from "next/image";
import { ImageIcon, User } from "lucide-react";
import { Card, CardHeader, CardTitle, CardContent } from "@/components/ui/card";
import { getFeaturedImageUrl, type WpPost } from "@/lib/wp-api";
import { formatDateShamsi } from "@/lib/format-date";
function stripHtml(html: string): string {
return html.replace(/<[^>]*>/g, "").trim();
}
type BlogPostCardVariant = "default" | "tiny";
export function BlogPostCard({
post,
variant = "default",
}: {
post: WpPost;
variant?: BlogPostCardVariant;
}) {
const excerpt = stripHtml(post.excerpt.rendered) || stripHtml(post.content.rendered).slice(0, 160);
const modifiedDate = post.modified ? formatDateShamsi(post.modified) : null;
const authorName = post._embedded?.author?.[0]?.name ?? null;
const thumbnailUrl = getFeaturedImageUrl(post, "full");
if (variant === "tiny") {
return (
<Link href={`/blog/${encodeURIComponent(post.slug)}`} className="block h-full">
<Card className="h-full overflow-hidden transition-all rounded-md duration-300 hover:shadow-md flex flex-col p-2">
<CardContent className="p-2 grid grid-cols-3 gap-4">
{thumbnailUrl ? (
<div className="relative rounded-md aspect-[4/3] w-full overflow-hidden bg-muted shrink-0 col-span-1">
<Image
src={thumbnailUrl}
alt=""
fill
className="object-cover"
sizes="(max-width: 640px) 100vw, (max-width: 1024px) 50vw, 33vw"
/>
</div>
) : (
<div
className="relative rounded-md flex aspect-[4/3] w-full items-center justify-center overflow-hidden bg-muted shrink-0"
aria-hidden
>
<ImageIcon className="size-8 text-muted-foreground/50" />
</div>
)}
<div className="col-span-2">
<h4 className="text-sm font-semibold leading-snug line-clamp-2">
{post.title.rendered}
</h4>
<div className="flex flex-wrap w-full items-center gap-x-4 gap-y-1 text-xs text-muted-foreground bg-muted inline-flex p-1 mt-2 rounded-sm">
{authorName && (
<span className="inline-flex flex-1 items-center gap-1.5">
<User className="size-3.5 shrink-0" aria-hidden />
{authorName}
</span>
)}
{modifiedDate && (
<time dateTime={post.modified}>
{modifiedDate}
</time>
)}
</div>
</div>
</CardContent>
</Card>
</Link>
);
}
return (
<Link href={`/blog/${encodeURIComponent(post.slug)}`} className="block h-full">
<Card className="h-full overflow-hidden transition-all duration-300 hover:-translate-y-1 hover:shadow-lg pt-0 gap-1">
{thumbnailUrl ? (
<div className="relative aspect-video w-full overflow-hidden bg-muted">
<Image
src={thumbnailUrl}
alt=""
fill
className="object-cover"
quality={100}
priority
sizes="(max-width: 640px) 100vw, (max-width: 1024px) 50vw, 350px"
/>
</div>
) : (
<div
className="relative flex aspect-video w-full items-center justify-center overflow-hidden bg-muted"
aria-hidden
>
<ImageIcon className="size-12 text-muted-foreground/50" />
</div>
)}
<CardHeader className="pb-2">
<CardTitle className="text-base font-bold leading-tight line-clamp-2 my-2">
{post.title.rendered}
</CardTitle>
<div className="flex flex-wrap items-center gap-x-4 gap-y-1 text-xs text-muted-foreground bg-muted inline-flex p-1 rounded-sm">
{authorName && (
<span className="inline-flex flex-1 items-center gap-1.5">
<User className="size-3.5 shrink-0" aria-hidden />
{authorName}
</span>
)}
{modifiedDate && (
<time dateTime={post.modified}>
{modifiedDate}
</time>
)}
</div>
</CardHeader>
<CardContent className="pt-0">
<p className="text-muted-foreground text-sm leading-relaxed line-clamp-3">
{excerpt}
</p>
</CardContent>
</Card>
</Link>
);
}
+182
View File
@@ -0,0 +1,182 @@
"use client";
import { useState, useCallback, useEffect } from "react";
import {
Link2,
Clock,
Calendar,
User,
Share2,
Check,
} from "lucide-react";
import { Button } from "@/components/ui/button";
import {
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from "@/components/ui/tooltip";
import { cn } from "@/lib/utils";
export interface BlogPostMetaProps {
/** Path (e.g. /blog/slug). Full URL is built on client for share/copy. */
url: string;
title: string;
readingTimeMinutes: number;
/** Shamsi publish date (e.g. "۱۴۰۳/۰۸/۱۵") */
date?: string | null;
/** ISO date for <time dateTime> */
dateTime?: string | null;
modifiedDate?: string | null;
authorName?: string | null;
className?: string;
}
export function BlogPostMeta({
url,
title,
readingTimeMinutes,
date,
dateTime,
modifiedDate,
authorName,
className,
}: BlogPostMetaProps) {
const [copied, setCopied] = useState(false);
const [shared, setShared] = useState(false);
const [fullUrl, setFullUrl] = useState(url);
const [canNativeShare, setCanNativeShare] = useState(false);
useEffect(() => {
if (url.startsWith("/")) {
setFullUrl(window.location.origin + url);
} else {
setFullUrl(url);
}
setCanNativeShare(Boolean(typeof navigator !== "undefined" && navigator.share));
}, [url]);
const copyLink = useCallback(async () => {
try {
await navigator.clipboard.writeText(fullUrl);
setCopied(true);
setTimeout(() => setCopied(false), 2000);
} catch {
setCopied(false);
}
}, [fullUrl]);
const handleShare = useCallback(async () => {
if (typeof navigator !== "undefined" && navigator.share) {
try {
await navigator.share({
title,
url: fullUrl,
text: title,
});
setShared(true);
setTimeout(() => setShared(false), 2000);
} catch (err) {
if ((err as Error).name !== "AbortError") copyLink();
}
} else {
copyLink();
}
}, [title, fullUrl, copyLink]);
return (
<TooltipProvider delayDuration={300}>
<div
className={cn(
"flex flex-wrap items-center gap-x-5 gap-y-2 text-sm text-muted-foreground",
className
)}
>
{/* Reading time */}
<span className="inline-flex items-center gap-1.5">
<Clock className="size-4 shrink-0" aria-hidden />
<span>
{readingTimeMinutes === 1
? "۱ دقیقه مطالعه"
: `${readingTimeMinutes.toLocaleString("fa-IR")} دقیقه مطالعه`}
</span>
</span>
{/* Publish date */}
{date && (
<time
className="inline-flex items-center gap-1.5"
dateTime={dateTime ?? undefined}
>
<Calendar className="size-4 shrink-0" aria-hidden />
<span>انتشار: {date}</span>
</time>
)}
{/* Last updated */}
{modifiedDate && (
<span className="inline-flex items-center gap-1.5">
<Calendar className="size-4 shrink-0" aria-hidden />
<span>آخرین بروزرسانی: {modifiedDate}</span>
</span>
)}
{/* Author */}
{authorName && (
<span className="inline-flex items-center gap-1.5">
<User className="size-4 shrink-0" aria-hidden />
<span>{authorName}</span>
</span>
)}
{/* Divider + actions */}
<span className="mr-auto inline-flex items-center gap-1">
<span
className="hidden sm:inline h-4 w-px bg-border ml-2"
aria-hidden
/>
<Tooltip>
<TooltipTrigger asChild>
<Button
variant="ghost"
size="icon-sm"
className="size-8 text-muted-foreground hover:text-foreground"
onClick={copyLink}
aria-label="کپی لینک"
>
{copied ? (
<Check className="size-4 text-green-600" />
) : (
<Link2 className="size-4" />
)}
</Button>
</TooltipTrigger>
<TooltipContent side="bottom">
{copied ? "کپی شد" : "کپی لینک"}
</TooltipContent>
</Tooltip>
<Tooltip>
<TooltipTrigger asChild>
<Button
variant="ghost"
size="icon-sm"
className="size-8 text-muted-foreground hover:text-foreground"
onClick={handleShare}
aria-label="اشتراک‌گذاری"
>
{shared ? (
<Check className="size-4 text-green-600" />
) : (
<Share2 className="size-4" />
)}
</Button>
</TooltipTrigger>
<TooltipContent side="bottom">
{shared ? "اشتراک گذاشته شد" : canNativeShare ? "اشتراک‌گذاری" : "کپی لینک"}
</TooltipContent>
</Tooltip>
</span>
</div>
</TooltipProvider>
);
}
+30
View File
@@ -0,0 +1,30 @@
"use client";
import type { BlockquoteBlockPayload } from "@/lib/blog-blocks";
interface BlogPullquoteProps {
payload: BlockquoteBlockPayload;
}
export function BlogPullquote({ payload }: BlogPullquoteProps) {
const { content, citation } = payload;
if (!content.trim()) return null;
return (
<figure className="my-10 not-prose">
<blockquote className="relative rounded-xl border border-border bg-muted/40 px-6 py-5 text-center text-foreground shadow-sm">
<div
className=""
dangerouslySetInnerHTML={{ __html: content }}
/>
{citation ? (
<footer className="mt-4">
<cite className="text-sm text-muted-foreground not-italic">
— {citation}
</cite>
</footer>
) : null}
</blockquote>
</figure>
);
}
+57
View File
@@ -0,0 +1,57 @@
"use client";
import type { TipBlockPayload } from "@/lib/blog-blocks";
import { Info, AlertTriangle, AlertCircle } from "lucide-react";
import { cn } from "@/lib/utils";
interface BlogTipProps {
payload: TipBlockPayload;
}
const variantConfig = {
info: {
icon: Info,
className:
"border-primary/40 bg-primary/5 text-foreground [&_strong]:text-primary",
iconClassName: "text-primary",
},
warn: {
icon: AlertTriangle,
className:
"border-amber-500/50 bg-amber-500/10 text-foreground [&_strong]:text-amber-600 dark:[&_strong]:text-amber-400",
iconClassName: "text-amber-500",
},
danger: {
icon: AlertCircle,
className:
"border-destructive/50 bg-destructive/10 text-foreground [&_strong]:text-destructive",
iconClassName: "text-destructive",
},
} as const;
export function BlogTip({ payload }: BlogTipProps) {
const { variant, content } = payload;
if (!content.trim()) return null;
const config = variantConfig[variant ?? 'info'];
const Icon = config.icon;
return (
<div
className={cn(
"my-6 flex gap-3 rounded-xl border p-4 pr-4 shadow-sm",
config.className
)}
role="note"
>
<Icon
className={cn("mt-0.5 size-8 shrink-0", config.iconClassName)}
aria-hidden
/>
<div
className="blog-tip-content min-w-0 text-sm leading-relaxed [&_p]:my-2 [&_p:first-child]:mt-0 [&_p:last-child]:mb-0 [&_a]:text-primary [&_a]:underline hover:[&_a]:no-underline"
dangerouslySetInnerHTML={{ __html: content }}
/>
</div>
);
}
+271
View File
@@ -0,0 +1,271 @@
"use client";
import { useCallback, useEffect, useMemo, useState } from "react";
import Link from "next/link";
import { ChevronDown, List } from "lucide-react";
import {
Collapsible,
CollapsibleContent,
CollapsibleTrigger,
} from "@/components/ui/collapsible";
import { ScrollArea } from "@/components/ui/scroll-area";
import { cn } from "@/lib/utils";
import type { TocItem } from "@/lib/blog-utils";
function useScrollProgress() {
const [progress, setProgress] = useState(0);
const update = useCallback(() => {
const { scrollY } = window;
const docHeight = document.documentElement.scrollHeight - window.innerHeight;
if (docHeight <= 0) {
setProgress(0);
return;
}
const value = Math.min(100, Math.max(0, (scrollY / docHeight) * 100));
setProgress(value);
}, []);
useEffect(() => {
update();
window.addEventListener("scroll", update, { passive: true });
window.addEventListener("resize", update);
return () => {
window.removeEventListener("scroll", update);
window.removeEventListener("resize", update);
};
}, [update]);
return progress;
}
interface TocGroup {
heading: TocItem;
children: TocItem[];
}
function groupByLevel2(items: TocItem[]): TocGroup[] {
const groups: TocGroup[] = [];
let current: TocGroup | null = null;
for (const item of items) {
if (item.level === 2) {
current = { heading: item, children: [] };
groups.push(current);
} else if (current) {
current.children.push(item);
}
}
return groups;
}
interface BlogTocProps {
items: TocItem[];
className?: string;
}
const tocLinkClass = (
level: number,
isActive: boolean
) =>
cn(
"block rounded-md border-r-2 rounded-s-none border-transparent py-1.5 text-sm transition-colors hover:text-foreground hover:bg-muted/60",
level === 2 && "pr-3",
level === 3 && "pr-5",
level === 4 && "pr-7",
isActive
? "font-medium text-foreground border-primary bg-muted/50"
: "text-muted-foreground"
);
export function BlogToc({ items, className }: BlogTocProps) {
const [activeId, setActiveId] = useState<string | null>(items[0]?.id ?? null);
const [mobileOpen, setMobileOpen] = useState(false);
const [openSections, setOpenSections] = useState<Set<string>>(() => new Set());
const scrollProgress = useScrollProgress();
const groups = useMemo(() => groupByLevel2(items), [items]);
useEffect(() => {
setOpenSections(new Set(groups.map((g) => g.heading.id)));
}, [groups]);
useEffect(() => {
if (items.length === 0) return;
const observer = new IntersectionObserver(
(entries) => {
for (const entry of entries) {
if (entry.isIntersecting) {
const id = entry.target.getAttribute("id");
if (id) setActiveId(id);
}
}
},
{ rootMargin: "-80px 0% -70% 0%", threshold: 0 }
);
items.forEach(({ id }) => {
const el = document.getElementById(id);
if (el) observer.observe(el);
});
return () => observer.disconnect();
}, [items]);
useEffect(() => {
if (activeId && groups.length > 0) {
const group = groups.find(
(g) => g.heading.id === activeId || g.children.some((c) => c.id === activeId)
);
if (group) {
setOpenSections((prev) => new Set(prev).add(group.heading.id));
}
}
}, [activeId, groups]);
if (items.length === 0) return null;
const renderTocLink = (item: TocItem, onNav?: () => void) => (
<Link
key={item.id}
href={`#${item.id}`}
onClick={() => {
setMobileOpen(false);
onNav?.();
}}
className={tocLinkClass(item.level, activeId === item.id)}
>
<span className="line-clamp-2">{item.text}</span>
</Link>
);
const tocList = (
<nav aria-label="فهرست مطالب" className="space-y-1">
{groups.map(({ heading, children }) => {
const hasChildren = children.length > 0;
const isOpen = openSections.has(heading.id);
if (!hasChildren) {
return renderTocLink(heading);
}
return (
<Collapsible
key={heading.id}
open={isOpen}
onOpenChange={(open) => {
setOpenSections((prev) => {
const next = new Set(prev);
if (open) next.add(heading.id);
else next.delete(heading.id);
return next;
});
}}
>
<div
className={cn(
"flex items-center gap-0.5 rounded-md border-r-2 rounded-s-none py-1.5 pr-3",
activeId === heading.id ? "border-primary bg-muted/50" : "border-transparent"
)}
>
<Link
href={`#${heading.id}`}
onClick={() => setMobileOpen(false)}
className={cn(
"min-w-0 flex-1 text-sm transition-colors hover:text-foreground hover:bg-muted/60 rounded-md py-0.5 -my-0.5",
activeId === heading.id ? "font-medium text-foreground" : "text-muted-foreground"
)}
>
<span className="line-clamp-2">{heading.text}</span>
</Link>
<CollapsibleTrigger
className="shrink-0 rounded p-1 text-muted-foreground hover:bg-muted/60 hover:text-foreground transition-colors"
aria-label={isOpen ? "بستن" : "باز کردن"}
>
<ChevronDown
className={cn("size-4 transition-transform", isOpen && "rotate-180")}
/>
</CollapsibleTrigger>
</div>
<CollapsibleContent>
<div className="space-y-1 pt-0.5 pr-2 border-r border-border/50">
{children.map((item) => renderTocLink(item))}
</div>
</CollapsibleContent>
</Collapsible>
);
})}
</nav>
);
return (
<>
{/* Desktop: sticky sidebar with scroll progress bar */}
<aside
className={cn(
"hidden lg:block shrink-0 relative",
"sticky top-24 self-start",
className
)}
>
{/* Horizontal scroll progress indicator */}
<div
className="absolute -top-2 left-0 right-0 h-1 rounded-full bg-muted overflow-hidden"
aria-hidden
>
<div
className="h-full min-w-[2px] bg-primary transition-[width] duration-150 ease-out rounded-full"
style={{ width: `${scrollProgress}%` }}
/>
</div>
<p className="mb-3 text-md ms-2 font-semibold uppercase tracking-wider text-muted-foreground">
فهرست مطالب
</p>
<ScrollArea className="h-full pb-8" dir="rtl">
{tocList}
</ScrollArea>
</aside>
{/* Mobile: collapsible with horizontal scroll progress bar */}
<Collapsible
open={mobileOpen}
onOpenChange={setMobileOpen}
className={cn(
"lg:hidden w-full sticky top-14 z-10 bg-background",
className
)}
>
<CollapsibleTrigger
className={cn(
"flex w-full items-center justify-between gap-2 rounded-lg border border-border bg-muted/30 px-4 py-3 text-sm font-medium transition-colors hover:bg-muted/50",
mobileOpen && "rounded-b-none border-b-0"
)}
>
{/* Horizontal scroll progress indicator (mobile) */}
<div
className="absolute right-1 top-1 inset-inline-0 h-1 w-[97%] bg-muted overflow-hidden rounded-full"
aria-hidden
>
<div
className="h-full bg-primary transition-[width] duration-150 ease-out rounded-full"
style={{ width: `${scrollProgress}%` }}
/>
</div>
<span className="flex items-center gap-2">
<List className="size-4 text-muted-foreground" />
فهرست مطالب
</span>
<ChevronDown
className={cn("size-4 text-muted-foreground transition-transform", mobileOpen && "rotate-180")}
/>
</CollapsibleTrigger>
<CollapsibleContent>
<div className="max-h-64 overflow-y-auto rounded-b-lg border border-t-0 border-border bg-muted/20 p-3">
{tocList}
</div>
</CollapsibleContent>
</Collapsible>
</>
);
}
+9 -17
View File
@@ -6,6 +6,10 @@ const links = [
title: "درباره ما",
href: "/about",
},
{
title: "بلاگ",
href: "/blog",
},
{
title: "راه حل‌ها",
href: "/solutions",
@@ -14,13 +18,11 @@ const links = [
export default function FooterSection() {
return (
<footer className="py-16 md:py-32">
<div className="mx-auto max-w-5xl px-6">
<Link href="/" aria-label="go home" className="mx-auto block size-fit">
<Logo />
</Link>
<footer className="py-16 md:py-16 bg-muted border-t">
<div className="mx-auto max-w-5xl px-6 flex items-center flex-col">
<Logo className="mx-auto" />
<div className="my-8 flex flex-wrap justify-center gap-6 text-sm">
<div className="my-4 flex flex-wrap justify-center gap-6 text-sm">
{links.map((link, index) => (
<Link
key={index}
@@ -31,18 +33,8 @@ export default function FooterSection() {
</Link>
))}
</div>
<div className="my-8 flex flex-wrap justify-center items-center gap-2 text-sm">
<span className="text-muted-foreground">تماس با تیم فیپ:</span>
<a
href="tel:09162949770"
className="text-foreground hover:text-accent transition-colors duration-150 font-medium"
>
09162949770
</a>
</div>
<span className="text-muted-foreground block text-center text-sm">
{" "}
© {new Date().getFullYear()} فیپ، تمامی حقوق محفوظ است
© 1404 فیپ، تمامی حقوق محفوظ است
</span>
</div>
</footer>
+57 -48
View File
@@ -1,5 +1,6 @@
'use client'
import Link from 'next/link'
import { usePathname } from 'next/navigation'
import { Logo } from '@/components/logo'
import { Menu, X } from 'lucide-react'
import { Button } from '@/components/ui/button'
@@ -9,12 +10,19 @@ import { ThemeToggle } from '@/components/theme-toggle'
const menuItems = [
{ name: 'صفحه اصلی', href: '/' },
{ name: 'درباره ما', href: '/about' },
{ name: 'بلاگ', href: '/blog' },
{ name: 'راه حل‌ها', href: '/solutions' },
{ name: 'قیمت‌گذاری', href: '/pricing' },
{ name: 'درباره ما', href: '/about' },
]
function isLinkActive(pathname: string, href: string): boolean {
if (href === '/') return pathname === '/'
return pathname === href || pathname.startsWith(href + '/')
}
export const HeroHeader = () => {
const pathname = usePathname()
const [menuState, setMenuState] = React.useState(false)
const [isScrolled, setIsScrolled] = React.useState(false)
@@ -30,69 +38,70 @@ export const HeroHeader = () => {
<nav
data-state={menuState && 'active'}
className="fixed z-20 w-full px-2">
<div className={cn('mx-auto mt-2 max-w-6xl px-6 transition-all duration-300 lg:px-12 bg-background/80 backdrop-blur-md rounded-2xl border border-border/50', isScrolled && 'bg-background/95 max-w-4xl shadow-lg lg:px-5')}>
<div className="relative flex flex-wrap items-center justify-between gap-6 py-2 lg:gap-0 lg:py-3">
<div className={cn('mx-auto mt-2 max-w-6xl px-2 transition-all duration-300 lg:px-2 bg-background/30 backdrop-blur-md rounded-md border border-border/10', isScrolled && 'bg-background/20 max-w-4xl shadow-lg lg:px-5')}>
<div className="relative flex flex-wrap items-center justify-between gap-2 lg:gap-0">
<div className="flex w-full justify-between lg:w-auto">
<Link
href="/"
aria-label="home"
className="flex items-center space-x-2">
<Logo />
</Link>
<Logo />
<button
onClick={() => setMenuState(!menuState)}
aria-label={menuState == true ? 'بستن منو' : 'باز کردن منو'}
className="relative z-20 -m-2.5 -mr-4 block cursor-pointer p-2.5 lg:hidden">
<Menu className="in-data-[state=active]:rotate-180 in-data-[state=active]:scale-0 in-data-[state=active]:opacity-0 m-auto size-6 duration-200" />
<X className="in-data-[state=active]:rotate-0 in-data-[state=active]:scale-100 in-data-[state=active]:opacity-100 absolute inset-0 m-auto size-6 -rotate-180 scale-0 opacity-0 duration-200" />
<Menu className="in-data-[state=active]:rotate-180 in-data-[state=active]:scale-0 in-data-[state=active]:opacity-0 m-auto size-4 duration-200" />
<X className="in-data-[state=active]:rotate-0 in-data-[state=active]:scale-100 in-data-[state=active]:opacity-100 absolute inset-0 m-auto size-4 -rotate-180 scale-0 opacity-0 duration-200" />
</button>
</div>
<div className="absolute inset-0 m-auto hidden size-fit lg:flex items-center gap-8">
<ul className="flex gap-8 text-sm">
{menuItems.map((item, index) => (
<li key={index}>
<Link
href={item.href}
className="text-muted-foreground hover:text-foreground transition-all duration-300 relative group">
<span className="relative z-10">{item.name}</span>
<span className="absolute bottom-0 right-0 w-0 h-0.5 bg-accent transition-all duration-300 group-hover:w-full"></span>
</Link>
</li>
))}
</ul>
<ThemeToggle />
</div>
<div className="bg-background in-data-[state=active]:block lg:in-data-[state=active]:flex mb-6 hidden w-full flex-wrap items-center justify-end space-y-8 rounded-3xl border p-6 shadow-2xl shadow-zinc-300/20 md:flex-nowrap lg:m-0 lg:flex lg:w-fit lg:gap-6 lg:space-y-0 lg:border-transparent lg:bg-transparent lg:p-0 lg:shadow-none dark:shadow-none dark:lg:bg-transparent">
<div className="lg:hidden w-full">
<ul className="space-y-6 text-base">
{menuItems.map((item, index) => (
<div className="absolute inset-0 m-auto hidden size-fit lg:flex items-center">
<ul className="flex gap-2 text-sm">
{menuItems.map((item, index) => {
const active = isLinkActive(pathname ?? '', item.href)
return (
<li key={index}>
<Link
href={item.href}
className="text-muted-foreground hover:text-foreground transition-all duration-300 relative group block">
className={cn(
'text-muted-foreground hover:text-foreground hover:bg-accent/20 transition-all duration-300 relative group p-2 px-4 rounded-md',
active && 'text-foreground bg-accent/40'
)}>
<span className="relative z-10">{item.name}</span>
<span className="absolute bottom-0 right-0 w-0 h-0.5 bg-accent transition-all duration-300 group-hover:w-full"></span>
</Link>
</li>
))}
)
})}
</ul>
</div>
<div className="in-data-[state=active]:block lg:in-data-[state=active]:flex mb-3 hidden w-full flex-wrap items-center justify-end space-y-8 md:flex-nowrap lg:m-0 lg:flex lg:w-fit lg:gap-6 lg:space-y-0 lg:border-transparent lg:bg-transparent lg:p-0 lg:shadow-none dark:shadow-none dark:lg:bg-transparent">
<div className="lg:hidden w-full">
<ul className="text-base flex flex-col items-stretch">
{menuItems.map((item, index) => {
const active = isLinkActive(pathname ?? '', item.href)
return (
<li key={index}>
<Link
href={item.href}
onClick={() => setMenuState(false)}
className={cn(
'text-muted-foreground block w-full hover:text-foreground hover:bg-accent/20 transition-all duration-300 relative group p-2 px-4 rounded-md',
active && 'text-foreground bg-accent/40'
)}>
<span className="relative z-10">{item.name}</span>
</Link>
</li>
)
})}
</ul>
<div className="mt-6 flex justify-end">
<ThemeToggle />
</div>
</div>
<div className="flex w-full flex-col space-y-3 sm:flex-row sm:gap-3 sm:space-y-0 md:w-fit">
<Button
asChild
size="sm"
className={cn(isScrolled ? 'lg:inline-flex' : 'hidden', 'transition-all duration-300 hover:scale-105')}>
<Link href="tel:09162949770">
<span>تماس با فیپ</span>
</Link>
</Button>
</div>
</div>
<div className={`flex justify-end max-md:absolute top-0.5 left-8 in-data-[state=active]:block`}>
<ThemeToggle />
</div>
</div>
</div>
+7 -8
View File
@@ -168,14 +168,13 @@ export default function HeroSection() {
...transitionVariants,
}}
>
<div className="relative -mr-56 mt-8 overflow-hidden px-2 sm:mr-0 sm:mt-12 md:mt-20">
<div className="ring-background bg-background relative mx-auto max-w-6xl overflow-hidden rounded-2xl border ring-1 shadow-[0_20px_50px_rgba(59,130,246,0.4)]">
<div className="relative mt-8 overflow-hidden px-2 sm:mt-12 md:mt-20 p-2">
<div className="ring-background bg-background relative max-w-5xl mx-auto overflow-hidden rounded-2xl border ring-1 aspect-[3/2]">
<Image
className="bg-background relative w-full h-full rounded-2xl object-contain object-center"
src="/assets/main-pic/Untitled-1 copy-min.jpg"
alt="یکپارچه‌سازی سیستم‌ها و شبکه دیجیتال"
width={1150}
height={613}
fill
className="object-cover rounded-2xl inline-block my-0"
quality={100}
priority
/>
@@ -233,13 +232,13 @@ export default function HeroSection() {
alt="Microsoft Windows Logo"
size="md"
className="translate-x-2"
/>
<LogoItem
/>
<LogoItem
icon="skill-icons:github-dark"
alt="github Logo"
size="md"
className="translate-x-4"
/>
/>
</div>
</div>
</div>
+8 -8
View File
@@ -10,9 +10,9 @@ export const Logo = ({
uniColor?: boolean;
}) => {
return (
<Link href="/" className="flex items-center space-x-2">
<Link href="/" className="flex items-center">
<LogoIcon className={className} uniColor={uniColor} />
<span className="font-bold text-sm">فیپ</span>
<span className="font-bold text-xl">فـــیــپ</span>
</Link>
);
};
@@ -25,12 +25,12 @@ export const LogoIcon = ({
uniColor?: boolean;
}) => {
return (
<Image
src="/assets/feep-Logo.svg"
alt="Logo"
width={50}
height={50}
className={cn("h-10 w-auto md:h-12 dark:invert", className)}
<Image
src="/assets/feep-Logo.svg"
alt="Logo"
width={50}
height={50}
className={cn("h-10 w-auto md:h-12 dark:invert", className)}
/>
);
};
+54
View File
@@ -0,0 +1,54 @@
"use client";
import { useEffect, useState } from "react";
import { ChevronUp } from "lucide-react";
import { Button } from "@/components/ui/button";
import { cn } from "@/lib/utils";
const SCROLL_THRESHOLD = 0.4; // show after 40% scroll
export function ScrollToTop({ className }: { className?: string }) {
const [visible, setVisible] = useState(false);
useEffect(() => {
function onScroll() {
const maxScroll =
document.documentElement.scrollHeight - window.innerHeight;
if (maxScroll <= 0) {
setVisible(false);
return;
}
const ratio = window.scrollY / maxScroll;
setVisible(ratio >= SCROLL_THRESHOLD);
}
onScroll();
window.addEventListener("scroll", onScroll, { passive: true });
return () => window.removeEventListener("scroll", onScroll);
}, []);
const scrollToTop = () => {
window.scrollTo({ top: 0, behavior: "smooth" });
};
if (!visible) return null;
return (
<div
className={cn(
"fixed bottom-6 start-6 z-50 transition-opacity duration-300",
className
)}
>
<Button
size="icon"
variant="outline"
onClick={scrollToTop}
aria-label="برگشت به بالا"
className="size-10 rounded-md shadow-md"
>
<ChevronUp className="size-5" />
</Button>
</div>
);
}
+104
View File
@@ -0,0 +1,104 @@
"use client";
import { usePathname } from "next/navigation";
import { useCallback, useEffect, useRef, useState } from "react";
const TOP_BAR_HEIGHT = 3;
const MAX_PERCENT = 90;
const COMPLETE_PERCENT = 100;
const TICK_MS = 50;
const INCREMENT = 2;
const COMPLETE_DURATION = 200;
export function TopProgressBar() {
const pathname = usePathname();
const [visible, setVisible] = useState(false);
const [percent, setPercent] = useState(0);
const timerRef = useRef<ReturnType<typeof setInterval> | null>(null);
const completeTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);
const prevPathnameRef = useRef(pathname);
const clearTimers = useCallback(() => {
if (timerRef.current) {
clearInterval(timerRef.current);
timerRef.current = null;
}
if (completeTimeoutRef.current) {
clearTimeout(completeTimeoutRef.current);
completeTimeoutRef.current = null;
}
}, []);
const startProgress = useCallback(() => {
clearTimers();
setVisible(true);
setPercent(0);
timerRef.current = setInterval(() => {
setPercent((p) => {
if (p >= MAX_PERCENT) {
if (timerRef.current) {
clearInterval(timerRef.current);
timerRef.current = null;
}
return MAX_PERCENT;
}
return Math.min(p + INCREMENT, MAX_PERCENT);
});
}, TICK_MS);
}, [clearTimers]);
const completeProgress = useCallback(() => {
clearTimers();
setPercent(COMPLETE_PERCENT);
completeTimeoutRef.current = setTimeout(() => {
setVisible(false);
setPercent(0);
completeTimeoutRef.current = null;
}, COMPLETE_DURATION);
}, [clearTimers]);
// Start bar when user clicks an internal link
useEffect(() => {
const handleClick = (e: MouseEvent) => {
const target = (e.target as Element).closest("a");
if (!target?.href) return;
try {
const url = new URL(target.href);
if (url.origin === window.location.origin && url.pathname !== pathname) {
startProgress();
}
} catch {
// ignore invalid URLs
}
};
document.addEventListener("click", handleClick, true);
return () => document.removeEventListener("click", handleClick, true);
}, [pathname, startProgress]);
// Complete bar when pathname changes (route loaded)
useEffect(() => {
if (prevPathnameRef.current !== pathname) {
prevPathnameRef.current = pathname;
if (visible) completeProgress();
}
}, [pathname, visible, completeProgress]);
useEffect(() => {
return clearTimers;
}, [clearTimers]);
if (!visible) return null;
return (
<div
className="fixed left-0 right-0 z-[9999] overflow-hidden bg-transparent"
style={{ height: TOP_BAR_HEIGHT }}
aria-hidden
>
<div
className="h-full bg-primary transition-[width] duration-150 ease-out"
style={{ width: `${percent}%` }}
/>
</div>
);
}
+336
View File
@@ -0,0 +1,336 @@
/**
* زیرساخت کشف بلاک‌های محتوای بلاگ و تبدیل به JSON.
* مرحله ۱: کشف بلاک‌های شناخته‌شده در HTML (collapse, و بعداً blockquote, code, ...)
* مرحله ۲: استخراج payload به صورت JSON برای هر بلاک
* مرحله ۳: لایه بیرونی بر اساس blockType و payload کامپوننت مناسب را رندر می‌کند.
*
* برای افزودن بلاک جدید: یک تعریف در BLOCK_DEFINITIONS اضافه کنید و در رندر blockType را هندل کنید.
*/
import * as cheerio from "cheerio";
import type { Element } from "domhandler";
// ─── انواع بلاک (برای گسترش بعدی) ───────────────────────────────────────────
/** نام نوع بلاک؛ با افزودن بلاک جدید این یونیون را گسترش دهید. */
export type BlockType = "collapse" | "blockquote" | "pullquote" | "tip" | "code";
/** Payload بلاک collapse (سوالات متداول / accordion). */
export interface CollapseBlockPayload {
title: string;
items: { question: string; answer: string }[];
}
/** Payload بلاک blockquote (نقل‌قول و منبع). content به صورت HTML است و باید با dangerouslySetInnerHTML رندر شود. */
export interface BlockquoteBlockPayload {
/** HTML محتوای نقل‌قول (بدون تگ cite). */
content: string;
citation?: string;
}
/** Payload بلاک tip (نکته / اینفو با variant: info, warn, danger). */
export interface TipBlockPayload {
variant: "info" | "warn" | "danger";
/** HTML محتوای داخل بلاک (عنوان + متن). */
content: string;
}
/** Payload بلاک code — بعداً پیاده‌سازی می‌شود. */
// export interface CodeBlockPayload { ... }
/** یونیون همهٔ payloadها برای تایپ‌امن رندر. */
export type BlockPayload =
| CollapseBlockPayload
| BlockquoteBlockPayload
| TipBlockPayload;
// | CodeBlockPayload
// ─── سگمنت خروجی برای لایه رندر ─────────────────────────────────────────────
/** یک سگمنت HTML خام. */
export interface HtmlSegment {
type: "html";
content: string;
}
/** یک بلاک با نوع و payload (JSON) برای رندر با کامپوننت مناسب. */
export interface BlockSegment {
type: "block";
blockType: BlockType;
payload: BlockPayload;
}
/** هر سگمنت محتوا: یا HTML یا یک بلاک با JSON. */
export type ContentSegment = HtmlSegment | BlockSegment;
/** خروجی کشف بلاک: HTML با placeholderها و آرایهٔ سگمنت‌ها (با payload توکار). */
export interface DiscoveredContent {
/** HTML بعد از جایگزینی بلاک‌ها با placeholder (برای ادامهٔ پردازش مثلاً تزریق id سرنویس). */
htmlWithPlaceholders: string;
/** سگمنت‌ها به ترتیب برای رندر؛ هر بلاک شامل blockType و payload است. */
segments: ContentSegment[];
}
// ─── تعریف بلاک‌ها (ثبت برای کشف و استخراج) ────────────────────────────────
export interface BlockDefinition<T extends BlockPayload = BlockPayload> {
blockType: BlockType;
/** سلکتور Cheerio برای پیدا کردن المنت ریشهٔ بلاک. */
selector: string;
/** استخراج payload (JSON) از المنت. */
extract: ($: cheerio.CheerioAPI, el: Element) => T;
}
function extractCollapsePayload(
$: cheerio.CheerioAPI,
el: Element
): CollapseBlockPayload {
const $el = $(el);
const innerHtml = $el.html() ?? "";
const $inner = cheerio.load(innerHtml, null, false);
const title = $inner("h2").first().text().trim();
const items: { question: string; answer: string }[] = [];
$inner("ul").each((_: number, ul: Element) => {
const question = $inner(ul).find("li strong").first().text().trim();
const answer = $inner(ul).nextAll("p").first().text().trim();
if (question || answer) items.push({ question, answer });
});
return { title, items };
}
/**
* استخراج از بلاک آکاردئون وردپرس (core/accordion):
* div.wp-block-accordion > div.wp-block-accordion-item با
* .wp-block-accordion-heading__toggle-title (سؤال) و .wp-block-accordion-panel (جواب به صورت HTML).
*/
function extractAccordionPayload(
$: cheerio.CheerioAPI,
el: Element
): CollapseBlockPayload {
const $el = $(el);
const items: { question: string; answer: string }[] = [];
$el.find(".wp-block-accordion-item").each((_: number, itemEl: Element) => {
const $item = $(itemEl);
const question = $item.find(".wp-block-accordion-heading__toggle-title").first().text().trim();
const $panel = $item.find(".wp-block-accordion-panel").first();
const answer = ($panel.html() ?? $panel.text() ?? "").trim();
if (question || answer) items.push({ question, answer });
});
return { title: "", items };
}
function extractBlockquotePayload(
$: cheerio.CheerioAPI,
el: Element
): BlockquoteBlockPayload {
const $el = $(el);
const citation = $el.find("cite").first().text().trim() || undefined;
const $body = $el.clone();
$body.find("cite").remove();
const content = ($body.html() ?? "").trim();
return { content, citation };
}
/**
* استخراج از بلاک pullquote وردپرس: figure.wp-block-pullquote > blockquote > p + cite.
* همان شکل blockquote (content HTML + citation).
*/
function extractPullquotePayload(
$: cheerio.CheerioAPI,
el: Element
): BlockquoteBlockPayload {
const $el = $(el);
const $blockquote = $el.find("blockquote").first();
const citation = $blockquote.find("cite").first().text().trim() || undefined;
const $body = $blockquote.clone();
$body.find("cite").remove();
const content = ($body.html() ?? "").trim();
return { content, citation };
}
/**
* استخراج از بلاک tip وردپرس: div.wp-block-group.tip با کلاس tip-info | tip-warn | tip-danger.
*/
function extractTipPayload($: cheerio.CheerioAPI, el: Element): TipBlockPayload {
const $el = $(el);
const classList = ($el.attr("class") ?? "").split(/\s+/);
let variant: "info" | "warn" | "danger" = "info";
if (classList.includes("tip-danger")) variant = "danger";
else if (classList.includes("tip-warn")) variant = "warn";
else if (classList.includes("tip-info")) variant = "info";
const content = ($el.html() ?? "").trim();
return { variant, content };
}
/**
* ثبت همهٔ بلاک‌های شناخته‌شده.
* برای بلاک‌های جدید (مثلاً blockquote, code) فقط یک مورد جدید به این آرایه اضافه کنید.
*/
export const BLOCK_DEFINITIONS: BlockDefinition[] = [
{
blockType: "collapse",
selector: "div.wp-block-group.collapse-section",
extract: extractCollapsePayload as BlockDefinition["extract"],
},
{
blockType: "collapse",
selector: "div.wp-block-accordion",
extract: extractAccordionPayload as BlockDefinition["extract"],
},
{
blockType: "blockquote",
selector: "blockquote.wp-block-quote",
extract: extractBlockquotePayload as BlockDefinition["extract"],
},
{
blockType: "pullquote",
selector: "figure.wp-block-pullquote",
extract: extractPullquotePayload as BlockDefinition["extract"],
},
{
blockType: "tip",
selector: "div.wp-block-group.tip",
extract: extractTipPayload as BlockDefinition["extract"],
},
// { blockType: "code", selector: "pre.wp-block-code", extract: extractCodePayload },
];
// ─── کشف بلاک و ساخت سگمنت‌ها ─────────────────────────────────────────────
const PLACEHOLDER_REGEX =
/<div data-blog-block-type="([^"]+)" data-blog-block-id="(\d+)"[^>]*><\/div>/g;
function isElement(node: unknown): node is Element {
return typeof node === "object" && node !== null && (node as Element).type === "tag";
}
/**
* ترتیب قرارگیری المنت در سند را با یک مپ نگه می‌دارد.
*/
function buildDocumentOrderMap($: cheerio.CheerioAPI): Map<Element, number> {
const map = new Map<Element, number>();
let index = 0;
$("*")
.toArray()
.filter(isElement)
.forEach((el) => {
map.set(el, index++);
});
return map;
}
/**
* مرحله ۱ و ۲: از روی HTML محتوا بلاک‌های شناخته‌شده را کشف می‌کند،
* به JSON (payload) تبدیل می‌کند، و خروجی را به صورت HTML با placeholder و
* آرایهٔ segments (با payload توکار) به لایه بیرونی می‌دهد.
*/
export function discoverBlocks(html: string): DiscoveredContent {
const $ = cheerio.load(html, null, false);
const orderMap = buildDocumentOrderMap($);
type BlockEntry = {
el: Element;
blockType: BlockType;
blockId: number;
payload: BlockPayload;
};
const entries: BlockEntry[] = [];
const blocksByType: Record<string, BlockPayload[]> = {
collapse: [],
blockquote: [],
pullquote: [],
tip: [],
code: [],
};
for (const def of BLOCK_DEFINITIONS) {
const elements = $(def.selector).toArray().filter(isElement);
for (const el of elements) {
const payload = def.extract($, el) as BlockPayload;
const blockId = blocksByType[def.blockType].length;
blocksByType[def.blockType].push(payload);
entries.push({
el,
blockType: def.blockType,
blockId,
payload,
});
}
}
// مرتب‌سازی بر اساس ترتیب در سند (از آخر به اول جایگزین می‌کنیم تا ایندکس‌ها جابجا نشوند)
entries.sort((a, b) => orderMap.get(a.el)! - orderMap.get(b.el)!);
// جایگزینی با placeholder از آخر به اول
for (let i = entries.length - 1; i >= 0; i--) {
const { el, blockType, blockId } = entries[i];
$(el).replaceWith(
`<div data-blog-block-type="${blockType}" data-blog-block-id="${blockId}"></div>`
);
}
const htmlWithPlaceholders = $.html() ?? html;
// ساخت segments: تقسیم با placeholder و قرار دادن payload در هر بلاک
const segments: ContentSegment[] = [];
let lastEnd = 0;
const regex = new RegExp(PLACEHOLDER_REGEX.source, "g");
let m: RegExpExecArray | null;
while ((m = regex.exec(htmlWithPlaceholders)) !== null) {
if (m.index > lastEnd) {
segments.push({
type: "html",
content: htmlWithPlaceholders.slice(lastEnd, m.index),
});
}
const blockType = m[1] as BlockType;
const blockId = parseInt(m[2], 10);
const list = blocksByType[blockType];
if (list && blockId >= 0 && blockId < list.length) {
segments.push({
type: "block",
blockType,
payload: list[blockId],
});
}
lastEnd = regex.lastIndex;
}
if (lastEnd < htmlWithPlaceholders.length) {
segments.push({
type: "html",
content: htmlWithPlaceholders.slice(lastEnd),
});
}
return { htmlWithPlaceholders, segments };
}
/**
* بعد از تزریق id سرنویس‌ها در HTML، محتوای هر سگمنت html را با نسخهٔ به‌روز (شامل idها) جایگزین می‌کند.
* لایهٔ blog-utils بعد از inject کردن idها این تابع را صدا می‌زند.
*/
export function mergeHeadingIdsIntoContentSegments(
contentWithIds: string,
segments: ContentSegment[]
): ContentSegment[] {
const parts: string[] = [];
let last = 0;
let m: RegExpExecArray | null;
const regex = new RegExp(PLACEHOLDER_REGEX.source, "g");
while ((m = regex.exec(contentWithIds)) !== null) {
parts.push(contentWithIds.slice(last, m.index));
last = regex.lastIndex;
}
parts.push(contentWithIds.slice(last));
let j = 0;
return segments.map((seg) => {
if (seg.type === "html") {
return { ...seg, content: parts[j++] ?? seg.content };
}
return seg;
});
}
+96
View File
@@ -0,0 +1,96 @@
import {
discoverBlocks,
mergeHeadingIdsIntoContentSegments,
type ContentSegment,
} from "@/lib/blog-blocks";
export type { ContentSegment } from "@/lib/blog-blocks";
/**
* Creates a URL-safe slug from heading text (strip HTML, normalize, Persian-friendly).
*/
function slugify(text: string): string {
const stripped = text.replace(/<[^>]*>/g, "").trim();
return stripped
.replace(/\s+/g, "-")
.replace(/[^\p{L}\p{N}-]/gu, "")
.replace(/-+/g, "-")
.replace(/^-|-$/g, "")
.toLowerCase() || "section";
}
export interface TocItem {
id: string;
text: string;
level: number; // 2 = h2, 3 = h3, 4 = h4
}
const HEADING_REGEX = /<(h[234])[^>]*>([\s\S]*?)<\/\1>/gi;
const usedIds = new Map<string, number>();
function ensureUniqueId(baseId: string): string {
const count = usedIds.get(baseId) ?? 0;
usedIds.set(baseId, count + 1);
return count === 0 ? baseId : `${baseId}-${count}`;
}
/**
* پارس محتوا: ابتدا بلاک‌ها کشف و به JSON تبدیل می‌شوند (blog-blocks)،
* سپس id سرنویس‌ها تزریق می‌شود و سگمنت‌های نهایی برای رندر برمی‌گردند.
*/
export function parseContentHeadings(html: string): {
contentWithIds: string;
toc: TocItem[];
segments: ContentSegment[];
} {
const { htmlWithPlaceholders, segments: rawSegments } = discoverBlocks(html);
usedIds.clear();
const toc: TocItem[] = [];
let lastIndex = 0;
let match: RegExpExecArray | null;
const parts: string[] = [];
const regex = new RegExp(HEADING_REGEX.source, "gi");
while ((match = regex.exec(htmlWithPlaceholders)) !== null) {
const [fullMatch, tag, inner] = match;
const level = parseInt(tag.charAt(1), 10) as 2 | 3 | 4;
const text = inner.replace(/<[^>]*>/g, "").trim();
const baseId = slugify(text);
const id = ensureUniqueId(baseId);
toc.push({ id, text, level });
const openTag = fullMatch.slice(0, fullMatch.indexOf(">") + 1);
const hasId = /^\s*<h[234][^>]*\bid\s*=/i.test(openTag);
const newOpenTag = hasId
? openTag
: openTag.replace(/^(<\s*h[234])(\s|>)/i, `$1 id="${id}"$2`);
parts.push(htmlWithPlaceholders.slice(lastIndex, match.index));
parts.push(newOpenTag + inner + `</${tag}>`);
lastIndex = regex.lastIndex;
}
parts.push(htmlWithPlaceholders.slice(lastIndex));
const contentWithIds = parts.join("");
const segments = mergeHeadingIdsIntoContentSegments(
contentWithIds,
rawSegments
);
return { contentWithIds, toc, segments };
}
/** Average reading speed in words per minute (Persian/English mix). */
const WPM = 200;
/**
* Estimates reading time in minutes from HTML content.
*/
export function getReadingTimeMinutes(html: string): number {
const text = html.replace(/<[^>]*>/g, " ").replace(/\s+/g, " ").trim();
const wordCount = text.split(/\s+/).filter(Boolean).length;
return Math.max(1, Math.ceil(wordCount / WPM));
}
+15
View File
@@ -0,0 +1,15 @@
import { format, newDate } from "date-fns-jalali";
import { faIR } from "date-fns-jalali/locale";
/**
* Format a Gregorian date (Date or ISO string) to Shamsi (Jalali) Persian string.
* Example: "۱۴ بهمن ۱۴۰۴"
*/
export function formatDateShamsi(
date: Date | string,
formatStr: string = "d MMMM yyyy"
): string {
const d = typeof date === "string" ? new Date(date) : date;
if (Number.isNaN(d.getTime())) return "";
return format(d, formatStr, { locale: faIR });
}
+169
View File
@@ -0,0 +1,169 @@
const WP_API_BASE = "https://wpa.feep.ir/wp-json/wp/v2";
const FETCH_TIMEOUT_MS = 15_000;
export interface WpMediaSize {
source_url: string;
width?: number;
height?: number;
}
export interface WpFeaturedMedia {
source_url: string;
alt_text?: string;
media_details?: {
width?: number;
height?: number;
sizes?: Record<string, WpMediaSize>;
};
}
export interface WpAuthor {
id: number;
name: string;
slug?: string;
}
export interface WpPost {
id: number;
date: string;
modified?: string;
slug: string;
title: { rendered: string };
content: { rendered: string };
excerpt: { rendered: string };
featured_media: number;
categories: number[];
author?: number;
_embedded?: {
"wp:featuredmedia"?: WpFeaturedMedia[];
author?: WpAuthor[];
};
}
/** Returns thumbnail URL for list/card (medium) or full for single post. */
export function getFeaturedImageUrl(
post: WpPost,
size: "medium" | "medium_large" | "full" = "medium"
): string | null {
const media = post._embedded?.["wp:featuredmedia"]?.[0];
if (!media?.source_url) return null;
const sizes = media.media_details?.sizes;
if (sizes?.[size]?.source_url) return sizes[size].source_url;
if (size !== "full" && sizes?.medium?.source_url) return sizes.medium.source_url;
return media.source_url;
}
export interface WpCategory {
id: number;
name: string;
slug: string;
count: number;
}
export interface GetPostsResult {
posts: WpPost[];
total: number;
totalPages: number;
}
export interface GetPostsOptions {
page?: number;
perPage?: number;
search?: string;
categoryId?: number;
}
async function fetchWithTimeout(
url: string,
options?: RequestInit
): Promise<Response> {
const controller = new AbortController();
const timeout = setTimeout(() => controller.abort(), FETCH_TIMEOUT_MS);
try {
const res = await fetch(url, {
...options,
signal: controller.signal,
next: { revalidate: 0 },
});
clearTimeout(timeout);
return res;
} catch (e) {
clearTimeout(timeout);
throw e;
}
}
export async function getPosts(
options: GetPostsOptions = {}
): Promise<GetPostsResult> {
const { page = 1, perPage = 9, search, categoryId } = options;
const params = new URLSearchParams();
params.set("page", String(page));
params.set("per_page", String(perPage));
if (search?.trim()) params.set("search", search.trim());
if (categoryId) params.set("categories", String(categoryId));
params.set("_embed", "1");
const url = `${WP_API_BASE}/posts?${params.toString()}`;
const res = await fetchWithTimeout(url);
if (!res.ok) {
throw new Error(`WP API error: ${res.status} ${res.statusText}`);
}
const total = parseInt(res.headers.get("x-wp-total") ?? "0", 10);
const totalPages = parseInt(res.headers.get("x-wp-totalpages") ?? "1", 10);
const posts: WpPost[] = await res.json();
return { posts, total, totalPages };
}
export async function getPostBySlug(slug: string): Promise<WpPost | null> {
const decodedSlug = decodeURIComponent(slug);
const params = new URLSearchParams({ slug: decodedSlug, _embed: "1" });
const url = `${WP_API_BASE}/posts?${params.toString()}`;
const res = await fetchWithTimeout(url);
if (!res.ok) {
throw new Error(`WP API error: ${res.status} ${res.statusText}`);
}
const posts: WpPost[] = await res.json();
return posts.length > 0 ? posts[0] : null;
}
export async function getRelatedPosts(
postId: number,
categoryIds: number[],
limit = 3
): Promise<WpPost[]> {
const params = new URLSearchParams();
params.set("exclude", String(postId));
params.set("per_page", String(limit));
params.set("_embed", "1");
if (categoryIds.length > 0) {
params.set("categories", String(categoryIds[0]));
}
const url = `${WP_API_BASE}/posts?${params.toString()}`;
const res = await fetchWithTimeout(url);
if (!res.ok) {
throw new Error(`WP API error: ${res.status} ${res.statusText}`);
}
const posts: WpPost[] = await res.json();
return posts;
}
export async function getCategories(): Promise<WpCategory[]> {
const url = `${WP_API_BASE}/categories?per_page=100`;
const res = await fetchWithTimeout(url);
if (!res.ok) {
throw new Error(`WP API error: ${res.status} ${res.statusText}`);
}
const data: WpCategory[] = await res.json();
return data.filter((c) => c.count > 0);
}
+4
View File
@@ -12,6 +12,10 @@ const nextConfig: NextConfig = {
protocol: "https",
hostname: "images.unsplash.com",
},
{
protocol: "https",
hostname: "wpa.feep.ir",
},
],
},
typescript: {
+2
View File
@@ -9,6 +9,7 @@
"lint": "eslint"
},
"dependencies": {
"cheerio": "^1.0.0",
"@hookform/resolvers": "^5.2.2",
"@iconify/react": "^6.0.2",
"@radix-ui/react-accordion": "^1.2.12",
@@ -41,6 +42,7 @@
"clsx": "^2.1.1",
"cmdk": "^1.1.1",
"date-fns": "^4.1.0",
"date-fns-jalali": "^4.1.0-0",
"embla-carousel-react": "^8.6.0",
"input-otp": "^1.4.2",
"lucide-react": "^0.555.0",
+462 -63
View File
@@ -21,7 +21,7 @@
resolved "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.28.5.tgz"
integrity sha512-6uFXyCayocRbqhZOB+6XcuZbkMNimwfVGFji8CTZnCzOHVGvDqzvitu1re2AU5LROliz7eQPhB8CpAMvnx9EjA==
"@babel/core@^7.0.0", "@babel/core@^7.24.4":
"@babel/core@^7.24.4":
version "7.28.5"
resolved "https://registry.npmjs.org/@babel/core/-/core-7.28.5.tgz"
integrity sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw==
@@ -164,6 +164,14 @@
"@emnapi/wasi-threads" "1.1.0"
tslib "^2.4.0"
"@emnapi/core@^1.6.0", "@emnapi/core@^1.7.1":
version "1.8.1"
resolved "https://registry.yarnpkg.com/@emnapi/core/-/core-1.8.1.tgz#fd9efe721a616288345ffee17a1f26ac5dd01349"
integrity sha512-AvT9QFpxK0Zd8J0jopedNm+w/2fIzvtPKPjqyw9jwvBaReTTqPBk9Hixaz7KbjimP+QNz605/XnjFcDAL2pqBg==
dependencies:
"@emnapi/wasi-threads" "1.1.0"
tslib "^2.4.0"
"@emnapi/runtime@^1.4.3":
version "1.7.1"
resolved "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.7.1.tgz"
@@ -171,9 +179,16 @@
dependencies:
tslib "^2.4.0"
"@emnapi/wasi-threads@1.1.0":
"@emnapi/runtime@^1.6.0", "@emnapi/runtime@^1.7.0", "@emnapi/runtime@^1.7.1":
version "1.8.1"
resolved "https://registry.yarnpkg.com/@emnapi/runtime/-/runtime-1.8.1.tgz#550fa7e3c0d49c5fb175a116e8cd70614f9a22a5"
integrity sha512-mehfKSMWjjNol8659Z8KxEMrdSJDDot5SXMq00dM8BN4o+CLNXQ0xH2V7EchNHV4RmbZLmmPdEaXZc5H2FXmDg==
dependencies:
tslib "^2.4.0"
"@emnapi/wasi-threads@1.1.0", "@emnapi/wasi-threads@^1.1.0":
version "1.1.0"
resolved "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.1.0.tgz"
resolved "https://registry.yarnpkg.com/@emnapi/wasi-threads/-/wasi-threads-1.1.0.tgz#60b2102fddc9ccb78607e4a3cf8403ea69be41bf"
integrity sha512-WI0DdZ8xFSbgMjR1sFsKABJ/C5OnRrjT06JXbZKexJGrDuPTzZdDYfFlsgcCXCyf+suG5QU2e/y1Wo2V/OapLQ==
dependencies:
tslib "^2.4.0"
@@ -320,6 +335,143 @@
resolved "https://registry.npmjs.org/@img/colour/-/colour-1.0.0.tgz"
integrity sha512-A5P/LfWGFSl6nsckYtjw9da+19jB8hkJ6ACTGcDfEJ0aE+l2n2El7dsVM7UVHZQ9s2lmYMWlrS21YLy2IR1LUw==
"@img/sharp-darwin-arm64@0.34.5":
version "0.34.5"
resolved "https://registry.yarnpkg.com/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.34.5.tgz#6e0732dcade126b6670af7aa17060b926835ea86"
integrity sha512-imtQ3WMJXbMY4fxb/Ndp6HBTNVtWCUI0WdobyheGf5+ad6xX8VIDO8u2xE4qc/fr08CKG/7dDseFtn6M6g/r3w==
optionalDependencies:
"@img/sharp-libvips-darwin-arm64" "1.2.4"
"@img/sharp-darwin-x64@0.34.5":
version "0.34.5"
resolved "https://registry.yarnpkg.com/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.34.5.tgz#19bc1dd6eba6d5a96283498b9c9f401180ee9c7b"
integrity sha512-YNEFAF/4KQ/PeW0N+r+aVVsoIY0/qxxikF2SWdp+NRkmMB7y9LBZAVqQ4yhGCm/H3H270OSykqmQMKLBhBJDEw==
optionalDependencies:
"@img/sharp-libvips-darwin-x64" "1.2.4"
"@img/sharp-libvips-darwin-arm64@1.2.4":
version "1.2.4"
resolved "https://registry.yarnpkg.com/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.2.4.tgz#2894c0cb87d42276c3889942e8e2db517a492c43"
integrity sha512-zqjjo7RatFfFoP0MkQ51jfuFZBnVE2pRiaydKJ1G/rHZvnsrHAOcQALIi9sA5co5xenQdTugCvtb1cuf78Vf4g==
"@img/sharp-libvips-darwin-x64@1.2.4":
version "1.2.4"
resolved "https://registry.yarnpkg.com/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.2.4.tgz#e63681f4539a94af9cd17246ed8881734386f8cc"
integrity sha512-1IOd5xfVhlGwX+zXv2N93k0yMONvUlANylbJw1eTah8K/Jtpi15KC+WSiaX/nBmbm2HxRM1gZ0nSdjSsrZbGKg==
"@img/sharp-libvips-linux-arm64@1.2.4":
version "1.2.4"
resolved "https://registry.yarnpkg.com/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.2.4.tgz#b1b288b36864b3bce545ad91fa6dadcf1a4ad318"
integrity sha512-excjX8DfsIcJ10x1Kzr4RcWe1edC9PquDRRPx3YVCvQv+U5p7Yin2s32ftzikXojb1PIFc/9Mt28/y+iRklkrw==
"@img/sharp-libvips-linux-arm@1.2.4":
version "1.2.4"
resolved "https://registry.yarnpkg.com/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.2.4.tgz#b9260dd1ebe6f9e3bdbcbdcac9d2ac125f35852d"
integrity sha512-bFI7xcKFELdiNCVov8e44Ia4u2byA+l3XtsAj+Q8tfCwO6BQ8iDojYdvoPMqsKDkuoOo+X6HZA0s0q11ANMQ8A==
"@img/sharp-libvips-linux-ppc64@1.2.4":
version "1.2.4"
resolved "https://registry.yarnpkg.com/@img/sharp-libvips-linux-ppc64/-/sharp-libvips-linux-ppc64-1.2.4.tgz#4b83ecf2a829057222b38848c7b022e7b4d07aa7"
integrity sha512-FMuvGijLDYG6lW+b/UvyilUWu5Ayu+3r2d1S8notiGCIyYU/76eig1UfMmkZ7vwgOrzKzlQbFSuQfgm7GYUPpA==
"@img/sharp-libvips-linux-riscv64@1.2.4":
version "1.2.4"
resolved "https://registry.yarnpkg.com/@img/sharp-libvips-linux-riscv64/-/sharp-libvips-linux-riscv64-1.2.4.tgz#880b4678009e5a2080af192332b00b0aaf8a48de"
integrity sha512-oVDbcR4zUC0ce82teubSm+x6ETixtKZBh/qbREIOcI3cULzDyb18Sr/Wcyx7NRQeQzOiHTNbZFF1UwPS2scyGA==
"@img/sharp-libvips-linux-s390x@1.2.4":
version "1.2.4"
resolved "https://registry.yarnpkg.com/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.2.4.tgz#74f343c8e10fad821b38f75ced30488939dc59ec"
integrity sha512-qmp9VrzgPgMoGZyPvrQHqk02uyjA0/QrTO26Tqk6l4ZV0MPWIW6LTkqOIov+J1yEu7MbFQaDpwdwJKhbJvuRxQ==
"@img/sharp-libvips-linux-x64@1.2.4":
version "1.2.4"
resolved "https://registry.yarnpkg.com/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.2.4.tgz#df4183e8bd8410f7d61b66859a35edeab0a531ce"
integrity sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw==
"@img/sharp-libvips-linuxmusl-arm64@1.2.4":
version "1.2.4"
resolved "https://registry.yarnpkg.com/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.2.4.tgz#c8d6b48211df67137541007ee8d1b7b1f8ca8e06"
integrity sha512-FVQHuwx1IIuNow9QAbYUzJ+En8KcVm9Lk5+uGUQJHaZmMECZmOlix9HnH7n1TRkXMS0pGxIJokIVB9SuqZGGXw==
"@img/sharp-libvips-linuxmusl-x64@1.2.4":
version "1.2.4"
resolved "https://registry.yarnpkg.com/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.2.4.tgz#be11c75bee5b080cbee31a153a8779448f919f75"
integrity sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg==
"@img/sharp-linux-arm64@0.34.5":
version "0.34.5"
resolved "https://registry.yarnpkg.com/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.34.5.tgz#7aa7764ef9c001f15e610546d42fce56911790cc"
integrity sha512-bKQzaJRY/bkPOXyKx5EVup7qkaojECG6NLYswgktOZjaXecSAeCWiZwwiFf3/Y+O1HrauiE3FVsGxFg8c24rZg==
optionalDependencies:
"@img/sharp-libvips-linux-arm64" "1.2.4"
"@img/sharp-linux-arm@0.34.5":
version "0.34.5"
resolved "https://registry.yarnpkg.com/@img/sharp-linux-arm/-/sharp-linux-arm-0.34.5.tgz#5fb0c3695dd12522d39c3ff7a6bc816461780a0d"
integrity sha512-9dLqsvwtg1uuXBGZKsxem9595+ujv0sJ6Vi8wcTANSFpwV/GONat5eCkzQo/1O6zRIkh0m/8+5BjrRr7jDUSZw==
optionalDependencies:
"@img/sharp-libvips-linux-arm" "1.2.4"
"@img/sharp-linux-ppc64@0.34.5":
version "0.34.5"
resolved "https://registry.yarnpkg.com/@img/sharp-linux-ppc64/-/sharp-linux-ppc64-0.34.5.tgz#9c213a81520a20caf66978f3d4c07456ff2e0813"
integrity sha512-7zznwNaqW6YtsfrGGDA6BRkISKAAE1Jo0QdpNYXNMHu2+0dTrPflTLNkpc8l7MUP5M16ZJcUvysVWWrMefZquA==
optionalDependencies:
"@img/sharp-libvips-linux-ppc64" "1.2.4"
"@img/sharp-linux-riscv64@0.34.5":
version "0.34.5"
resolved "https://registry.yarnpkg.com/@img/sharp-linux-riscv64/-/sharp-linux-riscv64-0.34.5.tgz#cdd28182774eadbe04f62675a16aabbccb833f60"
integrity sha512-51gJuLPTKa7piYPaVs8GmByo7/U7/7TZOq+cnXJIHZKavIRHAP77e3N2HEl3dgiqdD/w0yUfiJnII77PuDDFdw==
optionalDependencies:
"@img/sharp-libvips-linux-riscv64" "1.2.4"
"@img/sharp-linux-s390x@0.34.5":
version "0.34.5"
resolved "https://registry.yarnpkg.com/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.34.5.tgz#93eac601b9f329bb27917e0e19098c722d630df7"
integrity sha512-nQtCk0PdKfho3eC5MrbQoigJ2gd1CgddUMkabUj+rBevs8tZ2cULOx46E7oyX+04WGfABgIwmMC0VqieTiR4jg==
optionalDependencies:
"@img/sharp-libvips-linux-s390x" "1.2.4"
"@img/sharp-linux-x64@0.34.5":
version "0.34.5"
resolved "https://registry.yarnpkg.com/@img/sharp-linux-x64/-/sharp-linux-x64-0.34.5.tgz#55abc7cd754ffca5002b6c2b719abdfc846819a8"
integrity sha512-MEzd8HPKxVxVenwAa+JRPwEC7QFjoPWuS5NZnBt6B3pu7EG2Ge0id1oLHZpPJdn3OQK+BQDiw9zStiHBTJQQQQ==
optionalDependencies:
"@img/sharp-libvips-linux-x64" "1.2.4"
"@img/sharp-linuxmusl-arm64@0.34.5":
version "0.34.5"
resolved "https://registry.yarnpkg.com/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.34.5.tgz#d6515ee971bb62f73001a4829b9d865a11b77086"
integrity sha512-fprJR6GtRsMt6Kyfq44IsChVZeGN97gTD331weR1ex1c1rypDEABN6Tm2xa1wE6lYb5DdEnk03NZPqA7Id21yg==
optionalDependencies:
"@img/sharp-libvips-linuxmusl-arm64" "1.2.4"
"@img/sharp-linuxmusl-x64@0.34.5":
version "0.34.5"
resolved "https://registry.yarnpkg.com/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.34.5.tgz#d97978aec7c5212f999714f2f5b736457e12ee9f"
integrity sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q==
optionalDependencies:
"@img/sharp-libvips-linuxmusl-x64" "1.2.4"
"@img/sharp-wasm32@0.34.5":
version "0.34.5"
resolved "https://registry.yarnpkg.com/@img/sharp-wasm32/-/sharp-wasm32-0.34.5.tgz#2f15803aa626f8c59dd7c9d0bbc766f1ab52cfa0"
integrity sha512-OdWTEiVkY2PHwqkbBI8frFxQQFekHaSSkUIJkwzclWZe64O1X4UlUjqqqLaPbUpMOQk6FBu/HtlGXNblIs0huw==
dependencies:
"@emnapi/runtime" "^1.7.0"
"@img/sharp-win32-arm64@0.34.5":
version "0.34.5"
resolved "https://registry.yarnpkg.com/@img/sharp-win32-arm64/-/sharp-win32-arm64-0.34.5.tgz#3706e9e3ac35fddfc1c87f94e849f1b75307ce0a"
integrity sha512-WQ3AgWCWYSb2yt+IG8mnC6Jdk9Whs7O0gxphblsLvdhSpSTtmu69ZG1Gkb6NuvxsNACwiPV6cNSZNzt0KPsw7g==
"@img/sharp-win32-ia32@0.34.5":
version "0.34.5"
resolved "https://registry.yarnpkg.com/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.34.5.tgz#0b71166599b049e032f085fb9263e02f4e4788de"
integrity sha512-FV9m/7NmeCmSHDD5j4+4pNI8Cp3aW+JvLoXcTUo0IqyjSfAZJ8dIUmijx1qaJsIiU+Hosw6xM5KijAWRJCSgNg==
"@img/sharp-win32-x64@0.34.5":
version "0.34.5"
resolved "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.34.5.tgz"
@@ -359,6 +511,24 @@
"@jridgewell/resolve-uri" "^3.1.0"
"@jridgewell/sourcemap-codec" "^1.4.14"
"@napi-rs/wasm-runtime@^0.2.11":
version "0.2.12"
resolved "https://registry.yarnpkg.com/@napi-rs/wasm-runtime/-/wasm-runtime-0.2.12.tgz#3e78a8b96e6c33a6c517e1894efbd5385a7cb6f2"
integrity sha512-ZVWUcfwY4E/yPitQJl481FjFo3K22D6qF0DuFH6Y/nbnE11GY5uguDxZMGXPQ8WQ0128MXQD7TnfHyK4oWoIJQ==
dependencies:
"@emnapi/core" "^1.4.3"
"@emnapi/runtime" "^1.4.3"
"@tybys/wasm-util" "^0.10.0"
"@napi-rs/wasm-runtime@^1.0.7":
version "1.1.1"
resolved "https://registry.yarnpkg.com/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.1.tgz#c3705ab549d176b8dc5172723d6156c3dc426af2"
integrity sha512-p64ah1M1ld8xjWv3qbvFwHiFVWrq1yFvV4f7w+mzaqiR4IlSgkqhcRdHwsGgomwzBH51sRY4NEowLxnaBjcW/A==
dependencies:
"@emnapi/core" "^1.7.1"
"@emnapi/runtime" "^1.7.1"
"@tybys/wasm-util" "^0.10.1"
"@next/env@16.0.6":
version "16.0.6"
resolved "https://registry.npmjs.org/@next/env/-/env-16.0.6.tgz"
@@ -371,6 +541,41 @@
dependencies:
fast-glob "3.3.1"
"@next/swc-darwin-arm64@16.0.6":
version "16.0.6"
resolved "https://registry.yarnpkg.com/@next/swc-darwin-arm64/-/swc-darwin-arm64-16.0.6.tgz#e43c59c193a27a10f299c15c7305560226680493"
integrity sha512-AGzKiPlDiui+9JcPRHLI4V9WFTTcKukhJTfK9qu3e0tz+Y/88B7vo5yZoO7UaikplJEHORzG3QaBFQfkjhnL0Q==
"@next/swc-darwin-x64@16.0.6":
version "16.0.6"
resolved "https://registry.yarnpkg.com/@next/swc-darwin-x64/-/swc-darwin-x64-16.0.6.tgz#083292f12c7fbc31658b5c7a581be1ec1c0bb04e"
integrity sha512-LlLLNrK9WCIUkq2GciWDcquXYIf7vLxX8XE49gz7EncssZGL1vlHwgmURiJsUZAvk0HM1a8qb1ABDezsjAE/jw==
"@next/swc-linux-arm64-gnu@16.0.6":
version "16.0.6"
resolved "https://registry.yarnpkg.com/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-16.0.6.tgz#2495a4dc0d6ff6fba7bd5edfbec72c79b3221c29"
integrity sha512-r04NzmLSGGfG8EPXKVK72N5zDNnq9pa9el78LhdtqIC3zqKh74QfKHnk24DoK4PEs6eY7sIK/CnNpt30oc59kg==
"@next/swc-linux-arm64-musl@16.0.6":
version "16.0.6"
resolved "https://registry.yarnpkg.com/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-16.0.6.tgz#06b6a22512a7f721f4075166177c965902d9a735"
integrity sha512-hfB/QV0hA7lbD1OJxp52wVDlpffUMfyxUB5ysZbb/pBC5iuhyLcEKSVQo56PFUUmUQzbMsAtUu6k2Gh9bBtWXA==
"@next/swc-linux-x64-gnu@16.0.6":
version "16.0.6"
resolved "https://registry.yarnpkg.com/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-16.0.6.tgz#8ffa6574b3c64edae3c01255231c708f2abe21d9"
integrity sha512-PZJushBgfvKhJBy01yXMdgL+l5XKr7uSn5jhOQXQXiH3iPT2M9iG64yHpPNGIKitKrHJInwmhPVGogZBAJOCPw==
"@next/swc-linux-x64-musl@16.0.6":
version "16.0.6"
resolved "https://registry.yarnpkg.com/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-16.0.6.tgz#4d2d912563b8423b304b5ebac091f52aa5b4ba49"
integrity sha512-LqY76IojrH9yS5fyATjLzlOIOgwyzBuNRqXwVxcGfZ58DWNQSyfnLGlfF6shAEqjwlDNLh4Z+P0rnOI87Y9jEw==
"@next/swc-win32-arm64-msvc@16.0.6":
version "16.0.6"
resolved "https://registry.yarnpkg.com/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-16.0.6.tgz#d8f70405b5f0a3fb5037719deb0ae05085c335cd"
integrity sha512-eIfSNNqAkj0tqKRf0u7BVjqylJCuabSrxnpSENY3YKApqwDMeAqYPmnOwmVe6DDl3Lvkbe7cJAyP6i9hQ5PmmQ==
"@next/swc-win32-x64-msvc@16.0.6":
version "16.0.6"
resolved "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-16.0.6.tgz"
@@ -384,7 +589,7 @@
"@nodelib/fs.stat" "2.0.5"
run-parallel "^1.1.9"
"@nodelib/fs.stat@^2.0.2", "@nodelib/fs.stat@2.0.5":
"@nodelib/fs.stat@2.0.5", "@nodelib/fs.stat@^2.0.2":
version "2.0.5"
resolved "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz"
integrity sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==
@@ -478,7 +683,7 @@
"@radix-ui/react-use-previous" "1.1.1"
"@radix-ui/react-use-size" "1.1.1"
"@radix-ui/react-collapsible@^1.1.12", "@radix-ui/react-collapsible@1.1.12":
"@radix-ui/react-collapsible@1.1.12", "@radix-ui/react-collapsible@^1.1.12":
version "1.1.12"
resolved "https://registry.npmjs.org/@radix-ui/react-collapsible/-/react-collapsible-1.1.12.tgz"
integrity sha512-Uu+mSh4agx2ib1uIGPP4/CKNULyajb3p92LsVXmH2EHVMTfZWpll88XJ0j4W0z3f8NK1eYl1+Mf/szHPmcHzyA==
@@ -502,7 +707,7 @@
"@radix-ui/react-primitive" "2.1.3"
"@radix-ui/react-slot" "1.2.3"
"@radix-ui/react-compose-refs@^1.1.1", "@radix-ui/react-compose-refs@1.1.2":
"@radix-ui/react-compose-refs@1.1.2", "@radix-ui/react-compose-refs@^1.1.1":
version "1.1.2"
resolved "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.2.tgz"
integrity sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg==
@@ -529,7 +734,7 @@
resolved "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.1.3.tgz"
integrity sha512-ieIFACdMpYfMEjF0rEf5KLvfVyIkOz6PDGyNnP+u+4xQ6jny3VCgA4OgXOwNx2aUkxn8zx9fiVcM8CfFYv9Lxw==
"@radix-ui/react-dialog@^1.1.1", "@radix-ui/react-dialog@^1.1.15", "@radix-ui/react-dialog@^1.1.6", "@radix-ui/react-dialog@1.1.15":
"@radix-ui/react-dialog@1.1.15", "@radix-ui/react-dialog@^1.1.1", "@radix-ui/react-dialog@^1.1.15", "@radix-ui/react-dialog@^1.1.6":
version "1.1.15"
resolved "https://registry.npmjs.org/@radix-ui/react-dialog/-/react-dialog-1.1.15.tgz"
integrity sha512-TCglVRtzlffRNxRMEyR36DGBLJpeusFcgMVD9PZEzAKnUs1lKCgX5u9BmC2Yg+LL9MgZDugFFs1Vl+Jp4t/PGw==
@@ -607,7 +812,7 @@
"@radix-ui/react-primitive" "2.1.3"
"@radix-ui/react-use-controllable-state" "1.2.2"
"@radix-ui/react-id@^1.1.0", "@radix-ui/react-id@1.1.1":
"@radix-ui/react-id@1.1.1", "@radix-ui/react-id@^1.1.0":
version "1.1.1"
resolved "https://registry.npmjs.org/@radix-ui/react-id/-/react-id-1.1.1.tgz"
integrity sha512-kGkGegYIdQsOb4XjsfM97rXsiHaBwco+hFI66oO4s9LU+PLAC5oJ7khdOVFxkhsmlbpUqDAvXw11CluXP+jkHg==
@@ -734,7 +939,7 @@
"@radix-ui/react-compose-refs" "1.1.2"
"@radix-ui/react-use-layout-effect" "1.1.1"
"@radix-ui/react-primitive@^2.0.2", "@radix-ui/react-primitive@2.1.3":
"@radix-ui/react-primitive@2.1.3", "@radix-ui/react-primitive@^2.0.2":
version "2.1.3"
resolved "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz"
integrity sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==
@@ -853,13 +1058,6 @@
"@radix-ui/react-use-previous" "1.1.1"
"@radix-ui/react-use-size" "1.1.1"
"@radix-ui/react-slot@^1.2.4", "@radix-ui/react-slot@1.2.4":
version "1.2.4"
resolved "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.4.tgz"
integrity sha512-Jl+bCv8HxKnlTLVrcDE8zTMJ09R9/ukw4qBs/oZClOfoQk/cOTbDn+NceXfV7j09YPVQUryJPHurafcSg6EVKA==
dependencies:
"@radix-ui/react-compose-refs" "1.1.2"
"@radix-ui/react-slot@1.2.3":
version "1.2.3"
resolved "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz"
@@ -867,6 +1065,13 @@
dependencies:
"@radix-ui/react-compose-refs" "1.1.2"
"@radix-ui/react-slot@1.2.4", "@radix-ui/react-slot@^1.2.4":
version "1.2.4"
resolved "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.4.tgz"
integrity sha512-Jl+bCv8HxKnlTLVrcDE8zTMJ09R9/ukw4qBs/oZClOfoQk/cOTbDn+NceXfV7j09YPVQUryJPHurafcSg6EVKA==
dependencies:
"@radix-ui/react-compose-refs" "1.1.2"
"@radix-ui/react-switch@^1.2.6":
version "1.2.6"
resolved "https://registry.npmjs.org/@radix-ui/react-switch/-/react-switch-1.2.6.tgz"
@@ -907,7 +1112,7 @@
"@radix-ui/react-toggle" "1.1.10"
"@radix-ui/react-use-controllable-state" "1.2.2"
"@radix-ui/react-toggle@^1.1.10", "@radix-ui/react-toggle@1.1.10":
"@radix-ui/react-toggle@1.1.10", "@radix-ui/react-toggle@^1.1.10":
version "1.1.10"
resolved "https://registry.npmjs.org/@radix-ui/react-toggle/-/react-toggle-1.1.10.tgz"
integrity sha512-lS1odchhFTeZv3xwHH31YPObmJn8gOg7Lq12inrr0+BH/l3Tsq32VfjqH1oh80ARM3mlkfMic15n0kg4sD1poQ==
@@ -1034,6 +1239,68 @@
source-map-js "^1.2.1"
tailwindcss "4.1.17"
"@tailwindcss/oxide-android-arm64@4.1.17":
version "4.1.17"
resolved "https://registry.yarnpkg.com/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.1.17.tgz#17f0dc901f88a979c5bff618181bce596dff596d"
integrity sha512-BMqpkJHgOZ5z78qqiGE6ZIRExyaHyuxjgrJ6eBO5+hfrfGkuya0lYfw8fRHG77gdTjWkNWEEm+qeG2cDMxArLQ==
"@tailwindcss/oxide-darwin-arm64@4.1.17":
version "4.1.17"
resolved "https://registry.yarnpkg.com/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.1.17.tgz#63e12e62b83f6949dbb10b5a7f6e441606835efc"
integrity sha512-EquyumkQweUBNk1zGEU/wfZo2qkp/nQKRZM8bUYO0J+Lums5+wl2CcG1f9BgAjn/u9pJzdYddHWBiFXJTcxmOg==
"@tailwindcss/oxide-darwin-x64@4.1.17":
version "4.1.17"
resolved "https://registry.yarnpkg.com/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.1.17.tgz#6dad270d2777508f55e2b73eca0eaef625bc45a7"
integrity sha512-gdhEPLzke2Pog8s12oADwYu0IAw04Y2tlmgVzIN0+046ytcgx8uZmCzEg4VcQh+AHKiS7xaL8kGo/QTiNEGRog==
"@tailwindcss/oxide-freebsd-x64@4.1.17":
version "4.1.17"
resolved "https://registry.yarnpkg.com/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.1.17.tgz#e7628b4602ac7d73c11a9922ecb83c24337eff55"
integrity sha512-hxGS81KskMxML9DXsaXT1H0DyA+ZBIbyG/sSAjWNe2EDl7TkPOBI42GBV3u38itzGUOmFfCzk1iAjDXds8Oh0g==
"@tailwindcss/oxide-linux-arm-gnueabihf@4.1.17":
version "4.1.17"
resolved "https://registry.yarnpkg.com/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.1.17.tgz#4d96a6fe4c7ed20e7a013101ee46f46caca2233e"
integrity sha512-k7jWk5E3ldAdw0cNglhjSgv501u7yrMf8oeZ0cElhxU6Y2o7f8yqelOp3fhf7evjIS6ujTI3U8pKUXV2I4iXHQ==
"@tailwindcss/oxide-linux-arm64-gnu@4.1.17":
version "4.1.17"
resolved "https://registry.yarnpkg.com/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.1.17.tgz#adc3c01cd73610870bfc21db5713571e08fb2210"
integrity sha512-HVDOm/mxK6+TbARwdW17WrgDYEGzmoYayrCgmLEw7FxTPLcp/glBisuyWkFz/jb7ZfiAXAXUACfyItn+nTgsdQ==
"@tailwindcss/oxide-linux-arm64-musl@4.1.17":
version "4.1.17"
resolved "https://registry.yarnpkg.com/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.1.17.tgz#39ceda30407af56a1ee125b2c5ce856c6d29250f"
integrity sha512-HvZLfGr42i5anKtIeQzxdkw/wPqIbpeZqe7vd3V9vI3RQxe3xU1fLjss0TjyhxWcBaipk7NYwSrwTwK1hJARMg==
"@tailwindcss/oxide-linux-x64-gnu@4.1.17":
version "4.1.17"
resolved "https://registry.yarnpkg.com/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.1.17.tgz#a3d4bd876c04d09856af0c394f5095fbc8a2b14c"
integrity sha512-M3XZuORCGB7VPOEDH+nzpJ21XPvK5PyjlkSFkFziNHGLc5d6g3di2McAAblmaSUNl8IOmzYwLx9NsE7bplNkwQ==
"@tailwindcss/oxide-linux-x64-musl@4.1.17":
version "4.1.17"
resolved "https://registry.yarnpkg.com/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.1.17.tgz#bdc20aa4fb2d28cc928f2cfffff7a9cd03a51d5b"
integrity sha512-k7f+pf9eXLEey4pBlw+8dgfJHY4PZ5qOUFDyNf7SI6lHjQ9Zt7+NcscjpwdCEbYi6FI5c2KDTDWyf2iHcCSyyQ==
"@tailwindcss/oxide-wasm32-wasi@4.1.17":
version "4.1.17"
resolved "https://registry.yarnpkg.com/@tailwindcss/oxide-wasm32-wasi/-/oxide-wasm32-wasi-4.1.17.tgz#7c0804748935928751838f86ff4f879c38f8a6d7"
integrity sha512-cEytGqSSoy7zK4JRWiTCx43FsKP/zGr0CsuMawhH67ONlH+T79VteQeJQRO/X7L0juEUA8ZyuYikcRBf0vsxhg==
dependencies:
"@emnapi/core" "^1.6.0"
"@emnapi/runtime" "^1.6.0"
"@emnapi/wasi-threads" "^1.1.0"
"@napi-rs/wasm-runtime" "^1.0.7"
"@tybys/wasm-util" "^0.10.1"
tslib "^2.4.0"
"@tailwindcss/oxide-win32-arm64-msvc@4.1.17":
version "4.1.17"
resolved "https://registry.yarnpkg.com/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.1.17.tgz#7222fc2ceee9d45ebe5aebf38707ee9833a20475"
integrity sha512-JU5AHr7gKbZlOGvMdb4722/0aYbU+tN6lv1kONx0JK2cGsh7g148zVWLM0IKR3NeKLv+L90chBVYcJ8uJWbC9A==
"@tailwindcss/oxide-win32-x64-msvc@4.1.17":
version "4.1.17"
resolved "https://registry.npmjs.org/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.1.17.tgz"
@@ -1068,9 +1335,9 @@
postcss "^8.4.41"
tailwindcss "4.1.17"
"@tybys/wasm-util@^0.10.0":
"@tybys/wasm-util@^0.10.0", "@tybys/wasm-util@^0.10.1":
version "0.10.1"
resolved "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.1.tgz"
resolved "https://registry.yarnpkg.com/@tybys/wasm-util/-/wasm-util-0.10.1.tgz#ecddd3205cf1e2d5274649ff0eedd2991ed7f414"
integrity sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==
dependencies:
tslib "^2.4.0"
@@ -1148,12 +1415,12 @@
dependencies:
undici-types "~6.21.0"
"@types/react-dom@*", "@types/react-dom@^19":
"@types/react-dom@^19":
version "19.2.3"
resolved "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.3.tgz"
integrity sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==
"@types/react@*", "@types/react@^19", "@types/react@^19.2.0":
"@types/react@^19":
version "19.2.7"
resolved "https://registry.npmjs.org/@types/react/-/react-19.2.7.tgz"
integrity sha512-MWtvHrGZLFttgeEj28VXHxpmwYbor/ATPYbBfSFZEIRK0ecCFLl2Qo55z52Hss+UV9CRN7trSeq1zbgx7YDWWg==
@@ -1175,7 +1442,7 @@
natural-compare "^1.4.0"
ts-api-utils "^2.1.0"
"@typescript-eslint/parser@^8.48.0", "@typescript-eslint/parser@8.48.0":
"@typescript-eslint/parser@8.48.0":
version "8.48.0"
resolved "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.48.0.tgz"
integrity sha512-jCzKdm/QK0Kg4V4IK/oMlRZlY+QOcdjv89U2NgKHZk1CYTj82/RVSx1mV/0gqCVMJ/DA+Zf/S4NBWNF8GQ+eqQ==
@@ -1203,7 +1470,7 @@
"@typescript-eslint/types" "8.48.0"
"@typescript-eslint/visitor-keys" "8.48.0"
"@typescript-eslint/tsconfig-utils@^8.48.0", "@typescript-eslint/tsconfig-utils@8.48.0":
"@typescript-eslint/tsconfig-utils@8.48.0", "@typescript-eslint/tsconfig-utils@^8.48.0":
version "8.48.0"
resolved "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.48.0.tgz"
integrity sha512-WNebjBdFdyu10sR1M4OXTt2OkMd5KWIL+LLfeH9KhgP+jzfDV/LI3eXzwJ1s9+Yc0Kzo2fQCdY/OpdusCMmh6w==
@@ -1219,7 +1486,7 @@
debug "^4.3.4"
ts-api-utils "^2.1.0"
"@typescript-eslint/types@^8.48.0", "@typescript-eslint/types@8.48.0":
"@typescript-eslint/types@8.48.0", "@typescript-eslint/types@^8.48.0":
version "8.48.0"
resolved "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.48.0.tgz"
integrity sha512-cQMcGQQH7kwKoVswD1xdOytxQR60MWKM1di26xSUtxehaDs/32Zpqsu5WJlXTtTTqyAVK8R7hvsUnIXRS+bjvA==
@@ -1257,6 +1524,98 @@
"@typescript-eslint/types" "8.48.0"
eslint-visitor-keys "^4.2.1"
"@unrs/resolver-binding-android-arm-eabi@1.11.1":
version "1.11.1"
resolved "https://registry.yarnpkg.com/@unrs/resolver-binding-android-arm-eabi/-/resolver-binding-android-arm-eabi-1.11.1.tgz#9f5b04503088e6a354295e8ea8fe3cb99e43af81"
integrity sha512-ppLRUgHVaGRWUx0R0Ut06Mjo9gBaBkg3v/8AxusGLhsIotbBLuRk51rAzqLC8gq6NyyAojEXglNjzf6R948DNw==
"@unrs/resolver-binding-android-arm64@1.11.1":
version "1.11.1"
resolved "https://registry.yarnpkg.com/@unrs/resolver-binding-android-arm64/-/resolver-binding-android-arm64-1.11.1.tgz#7414885431bd7178b989aedc4d25cccb3865bc9f"
integrity sha512-lCxkVtb4wp1v+EoN+HjIG9cIIzPkX5OtM03pQYkG+U5O/wL53LC4QbIeazgiKqluGeVEeBlZahHalCaBvU1a2g==
"@unrs/resolver-binding-darwin-arm64@1.11.1":
version "1.11.1"
resolved "https://registry.yarnpkg.com/@unrs/resolver-binding-darwin-arm64/-/resolver-binding-darwin-arm64-1.11.1.tgz#b4a8556f42171fb9c9f7bac8235045e82aa0cbdf"
integrity sha512-gPVA1UjRu1Y/IsB/dQEsp2V1pm44Of6+LWvbLc9SDk1c2KhhDRDBUkQCYVWe6f26uJb3fOK8saWMgtX8IrMk3g==
"@unrs/resolver-binding-darwin-x64@1.11.1":
version "1.11.1"
resolved "https://registry.yarnpkg.com/@unrs/resolver-binding-darwin-x64/-/resolver-binding-darwin-x64-1.11.1.tgz#fd4d81257b13f4d1a083890a6a17c00de571f0dc"
integrity sha512-cFzP7rWKd3lZaCsDze07QX1SC24lO8mPty9vdP+YVa3MGdVgPmFc59317b2ioXtgCMKGiCLxJ4HQs62oz6GfRQ==
"@unrs/resolver-binding-freebsd-x64@1.11.1":
version "1.11.1"
resolved "https://registry.yarnpkg.com/@unrs/resolver-binding-freebsd-x64/-/resolver-binding-freebsd-x64-1.11.1.tgz#d2513084d0f37c407757e22f32bd924a78cfd99b"
integrity sha512-fqtGgak3zX4DCB6PFpsH5+Kmt/8CIi4Bry4rb1ho6Av2QHTREM+47y282Uqiu3ZRF5IQioJQ5qWRV6jduA+iGw==
"@unrs/resolver-binding-linux-arm-gnueabihf@1.11.1":
version "1.11.1"
resolved "https://registry.yarnpkg.com/@unrs/resolver-binding-linux-arm-gnueabihf/-/resolver-binding-linux-arm-gnueabihf-1.11.1.tgz#844d2605d057488d77fab09705f2866b86164e0a"
integrity sha512-u92mvlcYtp9MRKmP+ZvMmtPN34+/3lMHlyMj7wXJDeXxuM0Vgzz0+PPJNsro1m3IZPYChIkn944wW8TYgGKFHw==
"@unrs/resolver-binding-linux-arm-musleabihf@1.11.1":
version "1.11.1"
resolved "https://registry.yarnpkg.com/@unrs/resolver-binding-linux-arm-musleabihf/-/resolver-binding-linux-arm-musleabihf-1.11.1.tgz#204892995cefb6bd1d017d52d097193bc61ddad3"
integrity sha512-cINaoY2z7LVCrfHkIcmvj7osTOtm6VVT16b5oQdS4beibX2SYBwgYLmqhBjA1t51CarSaBuX5YNsWLjsqfW5Cw==
"@unrs/resolver-binding-linux-arm64-gnu@1.11.1":
version "1.11.1"
resolved "https://registry.yarnpkg.com/@unrs/resolver-binding-linux-arm64-gnu/-/resolver-binding-linux-arm64-gnu-1.11.1.tgz#023eb0c3aac46066a10be7a3f362e7b34f3bdf9d"
integrity sha512-34gw7PjDGB9JgePJEmhEqBhWvCiiWCuXsL9hYphDF7crW7UgI05gyBAi6MF58uGcMOiOqSJ2ybEeCvHcq0BCmQ==
"@unrs/resolver-binding-linux-arm64-musl@1.11.1":
version "1.11.1"
resolved "https://registry.yarnpkg.com/@unrs/resolver-binding-linux-arm64-musl/-/resolver-binding-linux-arm64-musl-1.11.1.tgz#9e6f9abb06424e3140a60ac996139786f5d99be0"
integrity sha512-RyMIx6Uf53hhOtJDIamSbTskA99sPHS96wxVE/bJtePJJtpdKGXO1wY90oRdXuYOGOTuqjT8ACccMc4K6QmT3w==
"@unrs/resolver-binding-linux-ppc64-gnu@1.11.1":
version "1.11.1"
resolved "https://registry.yarnpkg.com/@unrs/resolver-binding-linux-ppc64-gnu/-/resolver-binding-linux-ppc64-gnu-1.11.1.tgz#b111417f17c9d1b02efbec8e08398f0c5527bb44"
integrity sha512-D8Vae74A4/a+mZH0FbOkFJL9DSK2R6TFPC9M+jCWYia/q2einCubX10pecpDiTmkJVUH+y8K3BZClycD8nCShA==
"@unrs/resolver-binding-linux-riscv64-gnu@1.11.1":
version "1.11.1"
resolved "https://registry.yarnpkg.com/@unrs/resolver-binding-linux-riscv64-gnu/-/resolver-binding-linux-riscv64-gnu-1.11.1.tgz#92ffbf02748af3e99873945c9a8a5ead01d508a9"
integrity sha512-frxL4OrzOWVVsOc96+V3aqTIQl1O2TjgExV4EKgRY09AJ9leZpEg8Ak9phadbuX0BA4k8U5qtvMSQQGGmaJqcQ==
"@unrs/resolver-binding-linux-riscv64-musl@1.11.1":
version "1.11.1"
resolved "https://registry.yarnpkg.com/@unrs/resolver-binding-linux-riscv64-musl/-/resolver-binding-linux-riscv64-musl-1.11.1.tgz#0bec6f1258fc390e6b305e9ff44256cb207de165"
integrity sha512-mJ5vuDaIZ+l/acv01sHoXfpnyrNKOk/3aDoEdLO/Xtn9HuZlDD6jKxHlkN8ZhWyLJsRBxfv9GYM2utQ1SChKew==
"@unrs/resolver-binding-linux-s390x-gnu@1.11.1":
version "1.11.1"
resolved "https://registry.yarnpkg.com/@unrs/resolver-binding-linux-s390x-gnu/-/resolver-binding-linux-s390x-gnu-1.11.1.tgz#577843a084c5952f5906770633ccfb89dac9bc94"
integrity sha512-kELo8ebBVtb9sA7rMe1Cph4QHreByhaZ2QEADd9NzIQsYNQpt9UkM9iqr2lhGr5afh885d/cB5QeTXSbZHTYPg==
"@unrs/resolver-binding-linux-x64-gnu@1.11.1":
version "1.11.1"
resolved "https://registry.yarnpkg.com/@unrs/resolver-binding-linux-x64-gnu/-/resolver-binding-linux-x64-gnu-1.11.1.tgz#36fb318eebdd690f6da32ac5e0499a76fa881935"
integrity sha512-C3ZAHugKgovV5YvAMsxhq0gtXuwESUKc5MhEtjBpLoHPLYM+iuwSj3lflFwK3DPm68660rZ7G8BMcwSro7hD5w==
"@unrs/resolver-binding-linux-x64-musl@1.11.1":
version "1.11.1"
resolved "https://registry.yarnpkg.com/@unrs/resolver-binding-linux-x64-musl/-/resolver-binding-linux-x64-musl-1.11.1.tgz#bfb9af75f783f98f6a22c4244214efe4df1853d6"
integrity sha512-rV0YSoyhK2nZ4vEswT/QwqzqQXw5I6CjoaYMOX0TqBlWhojUf8P94mvI7nuJTeaCkkds3QE4+zS8Ko+GdXuZtA==
"@unrs/resolver-binding-wasm32-wasi@1.11.1":
version "1.11.1"
resolved "https://registry.yarnpkg.com/@unrs/resolver-binding-wasm32-wasi/-/resolver-binding-wasm32-wasi-1.11.1.tgz#752c359dd875684b27429500d88226d7cc72f71d"
integrity sha512-5u4RkfxJm+Ng7IWgkzi3qrFOvLvQYnPBmjmZQ8+szTK/b31fQCnleNl1GgEt7nIsZRIf5PLhPwT0WM+q45x/UQ==
dependencies:
"@napi-rs/wasm-runtime" "^0.2.11"
"@unrs/resolver-binding-win32-arm64-msvc@1.11.1":
version "1.11.1"
resolved "https://registry.yarnpkg.com/@unrs/resolver-binding-win32-arm64-msvc/-/resolver-binding-win32-arm64-msvc-1.11.1.tgz#ce5735e600e4c2fbb409cd051b3b7da4a399af35"
integrity sha512-nRcz5Il4ln0kMhfL8S3hLkxI85BXs3o8EYoattsJNdsX4YUU89iOkVn7g0VHSRxFuVMdM4Q1jEpIId1Ihim/Uw==
"@unrs/resolver-binding-win32-ia32-msvc@1.11.1":
version "1.11.1"
resolved "https://registry.yarnpkg.com/@unrs/resolver-binding-win32-ia32-msvc/-/resolver-binding-win32-ia32-msvc-1.11.1.tgz#72fc57bc7c64ec5c3de0d64ee0d1810317bc60a6"
integrity sha512-DCEI6t5i1NmAZp6pFonpD5m7i6aFrpofcp4LA2i8IIq60Jyo28hamKBxNrZcyOwVOZkgsRp9O2sXWBWP8MnvIQ==
"@unrs/resolver-binding-win32-x64-msvc@1.11.1":
version "1.11.1"
resolved "https://registry.npmjs.org/@unrs/resolver-binding-win32-x64-msvc/-/resolver-binding-win32-x64-msvc-1.11.1.tgz"
@@ -1267,7 +1626,7 @@ acorn-jsx@^5.3.2:
resolved "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz"
integrity sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==
"acorn@^6.0.0 || ^7.0.0 || ^8.0.0", acorn@^8.15.0:
acorn@^8.15.0:
version "8.15.0"
resolved "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz"
integrity sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==
@@ -1456,7 +1815,7 @@ braces@^3.0.3:
dependencies:
fill-range "^7.1.1"
browserslist@^4.24.0, "browserslist@>= 4.21.0":
browserslist@^4.24.0:
version "4.28.0"
resolved "https://registry.npmjs.org/browserslist/-/browserslist-4.28.0.tgz"
integrity sha512-tbydkR/CxfMwelN0vwdP/pLkDwyAASZ+VfWm4EOwlB6SWhx1sYnWLqo8N5j0rAzPfzfRaxt0mM/4wPU/Su84RQ==
@@ -1574,7 +1933,7 @@ csstype@^3.0.2, csstype@^3.2.2:
resolved "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz"
integrity sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==
d3-array@^3.1.6, "d3-array@2 - 3", "d3-array@2.10.0 - 3":
"d3-array@2 - 3", "d3-array@2.10.0 - 3", d3-array@^3.1.6:
version "3.2.4"
resolved "https://registry.npmjs.org/d3-array/-/d3-array-3.2.4.tgz"
integrity sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==
@@ -1596,7 +1955,7 @@ d3-ease@^3.0.1:
resolved "https://registry.npmjs.org/d3-format/-/d3-format-3.1.0.tgz"
integrity sha512-YyUI6AEuY/Wpt8KWLgZHsIU86atmikuoOmCfommt0LYHiQSPjvX2AcFc38PX0CBpr2RCyZhjex+NS/LPOv6YqA==
d3-interpolate@^3.0.1, "d3-interpolate@1.2.0 - 3":
"d3-interpolate@1.2.0 - 3", d3-interpolate@^3.0.1:
version "3.0.1"
resolved "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-3.0.1.tgz"
integrity sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==
@@ -1633,7 +1992,7 @@ d3-shape@^3.1.0:
dependencies:
d3-time "1 - 3"
d3-time@^3.0.0, "d3-time@1 - 3", "d3-time@2.1.1 - 3":
"d3-time@1 - 3", "d3-time@2.1.1 - 3", d3-time@^3.0.0:
version "3.1.0"
resolved "https://registry.npmjs.org/d3-time/-/d3-time-3.1.0.tgz"
integrity sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q==
@@ -1978,7 +2337,7 @@ eslint-module-utils@^2.12.1:
dependencies:
debug "^3.2.7"
eslint-plugin-import@*, eslint-plugin-import@^2.32.0:
eslint-plugin-import@^2.32.0:
version "2.32.0"
resolved "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.32.0.tgz"
integrity sha512-whOE1HFo/qJDyX4SnXzP4N6zOWn79WhnCUY/iDR0mPfQZO8wcYE4JClzI2oZrhBnnMUCBCHZhO6VQyoBU95mZA==
@@ -2077,7 +2436,7 @@ eslint-visitor-keys@^4.2.1:
resolved "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz"
integrity sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==
eslint@*, "eslint@^2 || ^3 || ^4 || ^5 || ^6 || ^7.2.0 || ^8 || ^9", "eslint@^3 || ^4 || ^5 || ^6 || ^7 || ^8 || ^9", "eslint@^3 || ^4 || ^5 || ^6 || ^7 || ^8 || ^9.7", "eslint@^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0", "eslint@^6.0.0 || ^7.0.0 || >=8.0.0", "eslint@^8.57.0 || ^9.0.0", eslint@^9, eslint@>=9.0.0:
eslint@^9:
version "9.39.1"
resolved "https://registry.npmjs.org/eslint/-/eslint-9.39.1.tgz"
integrity sha512-BhHmn2yNOFA9H9JmmIVKJmd288g9hrVRDkdoIgRCRuSySRUHH7r/DI6aAXW9T1WwUuY3DFgrcaqB+deURBLR5g==
@@ -2340,16 +2699,16 @@ glob-parent@^6.0.2:
dependencies:
is-glob "^4.0.3"
globals@^14.0.0:
version "14.0.0"
resolved "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz"
integrity sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==
globals@16.4.0:
version "16.4.0"
resolved "https://registry.npmjs.org/globals/-/globals-16.4.0.tgz"
integrity sha512-ob/2LcVVaVGCYN+r14cnwnoDPUufjiYgSqRhiFD0Q1iI4Odora5RE8Iv1D24hAz5oMophRGkGz+yuvQmmUMnMw==
globals@^14.0.0:
version "14.0.0"
resolved "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz"
integrity sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==
globalthis@^1.0.4:
version "1.0.4"
resolved "https://registry.npmjs.org/globalthis/-/globalthis-1.0.4.tgz"
@@ -2682,7 +3041,7 @@ iterator.prototype@^1.1.4:
has-symbols "^1.1.0"
set-function-name "^2.0.2"
jiti@*, jiti@^2.6.1:
jiti@^2.6.1:
version "2.6.1"
resolved "https://registry.npmjs.org/jiti/-/jiti-2.6.1.tgz"
integrity sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==
@@ -2768,6 +3127,56 @@ levn@^0.4.1:
prelude-ls "^1.2.1"
type-check "~0.4.0"
lightningcss-android-arm64@1.30.2:
version "1.30.2"
resolved "https://registry.yarnpkg.com/lightningcss-android-arm64/-/lightningcss-android-arm64-1.30.2.tgz#6966b7024d39c94994008b548b71ab360eb3a307"
integrity sha512-BH9sEdOCahSgmkVhBLeU7Hc9DWeZ1Eb6wNS6Da8igvUwAe0sqROHddIlvU06q3WyXVEOYDZ6ykBZQnjTbmo4+A==
lightningcss-darwin-arm64@1.30.2:
version "1.30.2"
resolved "https://registry.yarnpkg.com/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.30.2.tgz#a5fa946d27c029e48c7ff929e6e724a7de46eb2c"
integrity sha512-ylTcDJBN3Hp21TdhRT5zBOIi73P6/W0qwvlFEk22fkdXchtNTOU4Qc37SkzV+EKYxLouZ6M4LG9NfZ1qkhhBWA==
lightningcss-darwin-x64@1.30.2:
version "1.30.2"
resolved "https://registry.yarnpkg.com/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.30.2.tgz#5ce87e9cd7c4f2dcc1b713f5e8ee185c88d9b7cd"
integrity sha512-oBZgKchomuDYxr7ilwLcyms6BCyLn0z8J0+ZZmfpjwg9fRVZIR5/GMXd7r9RH94iDhld3UmSjBM6nXWM2TfZTQ==
lightningcss-freebsd-x64@1.30.2:
version "1.30.2"
resolved "https://registry.yarnpkg.com/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.30.2.tgz#6ae1d5e773c97961df5cff57b851807ef33692a5"
integrity sha512-c2bH6xTrf4BDpK8MoGG4Bd6zAMZDAXS569UxCAGcA7IKbHNMlhGQ89eRmvpIUGfKWNVdbhSbkQaWhEoMGmGslA==
lightningcss-linux-arm-gnueabihf@1.30.2:
version "1.30.2"
resolved "https://registry.yarnpkg.com/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.30.2.tgz#62c489610c0424151a6121fa99d77731536cdaeb"
integrity sha512-eVdpxh4wYcm0PofJIZVuYuLiqBIakQ9uFZmipf6LF/HRj5Bgm0eb3qL/mr1smyXIS1twwOxNWndd8z0E374hiA==
lightningcss-linux-arm64-gnu@1.30.2:
version "1.30.2"
resolved "https://registry.yarnpkg.com/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.30.2.tgz#2a3661b56fe95a0cafae90be026fe0590d089298"
integrity sha512-UK65WJAbwIJbiBFXpxrbTNArtfuznvxAJw4Q2ZGlU8kPeDIWEX1dg3rn2veBVUylA2Ezg89ktszWbaQnxD/e3A==
lightningcss-linux-arm64-musl@1.30.2:
version "1.30.2"
resolved "https://registry.yarnpkg.com/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.30.2.tgz#d7ddd6b26959245e026bc1ad9eb6aa983aa90e6b"
integrity sha512-5Vh9dGeblpTxWHpOx8iauV02popZDsCYMPIgiuw97OJ5uaDsL86cnqSFs5LZkG3ghHoX5isLgWzMs+eD1YzrnA==
lightningcss-linux-x64-gnu@1.30.2:
version "1.30.2"
resolved "https://registry.yarnpkg.com/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.30.2.tgz#5a89814c8e63213a5965c3d166dff83c36152b1a"
integrity sha512-Cfd46gdmj1vQ+lR6VRTTadNHu6ALuw2pKR9lYq4FnhvgBc4zWY1EtZcAc6EffShbb1MFrIPfLDXD6Xprbnni4w==
lightningcss-linux-x64-musl@1.30.2:
version "1.30.2"
resolved "https://registry.yarnpkg.com/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.30.2.tgz#808c2e91ce0bf5d0af0e867c6152e5378c049728"
integrity sha512-XJaLUUFXb6/QG2lGIW6aIk6jKdtjtcffUT0NKvIqhSBY3hh9Ch+1LCeH80dR9q9LBjG3ewbDjnumefsLsP6aiA==
lightningcss-win32-arm64-msvc@1.30.2:
version "1.30.2"
resolved "https://registry.yarnpkg.com/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.30.2.tgz#ab4a8a8a2e6a82a4531e8bbb6bf0ff161ee6625a"
integrity sha512-FZn+vaj7zLv//D/192WFFVA0RgHawIcHqLX9xuWiQt7P0PtdFEVaxgF9rjM/IRYHQXNnk61/H/gb2Ei+kUQ4xQ==
lightningcss-win32-x64-msvc@1.30.2:
version "1.30.2"
resolved "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.30.2.tgz"
@@ -3076,7 +3485,7 @@ picomatch@^2.3.1:
resolved "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz"
integrity sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==
"picomatch@^3 || ^4", picomatch@^4.0.3:
picomatch@^4.0.3:
version "4.0.3"
resolved "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz"
integrity sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==
@@ -3086,15 +3495,6 @@ possible-typed-array-names@^1.0.0:
resolved "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz"
integrity sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==
postcss@^8.4.41:
version "8.5.6"
resolved "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz"
integrity sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==
dependencies:
nanoid "^3.3.11"
picocolors "^1.1.1"
source-map-js "^1.2.1"
postcss@8.4.31:
version "8.4.31"
resolved "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz"
@@ -3104,6 +3504,15 @@ postcss@8.4.31:
picocolors "^1.0.0"
source-map-js "^1.0.2"
postcss@^8.4.41:
version "8.5.6"
resolved "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz"
integrity sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==
dependencies:
nanoid "^3.3.11"
picocolors "^1.1.1"
source-map-js "^1.2.1"
prelude-ls@^1.2.1:
version "1.2.1"
resolved "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz"
@@ -3137,14 +3546,14 @@ react-day-picker@^9.11.3:
date-fns "^4.1.0"
date-fns-jalali "^4.1.0-0"
"react-dom@^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react-dom@^16.14.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc", "react-dom@^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc", "react-dom@^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom@^16.8 || ^17.0 || ^18.0 || ^19.0.0 || ^19.0.0-rc", "react-dom@^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react-dom@^18 || ^19 || ^19.0.0-rc", "react-dom@^18.0.0 || ^19.0.0", "react-dom@^18.0.0 || ^19.0.0 || ^19.0.0-rc", "react-dom@^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", react-dom@>=16.6.0, react-dom@>=16.8.0, react-dom@19.2.0:
react-dom@19.2.0:
version "19.2.0"
resolved "https://registry.npmjs.org/react-dom/-/react-dom-19.2.0.tgz"
integrity sha512-UlbRu4cAiGaIewkPyiRGJk0imDN2T3JjieT6spoL2UeSf5od4n5LB/mQ4ejmxhCFT1tYe8IvaFulzynWovsEFQ==
dependencies:
scheduler "^0.27.0"
react-hook-form@^7.55.0, react-hook-form@^7.67.0:
react-hook-form@^7.67.0:
version "7.67.0"
resolved "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.67.0.tgz"
integrity sha512-E55EOwKJHHIT/I6J9DmQbCWToAYSw9nN5R57MZw9rMtjh+YQreMDxRLfdjfxQbiJ3/qbg3Z02wGzBX4M+5fMtQ==
@@ -3210,7 +3619,7 @@ react-transition-group@^4.4.5:
loose-envify "^1.4.0"
prop-types "^15.6.2"
"react@^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react@^16.14.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc", "react@^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react@^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc", "react@^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react@^16.8 || ^17.0 || ^18.0 || ^19.0.0 || ^19.0.0-rc", "react@^16.8.0 || ^17 || ^18 || ^19", "react@^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react@^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc", "react@^16.8.0 || ^17.0.1 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc", "react@^18 || ^19 || ^19.0.0-rc", "react@^18.0.0 || ^19.0.0", "react@^18.0.0 || ^19.0.0 || ^19.0.0-rc", "react@^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", react@^19.2.0, "react@>= 16.8.0 || 17.x.x || ^18.0.0-0 || ^19.0.0-0", react@>=16, react@>=16.6.0, react@>=16.8.0, react@19.2.0:
react@19.2.0:
version "19.2.0"
resolved "https://registry.npmjs.org/react/-/react-19.2.0.tgz"
integrity sha512-tmbWg6W31tQLeB5cdIBOicJDJRR2KzXsV7uSK9iNfLWQ5bIZfxuPEHp7M8wiHyHnn0DD1i7w3Zmin0FtkrwoCQ==
@@ -3340,17 +3749,7 @@ semver@^6.3.1:
resolved "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz"
integrity sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==
semver@^7.6.0:
version "7.7.3"
resolved "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz"
integrity sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==
semver@^7.7.1:
version "7.7.3"
resolved "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz"
integrity sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==
semver@^7.7.3:
semver@^7.6.0, semver@^7.7.1, semver@^7.7.3:
version "7.7.3"
resolved "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz"
integrity sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==
@@ -3597,7 +3996,7 @@ tailwind-merge@^3.4.0:
resolved "https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-3.4.0.tgz"
integrity sha512-uSaO4gnW+b3Y2aWoWfFpX62vn2sR3skfhbjsEnaBI81WD1wBLlHZe5sWf0AqjksNdYTbGBEd0UasQMT3SNV15g==
tailwindcss@^4, tailwindcss@4.1.17:
tailwindcss@4.1.17, tailwindcss@^4:
version "4.1.17"
resolved "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.17.tgz"
integrity sha512-j9Ee2YjuQqYT9bbRTfTZht9W/ytp5H+jJpZKiYdP/bpnXARAuELt9ofP0lPnmHjbga7SNQIxdTAXCmtKVYjN+Q==
@@ -3714,7 +4113,7 @@ typescript-eslint@^8.46.0:
"@typescript-eslint/typescript-estree" "8.48.0"
"@typescript-eslint/utils" "8.48.0"
typescript@^5, typescript@>=3.3.1, typescript@>=4.8.4, "typescript@>=4.8.4 <6.0.0":
typescript@^5:
version "5.9.3"
resolved "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz"
integrity sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==