phase 1-2 done

This commit is contained in:
Ali Taghavi
2026-04-24 03:42:34 +03:30
commit 95a546df11
83 changed files with 17168 additions and 0 deletions

13
src/lib/fonts.ts Normal file
View File

@@ -0,0 +1,13 @@
import { JetBrains_Mono, Plus_Jakarta_Sans } from "next/font/google";
export const jetbrainsMono = JetBrains_Mono({
variable: "--font-jetbrains",
subsets: ["latin"],
display: "swap",
});
export const plusJakartaSans = Plus_Jakarta_Sans({
variable: "--font-jakarta",
subsets: ["latin"],
display: "swap",
});

13
src/lib/i18n/config.ts Normal file
View File

@@ -0,0 +1,13 @@
export const locales = ["fa", "en"] as const;
export type Locale = (typeof locales)[number];
export const defaultLocale: Locale = "fa";
export const localeNames: Record<Locale, string> = {
fa: "فارسی",
en: "English",
};
export const localeDir: Record<Locale, "rtl" | "ltr"> = {
fa: "rtl",
en: "ltr",
};

View File

@@ -0,0 +1,5 @@
import { createNavigation } from "next-intl/navigation";
import { routing } from "./routing";
export const { Link, redirect, usePathname, useRouter, getPathname } =
createNavigation(routing);

15
src/lib/i18n/request.ts Normal file
View File

@@ -0,0 +1,15 @@
import { getRequestConfig } from "next-intl/server";
import { routing } from "./routing";
export default getRequestConfig(async ({ requestLocale }) => {
let locale = await requestLocale;
if (!locale || !routing.locales.includes(locale as "fa" | "en")) {
locale = routing.defaultLocale;
}
return {
locale,
messages: (await import(`../../messages/${locale}.json`)).default,
};
});

8
src/lib/i18n/routing.ts Normal file
View File

@@ -0,0 +1,8 @@
import { defineRouting } from "next-intl/routing";
import { locales, defaultLocale } from "./config";
export const routing = defineRouting({
locales,
defaultLocale,
localePrefix: "as-needed",
});

18
src/lib/sanity/client.ts Normal file
View File

@@ -0,0 +1,18 @@
import { createClient } from "next-sanity";
export const client = createClient({
projectId: process.env.NEXT_PUBLIC_SANITY_PROJECT_ID!,
dataset: process.env.NEXT_PUBLIC_SANITY_DATASET ?? "production",
apiVersion: "2024-01-01",
useCdn: process.env.NODE_ENV === "production",
token: process.env.SANITY_API_TOKEN,
});
export async function sanityFetch<T>(
query: string,
params: Record<string, unknown> = {}
): Promise<T> {
return client.fetch<T>(query, params, {
next: { tags: ["sanity"], revalidate: process.env.NODE_ENV === "development" ? 0 : 3600 },
});
}

9
src/lib/sanity/image.ts Normal file
View File

@@ -0,0 +1,9 @@
import imageUrlBuilder from "@sanity/image-url";
import type { SanityImageSource } from "@sanity/image-url/lib/types/types";
import { client } from "./client";
const builder = imageUrlBuilder(client);
export function urlFor(source: SanityImageSource) {
return builder.image(source);
}

98
src/lib/sanity/queries.ts Normal file
View File

