phase 1-2 done
This commit is contained in:
13
src/lib/fonts.ts
Normal file
13
src/lib/fonts.ts
Normal 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
13
src/lib/i18n/config.ts
Normal 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",
|
||||
};
|
||||
5
src/lib/i18n/navigation.ts
Normal file
5
src/lib/i18n/navigation.ts
Normal 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
15
src/lib/i18n/request.ts
Normal 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
8
src/lib/i18n/routing.ts
Normal 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
18
src/lib/sanity/client.ts
Normal 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
9
src/lib/sanity/image.ts
Normal 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
98
src/lib/sanity/queries.ts
Normal 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
63
src/lib/sanity/types.ts
Normal 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
34
src/lib/sanity/utils.ts
Normal 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
6
src/lib/utils.ts
Normal 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));
|
||||
}
|
||||
Reference in New Issue
Block a user