diff --git a/public/images/brand/MeshkeeLogo.png b/public/images/brand/MeshkeeLogo.png new file mode 100755 index 0000000..c1efc44 Binary files /dev/null and b/public/images/brand/MeshkeeLogo.png differ 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}