@@ -0,0 +1,98 @@
import { groq } from "next-sanity";
// ── Fragments ──────────────────────────────────────────────────────────────
const postFields = groq`
_id, title, slug, locale, category, excerpt, publishedAt, featured,
"coverImage": coverImage { asset->, alt },
"tags": tags[]->{ _id, title, slug },
"author": author->{ name, image }
`;
const projectFields = groq`
_id, title, slug, locale, projectType, description, featured,
techStack, toolsUsed, liveUrl, githubUrl,
"coverImage": coverImage { asset->, alt }
`;
// ── Posts ──────────────────────────────────────────────────────────────────
export const allPostsQuery = groq`
*[_type == "post" && locale == $locale] | order(publishedAt desc) {
${postFields}
}
`;
export const postsByCategoryQuery = groq`
*[_type == "post" && locale == $locale && category == $category] | order(publishedAt desc) {
${postFields}
}
`;
export const featuredPostsQuery = groq`
*[_type == "post" && locale == $locale && featured == true] | order(publishedAt desc)[0...4] {
${postFields}
}
`;
export const postBySlugQuery = groq`
*[_type == "post" && slug.current == $slug][0] {
${postFields},
body,
"seo": seo { title, description, "ogImage": ogImage.asset-> }
}
`;
export const relatedPostsQuery = groq`
*[_type == "post" && locale == $locale && category == $category && slug.current != $slug] | order(publishedAt desc)[0...3] {
${postFields}
}
`;
export const allPostSlugsQuery = groq`
*[_type == "post"] { "slug": slug.current, locale }
`;
// ── Projects ───────────────────────────────────────────────────────────────
export const allProjectsQuery = groq`
*[_type == "project" && locale == $locale] | order(_createdAt desc) {
${projectFields}
}
`;
export const projectsByTypeQuery = groq`
*[_type == "project" && locale == $locale && projectType == $projectType] | order(_createdAt desc) {
${projectFields}
}
`;
export const projectBySlugQuery = groq`
*[_type == "project" && slug.current == $slug][0] {
${projectFields},
body,
"gallery": gallery[] { asset->, alt },
"seo": seo { title, description, "ogImage": ogImage.asset-> }
}
`;
export const allProjectSlugsQuery = groq`
*[_type == "project"] { "slug": slug.current, locale }
`;
// ── Site Settings ──────────────────────────────────────────────────────────
export const siteSettingsQuery = groq`
*[_type == "siteSettings"][0] {
siteTitle, description, currentStatus, telegramChannel, socialLinks,
"defaultOgImage": defaultOgImage.asset->
}
`;
// ── RSS (all fa posts) ─────────────────────────────────────────────────────
export const rssPostsQuery = groq`
*[_type == "post" && locale == "fa"] | order(publishedAt desc)[0...20] {
_id, title, slug, excerpt, publishedAt, category
}
`;

63
src/lib/sanity/types.ts Normal file
View File

@@ -0,0 +1,63 @@
export type PostCategory =
| "founder-notes"
| "marketing-branding"
| "product-thinking"
| "tech-builds"
| "business-experiments"
| "systems-productivity";
export type ProjectType = "product" | "brand-system" | "open-source" | "creative-work";
export interface SanityImage {
asset: { url: string; metadata: { lqip: string; dimensions: { width: number; height: number } } };
alt?: string;
}
export interface Post {
_id: string;
title: string;
slug: { current: string };
locale: "fa" | "en";
category: PostCategory;
excerpt?: string;
publishedAt: string;
featured?: boolean;
coverImage?: SanityImage;
tags?: { _id: string; title: string; slug: { current: string } }[];
author?: { name: string; image?: SanityImage };
body?: unknown[];
seo?: { title?: string; description?: string; ogImage?: { url: string } };
}
export interface Project {
_id: string;
title: string;
slug: { current: string };
locale: "fa" | "en";
projectType: ProjectType;
description?: string;
featured?: boolean;
coverImage?: SanityImage;
gallery?: SanityImage[];
techStack?: string[];
toolsUsed?: string[];
liveUrl?: string;
githubUrl?: string;
body?: unknown[];
seo?: { title?: string; description?: string; ogImage?: { url: string } };
}
export interface SiteSettings {
siteTitle?: string;
description?: string;
currentStatus?: string;
telegramChannel?: string;
socialLinks?: {
github?: string;
linkedin?: string;
twitter?: string;
telegram?: string;
instagram?: string;
};
defaultOgImage?: { url: string };
}

34
src/lib/sanity/utils.ts Normal file
View File

@@ -0,0 +1,34 @@
export function estimateReadTime(body: unknown[]): number {
if (!body) return 1;
const text = body
.filter((b: unknown) => (b as { _type: string })._type === "block")
.map((b: unknown) => {
const block = b as { children?: { text?: string }[] };
return block.children?.map((c) => c.text ?? "").join("") ?? "";
})
.join(" ");
const words = text.trim().split(/\s+/).length;
return Math.max(1, Math.ceil(words / 200));
}
export function formatPersianDate(dateStr: string): string {
if (!dateStr) return "";
try {
const d = new Date(dateStr);
return new Intl.DateTimeFormat("fa-IR", { year: "numeric", month: "long", day: "numeric" }).format(d);
} catch {
return dateStr;
}
}
export function formatDate(dateStr: string, locale: string): string {
if (!dateStr) return "";
try {
const d = new Date(dateStr);
return new Intl.DateTimeFormat(locale === "fa" ? "fa-IR" : "en-US", {
year: "numeric", month: "long", day: "numeric",
}).format(d);
} catch {
return dateStr;
}
}

6
src/lib/utils.ts Normal file
View File

@@ -0,0 +1,6 @@
import { clsx, type ClassValue } from "clsx";
import { twMerge } from "tailwind-merge";
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs));
}