From 908d86939c170d425554654a2ed2c8611eb17746 Mon Sep 17 00:00:00 2001 From: Alireza Hassani Date: Sun, 23 Aug 2026 22:17:25 +0330 Subject: [PATCH] Polish portfolio, blog, and site chrome for clearer browsing. Add patterned title bands, lightbox gallery, project link bar, footer stats/credit, gradient link hovers, and more reliable blog covers/body rendering. Co-authored-by: Cursor --- public/images/brand/MeshkeeLogo.png | Bin 0 -> 10541 bytes src/app/blog/[slug]/page.tsx | 121 ++++++++---- src/app/blog/page.tsx | 11 +- src/app/globals.css | 187 +++++++++++++++++- src/app/portfolio/[slug]/page.tsx | 114 +++++++---- src/app/portfolio/page.tsx | 62 +++--- ... White -2.png => Meshkee Logo - White.png} | Bin src/components/BlogCoverImage.tsx | 33 ++++ src/components/BlogSection.tsx | 11 +- src/components/ContentPageShell.tsx | 5 +- src/components/Footer.tsx | 140 +++++++++++-- src/components/Header.tsx | 12 +- src/components/PortfolioGallery.tsx | 106 +++++++--- src/components/PortfolioProjectLink.tsx | 49 +++++ src/components/RevealObserver.tsx | 25 ++- src/components/Services.tsx | 2 +- src/data/assets.ts | 1 + src/data/home.ts | 2 +- src/lib/api.ts | 22 ++- 19 files changed, 710 insertions(+), 193 deletions(-) create mode 100755 public/images/brand/MeshkeeLogo.png rename src/assets/logo/{Meshkee Logo (No BG) - White -2.png => Meshkee Logo - White.png} (100%) create mode 100644 src/components/BlogCoverImage.tsx create mode 100644 src/components/PortfolioProjectLink.tsx diff --git a/public/images/brand/MeshkeeLogo.png b/public/images/brand/MeshkeeLogo.png new file mode 100755 index 0000000000000000000000000000000000000000..c1efc446580449b8783969c7a9743e2b3042aea0 GIT binary patch literal 10541 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 literal 0 HcmV?d00001 diff --git a/src/app/blog/[slug]/page.tsx b/src/app/blog/[slug]/page.tsx index 483f0de..c860cb1 100644 --- a/src/app/blog/[slug]/page.tsx +++ b/src/app/blog/[slug]/page.tsx @@ -1,6 +1,7 @@ import type { Metadata } from "next"; import Link from "next/link"; import { notFound } from "next/navigation"; +import { BlogCoverImage } from "@/components/BlogCoverImage"; import { ContentPageShell } from "@/components/ContentPageShell"; import { HtmlContent } from "@/components/HtmlContent"; import { @@ -15,6 +16,28 @@ type BlogDetailPageProps = { params: Promise<{ slug: string }>; }; +function stripLeadingCoverImage(html: string, cover: string | null): string { + if (!html) return html; + + let next = html.replace( + /^\s*]*>\s*]*>\s*<\/p>/i, + "", + ); + + if (cover) { + const escaped = cover.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); + next = next.replace( + new RegExp( + `]*>\\s*]*src=["']${escaped}["'][^>]*>\\s*<\\/p>`, + "i", + ), + "", + ); + } + + return next.trim(); +} + export async function generateMetadata({ params, }: BlogDetailPageProps): Promise { @@ -45,54 +68,82 @@ export default async function BlogDetailPage({ params }: BlogDetailPageProps) { const cover = getBlogCoverImage(blog); const date = formatFaDate(blog.publishedAt); const author = authorName(blog.author); + const category = blog.categoryName; + const bodyHtml = blog.mainTextHtml + ? stripLeadingCoverImage(blog.mainTextHtml, cover) + : null; + const titleEn = category || "Blog"; return ( - +
-
-
- +
+

- بازگشت به مقالات - + {titleEn} +

- {blog.categoryName ? ( -

- {blog.categoryName} -

- ) : null} +
+ -

- {blog.title} -

+

+ {blog.title} +

- {blog.abstract ? ( -

- {blog.abstract} -

- ) : null} - -
- {date ? : null} - {author ? نویسنده: {author} : null} + {blog.abstract ? ( +

+ {blog.abstract} +

+ ) : null}
+
- {cover && blog.titleImageUrl ? ( -
- +
+ {cover ? ( +
+
) : null} - {blog.mainTextHtml ? ( -
- + {(date || author) ? ( +
+ {author ? نویسنده: {author} : } + {date ? ( + + ) : null} +
+ ) : cover ? ( +
+ ) : null} + + {bodyHtml ? ( +
+
) : null}
diff --git a/src/app/blog/page.tsx b/src/app/blog/page.tsx index 5e5b6bc..721592c 100644 --- a/src/app/blog/page.tsx +++ b/src/app/blog/page.tsx @@ -1,5 +1,6 @@ import type { Metadata } from "next"; import Link from "next/link"; +import { BlogCoverImage } from "@/components/BlogCoverImage"; import { ContentPageShell } from "@/components/ContentPageShell"; import { Pagination } from "@/components/Pagination"; import { getBlogCoverImage, getBlogs } from "@/lib/api"; @@ -67,14 +68,8 @@ export default async function BlogListPage({ searchParams }: BlogPageProps) { }`} > -
- {image ? ( - - ) : null} +
+

{post.title} diff --git a/src/app/globals.css b/src/app/globals.css index 0a76cc9..81e2623 100644 --- a/src/app/globals.css +++ b/src/app/globals.css @@ -16,6 +16,13 @@ --brand-gradient: linear-gradient(135deg, var(--accent-from), var(--accent-to)); --background: var(--paper); --foreground: var(--ink); + --header-height: 9rem; +} + +@media (min-width: 1024px) { + :root { + --header-height: 11rem; + } } @theme inline { @@ -125,15 +132,45 @@ h4 { } .link-hover { - transition: color 0.25s ease; + display: inline-block; + width: fit-content; + max-width: 100%; + background-image: var(--brand-gradient); + background-clip: text; + -webkit-background-clip: text; + -webkit-text-fill-color: currentColor; + color: inherit; + transition: + color 0.35s ease, + -webkit-text-fill-color 0.35s ease; } .link-hover:hover { - color: var(--accent); - background: var(--brand-gradient); - background-clip: text; - -webkit-background-clip: text; -webkit-text-fill-color: transparent; + color: transparent; +} + +/* Keep flex layout when the link is already a flex row (nav + icon) */ +a.link-hover.inline-flex, +.link-hover.inline-flex { + display: inline-flex; +} + +/* Pill / button-style links: solid accent, no gradient fill */ +a.link-hover.rounded-full, +.link-hover.rounded-full, +.link-hover.btn-primary { + background-image: none; + background-clip: unset; + -webkit-background-clip: unset; + -webkit-text-fill-color: unset; +} + +a.link-hover.rounded-full:hover, +.link-hover.rounded-full:hover, +.link-hover.btn-primary:hover { + color: var(--accent); + -webkit-text-fill-color: var(--accent); } .border-hover { @@ -165,11 +202,20 @@ a.group-border-hover:hover { } .group-text-hover { - transition: color 0.25s ease; + display: inline; + background-image: var(--brand-gradient); + background-clip: text; + -webkit-background-clip: text; + -webkit-text-fill-color: currentColor; + color: inherit; + transition: + color 0.35s ease, + -webkit-text-fill-color 0.35s ease; } .group:hover .group-text-hover { - color: var(--accent); + -webkit-text-fill-color: transparent; + color: transparent; } .carousel-mask { @@ -207,6 +253,31 @@ a.group-border-hover:hover { mask-image: radial-gradient(ellipse 70% 60% at 50% 30%, black 20%, transparent 75%); } +.page-title-band { + border-bottom: 1px solid rgba(14, 17, 22, 0.08); + background-color: #fafafa; + background-image: + radial-gradient(rgba(14, 17, 22, 0.05) 1px, transparent 1px), + linear-gradient(rgba(14, 17, 22, 0.02) 1px, transparent 1px), + linear-gradient(90deg, rgba(14, 17, 22, 0.02) 1px, transparent 1px); + background-size: 20px 20px, 40px 40px, 40px 40px; +} + +.project-link-bar { + position: fixed; + inset-inline: 0; + bottom: 0; + z-index: 40; + border-top: 1px solid rgba(255, 255, 255, 0.1); + background-color: rgba(14, 17, 22, 0.94); + background-image: + radial-gradient(rgba(255, 255, 255, 0.09) 1px, transparent 1px), + linear-gradient(rgba(255, 255, 255, 0.045) 1px, transparent 1px), + linear-gradient(90deg, rgba(255, 255, 255, 0.045) 1px, transparent 1px); + background-size: 18px 18px, 36px 36px, 36px 36px; + backdrop-filter: blur(10px); +} + .mesh-glow { background: radial-gradient(ellipse 55% 45% at 78% 35%, var(--glow), transparent 70%), @@ -325,8 +396,19 @@ a.group-border-hover:hover { .prose-content a { color: var(--accent); - text-decoration: underline; - text-underline-offset: 3px; + text-decoration: none; + background-image: var(--brand-gradient); + background-clip: text; + -webkit-background-clip: text; + -webkit-text-fill-color: currentColor; + transition: + color 0.35s ease, + -webkit-text-fill-color 0.35s ease; +} + +.prose-content a:hover { + -webkit-text-fill-color: transparent; + color: transparent; } .prose-content img { @@ -364,3 +446,90 @@ a.group-border-hover:hover { padding: 0.55rem 0.75rem; text-align: start; } + +.footer-bottom-bar { + border-top: 1px solid #e5e5e5; + background: #f7f7f7; +} + +.meshkee-credit { + position: relative; +} + +.meshkee-credit__frame { + position: absolute; + inset: 0; + pointer-events: none; +} + +.meshkee-credit__line { + position: absolute; + background: #c8c8c8; + transition: transform 0.28s cubic-bezier(0.22, 1, 0.36, 1); +} + +.meshkee-credit__line--l { + top: 0; + left: 0; + width: 1px; + height: 100%; + transform: scaleY(0); + transform-origin: top; + transition-delay: 0s; +} + +.meshkee-credit__line--b { + bottom: 0; + left: 0; + width: 100%; + height: 1px; + transform: scaleX(0); + transform-origin: left; + transition-delay: 0.14s; +} + +.meshkee-credit__line--r { + top: 0; + right: 0; + width: 1px; + height: 100%; + transform: scaleY(0); + transform-origin: bottom; + transition-delay: 0.28s; +} + +.meshkee-credit__line--t { + top: 0; + left: 0; + width: 100%; + height: 1px; + transform: scaleX(0); + transform-origin: right; + transition-delay: 0.42s; +} + +.meshkee-credit:hover .meshkee-credit__line--l, +.meshkee-credit:hover .meshkee-credit__line--r { + transform: scaleY(1); +} + +.meshkee-credit:hover .meshkee-credit__line--t, +.meshkee-credit:hover .meshkee-credit__line--b { + transform: scaleX(1); +} + +.meshkee-credit:not(:hover) .meshkee-credit__line--l { + transition-delay: 0.42s; +} + +.meshkee-credit:not(:hover) .meshkee-credit__line--b { + transition-delay: 0.28s; +} + +.meshkee-credit:not(:hover) .meshkee-credit__line--r { + transition-delay: 0.14s; +} + +.meshkee-credit:not(:hover) .meshkee-credit__line--t { + transition-delay: 0s; +} diff --git a/src/app/portfolio/[slug]/page.tsx b/src/app/portfolio/[slug]/page.tsx index 7237295..8a36d65 100644 --- a/src/app/portfolio/[slug]/page.tsx +++ b/src/app/portfolio/[slug]/page.tsx @@ -4,6 +4,7 @@ import { notFound } from "next/navigation"; import { ContentPageShell } from "@/components/ContentPageShell"; import { HtmlContent } from "@/components/HtmlContent"; import { PortfolioGallery } from "@/components/PortfolioGallery"; +import { PortfolioProjectLink } from "@/components/PortfolioProjectLink"; import { categoryLabelFa, findPortfolioCategory, @@ -103,58 +104,82 @@ export default async function PortfolioDetailPage({ (item) => item.url !== cover, ); + const titleEn = + portfolio.titleEn ?? + (portfolio.titleFa && portfolio.title !== portfolio.titleFa + ? portfolio.title + : null); + const categoryHref = rootCategory + ? portfolioCategoryHref(rootCategory.slug) + : "/portfolio"; + return ( - +
-
-
- - بازگشت به نمونه‌کارها - - - {categoryFa ? ( -

{categoryFa}

- ) : null} - -

- {title} -

- - {portfolio.titleEn ? ( -

- {portfolio.titleEn} -

- ) : null} - - {portfolio.abstract ? ( -

- {portfolio.abstract} -

- ) : null} - - {date ? ( -
+
+ +
({ id: item.mediaId, url: item.url, }))} + category={categoryFa} + date={date} + dateTime={portfolio.publishedAt} /> {portfolio.mainTextHtml ? ( @@ -163,6 +188,13 @@ export default async function PortfolioDetailPage({
) : null}

+ + {portfolio.projectUrl ? ( + + ) : null}
); diff --git a/src/app/portfolio/page.tsx b/src/app/portfolio/page.tsx index fa073e5..ca2ae65 100644 --- a/src/app/portfolio/page.tsx +++ b/src/app/portfolio/page.tsx @@ -92,21 +92,28 @@ export default async function PortfolioListPage({ rootCategories.length > 0 ? rootCategories : topPortfolioCategories; return ( - +
-
-
-

- نمونه‌کارها -

-

+

+
+

Portfolio

-

- نگاهی به پروژه‌ها و کارهای انجام‌شده توسط تیم مشکی. -

+
+

+ نمونه‌کارها +

+

+ نگاهی به پروژه‌ها و کارهای انجام‌شده توسط تیم مشکی. +

+
+
+
{items.map((item, index) => { const image = getPortfolioCoverImage(item); @@ -162,13 +169,11 @@ export default async function PortfolioListPage({
@@ -182,21 +187,18 @@ export default async function PortfolioListPage({ ) : null}
-
- {titleEn ? ( -

- {titleEn} -

- ) : null} -

- {titleFa} -

-
{categoryFa ? ( -

{categoryFa}

+

+ {categoryFa} +

+ ) : null} +

+ {titleFa} +

+ {titleEn ? ( +

+ {titleEn} +

) : null} {item.abstract ? (

diff --git a/src/assets/logo/Meshkee Logo (No BG) - White -2.png b/src/assets/logo/Meshkee Logo - White.png similarity index 100% rename from src/assets/logo/Meshkee Logo (No BG) - White -2.png rename to src/assets/logo/Meshkee Logo - White.png diff --git a/src/components/BlogCoverImage.tsx b/src/components/BlogCoverImage.tsx new file mode 100644 index 0000000..3e466ed --- /dev/null +++ b/src/components/BlogCoverImage.tsx @@ -0,0 +1,33 @@ +"use client"; + +import { useState } from "react"; + +type BlogCoverImageProps = { + src: string | null; + className?: string; +}; + +export function BlogCoverImage({ src, className = "" }: BlogCoverImageProps) { + const [failed, setFailed] = useState(false); + + if (!src || failed) { + return ( +

+ + Meshkee Blog + +
+ ); + } + + return ( + setFailed(true)} + className={`h-auto w-full object-contain transition duration-700 group-hover:scale-105 ${className}`.trim()} + /> + ); +} diff --git a/src/components/BlogSection.tsx b/src/components/BlogSection.tsx index 0dc033b..74dab8a 100644 --- a/src/components/BlogSection.tsx +++ b/src/components/BlogSection.tsx @@ -1,4 +1,5 @@ import Link from "next/link"; +import { BlogCoverImage } from "@/components/BlogCoverImage"; import { getBlogCoverImage, getBlogs } from "@/lib/api"; export async function BlogSection() { @@ -51,14 +52,8 @@ export async function BlogSection() { }`} > -
- {image ? ( - - ) : null} +
+

{post.title} diff --git a/src/components/ContentPageShell.tsx b/src/components/ContentPageShell.tsx index cc3e46f..b60bfe1 100644 --- a/src/components/ContentPageShell.tsx +++ b/src/components/ContentPageShell.tsx @@ -4,14 +4,15 @@ import { RevealObserver } from "@/components/RevealObserver"; type ContentPageShellProps = { children: React.ReactNode; + flushTop?: boolean; }; -export function ContentPageShell({ children }: ContentPageShellProps) { +export function ContentPageShell({ children, flushTop = false }: ContentPageShellProps) { return ( <>
-
{children}
+
{children}