diff --git a/next.config.mjs b/next.config.mjs index 11f2e90..70d8f86 100644 --- a/next.config.mjs +++ b/next.config.mjs @@ -1,6 +1,13 @@ /** @type {import('next').NextConfig} */ const nextConfig = { output: 'standalone', + serverExternalPackages: ['better-sqlite3'], + outputFileTracingIncludes: { + '/*': ['./migrations/**/*'], + }, + outputFileTracingExcludes: { + '*': ['./data/**/*', './backups/**/*', './**/auth-secret', './**/*.sqlite*', './.env*'], + }, experimental: { globalNotFound: true, }, diff --git a/src/app/(en)/layout.tsx b/src/app/(en)/layout.tsx index 11ed4c0..2c60d07 100644 --- a/src/app/(en)/layout.tsx +++ b/src/app/(en)/layout.tsx @@ -1,21 +1,5 @@ -import type { Metadata } from 'next'; -import { SiteRoot } from '@/app/SiteRoot'; -import { PortfolioPage } from '@/app/PortfolioPage'; -import { PortfolioRouteShell } from '@/components/PortfolioRouteShell'; -import { messagesByLocale } from '@/i18n/content'; -import '@/styles/globals.css'; - -export const metadata: Metadata = messagesByLocale.en.metadata; - -export default function EnglishLayout({ - children, -}: Readonly<{ children: React.ReactNode }>) { - return ( - - - - {children} - - - ); +import { PublicLayout } from '@/app/PublicLayout'; +export default async function Layout({ children, }: { children: React.ReactNode; }) { + const locale = undefined; + return {children}; } diff --git a/src/app/(en)/page.tsx b/src/app/(en)/page.tsx index 4032352..c398d0b 100644 --- a/src/app/(en)/page.tsx +++ b/src/app/(en)/page.tsx @@ -1,3 +1,5 @@ -export default function Home() { - return null; +import { publicMetadata } from '@/app/PublicLayout'; +export async function generateMetadata() { + return publicMetadata(); } +export default function Home() { return null; } diff --git a/src/app/(en)/projects/[projectId]/page.tsx b/src/app/(en)/projects/[projectId]/page.tsx index 1c87ff6..6c29ba6 100644 --- a/src/app/(en)/projects/[projectId]/page.tsx +++ b/src/app/(en)/projects/[projectId]/page.tsx @@ -1,24 +1,10 @@ -import type { Metadata } from 'next'; -import { notFound } from 'next/navigation'; -import { CaseStudyContent } from '@/components/CaseStudyContent'; -import { getAdjacentProjects, getProject, projectIds } from '@/data/projects'; - -type Props = { params: Promise<{ projectId: string }> }; - -export const dynamicParams = false; -export const generateStaticParams = () => projectIds.map((projectId) => ({ projectId })); - -export async function generateMetadata({ params }: Props): Promise { - const { projectId } = await params; - const project = getProject('en', projectId); - if (!project) return {}; - return { title: `${project.title} case study | Amirhossein Khalili`, description: project.caseStudy.introduction }; +import { ProjectPage, projectMetadata } from '@/app/ProjectPage'; +type Props = { params: Promise<{ projectId: string; }> }; +export async function generateMetadata({ params }: Props) { + const values = await params; + return projectMetadata(values.projectId); } - -export default async function ProjectPage({ params }: Props) { - const { projectId } = await params; - const project = getProject('en', projectId); - const adjacent = getAdjacentProjects('en', projectId); - if (!project || !adjacent) notFound(); - return ; +export default async function Page({ params }: Props) { + const values = await params; + return ; } diff --git a/src/app/(en)/projects/layout.tsx b/src/app/(en)/projects/layout.tsx index 593c25f..bba52c7 100644 --- a/src/app/(en)/projects/layout.tsx +++ b/src/app/(en)/projects/layout.tsx @@ -1,5 +1,6 @@ import { CaseStudyDrawer } from '@/components/CaseStudyDrawer'; - -export default function ProjectsLayout({ children }: { children: React.ReactNode }) { - return {children}; +import { getPublicRoute } from '@/server/public'; +export default async function ProjectsLayout({ children, }: { children: React.ReactNode; }) { + const { portfolio } = await getPublicRoute(); + return {children}; } diff --git a/src/app/(fa)/fa/layout.tsx b/src/app/(fa)/fa/layout.tsx deleted file mode 100644 index 13c3ef3..0000000 --- a/src/app/(fa)/fa/layout.tsx +++ /dev/null @@ -1,21 +0,0 @@ -import type { Metadata } from 'next'; -import { SiteRoot } from '@/app/SiteRoot'; -import { PortfolioPage } from '@/app/PortfolioPage'; -import { PortfolioRouteShell } from '@/components/PortfolioRouteShell'; -import { messagesByLocale } from '@/i18n/content'; -import '@/styles/globals.css'; - -export const metadata: Metadata = messagesByLocale.fa.metadata; - -export default function PersianLayout({ - children, -}: Readonly<{ children: React.ReactNode }>) { - return ( - - - - {children} - - - ); -} diff --git a/src/app/(fa)/fa/page.tsx b/src/app/(fa)/fa/page.tsx deleted file mode 100644 index 51955ad..0000000 --- a/src/app/(fa)/fa/page.tsx +++ /dev/null @@ -1,3 +0,0 @@ -export default function PersianHome() { - return null; -} diff --git a/src/app/(fa)/fa/projects/[projectId]/page.tsx b/src/app/(fa)/fa/projects/[projectId]/page.tsx deleted file mode 100644 index fb97d70..0000000 --- a/src/app/(fa)/fa/projects/[projectId]/page.tsx +++ /dev/null @@ -1,24 +0,0 @@ -import type { Metadata } from 'next'; -import { notFound } from 'next/navigation'; -import { CaseStudyContent } from '@/components/CaseStudyContent'; -import { getAdjacentProjects, getProject, projectIds } from '@/data/projects'; - -type Props = { params: Promise<{ projectId: string }> }; - -export const dynamicParams = false; -export const generateStaticParams = () => projectIds.map((projectId) => ({ projectId })); - -export async function generateMetadata({ params }: Props): Promise { - const { projectId } = await params; - const project = getProject('fa', projectId); - if (!project) return {}; - return { title: `مطالعه موردی ${project.title} | امیرحسین خلیلی`, description: project.caseStudy.introduction }; -} - -export default async function ProjectPage({ params }: Props) { - const { projectId } = await params; - const project = getProject('fa', projectId); - const adjacent = getAdjacentProjects('fa', projectId); - if (!project || !adjacent) notFound(); - return ; -} diff --git a/src/app/(fa)/fa/projects/layout.tsx b/src/app/(fa)/fa/projects/layout.tsx deleted file mode 100644 index ce9c8a5..0000000 --- a/src/app/(fa)/fa/projects/layout.tsx +++ /dev/null @@ -1,5 +0,0 @@ -import { CaseStudyDrawer } from '@/components/CaseStudyDrawer'; - -export default function ProjectsLayout({ children }: { children: React.ReactNode }) { - return {children}; -} diff --git a/src/app/(localized)/[locale]/layout.tsx b/src/app/(localized)/[locale]/layout.tsx new file mode 100644 index 0000000..368f1a6 --- /dev/null +++ b/src/app/(localized)/[locale]/layout.tsx @@ -0,0 +1,5 @@ +import { PublicLayout } from '@/app/PublicLayout'; +export default async function Layout({ children, params }: { children: React.ReactNode; params: Promise<{ locale: string }>; }) { + const { locale } = await params; + return {children}; +} diff --git a/src/app/(localized)/[locale]/page.tsx b/src/app/(localized)/[locale]/page.tsx new file mode 100644 index 0000000..e0bb2da --- /dev/null +++ b/src/app/(localized)/[locale]/page.tsx @@ -0,0 +1,5 @@ +import { publicMetadata } from '@/app/PublicLayout'; +export async function generateMetadata({ params }: { params: Promise<{ locale: string }> }) { + return publicMetadata((await params).locale); +} +export default function Home() { return null; } diff --git a/src/app/(localized)/[locale]/projects/[projectId]/page.tsx b/src/app/(localized)/[locale]/projects/[projectId]/page.tsx new file mode 100644 index 0000000..a4438bb --- /dev/null +++ b/src/app/(localized)/[locale]/projects/[projectId]/page.tsx @@ -0,0 +1,10 @@ +import { ProjectPage, projectMetadata } from '@/app/ProjectPage'; +type Props = { params: Promise<{ projectId: string; locale: string; }> }; +export async function generateMetadata({ params }: Props) { + const values = await params; + return projectMetadata(values.projectId, values.locale); +} +export default async function Page({ params }: Props) { + const values = await params; + return ; +} diff --git a/src/app/(localized)/[locale]/projects/layout.tsx b/src/app/(localized)/[locale]/projects/layout.tsx new file mode 100644 index 0000000..29de877 --- /dev/null +++ b/src/app/(localized)/[locale]/projects/layout.tsx @@ -0,0 +1,6 @@ +import { CaseStudyDrawer } from '@/components/CaseStudyDrawer'; +import { getPublicRoute } from '@/server/public'; +export default async function ProjectsLayout({ children, params }: { children: React.ReactNode; params: Promise<{ locale: string }>; }) { + const { portfolio } = await getPublicRoute([(await params).locale]); + return {children}; +} diff --git a/src/app/PortfolioPage.tsx b/src/app/PortfolioPage.tsx index 2d54ae7..5748687 100644 --- a/src/app/PortfolioPage.tsx +++ b/src/app/PortfolioPage.tsx @@ -6,19 +6,19 @@ import { EducationSection } from '@/components/EducationSection'; import { MilestonesSection } from '@/components/MilestonesSection'; import { FooterSection } from '@/components/FooterSection'; import { MotionController } from '@/components/MotionController'; -import type { Locale } from '@/i18n/content'; +import type { PortfolioProps } from '@/server/content'; -export function PortfolioPage({ locale }: { locale: Locale }) { +export function PortfolioPage({ portfolio }: PortfolioProps) { return ( -
+
- - - - - - - + + {portfolio.sections.projects.visible && portfolio.projects.length > 0 && } + {portfolio.sections.experience.visible && portfolio.experience.length > 0 && } + {portfolio.sections.skills.visible && portfolio.skills.length > 0 && } + {portfolio.sections.education.visible && portfolio.education.length > 0 && } + {portfolio.sections.milestones.visible && portfolio.milestones.length > 0 && } + {portfolio.sections.footer.visible && }
); } diff --git a/src/app/ProjectPage.tsx b/src/app/ProjectPage.tsx new file mode 100644 index 0000000..f2182cf --- /dev/null +++ b/src/app/ProjectPage.tsx @@ -0,0 +1,17 @@ +import type { Metadata } from 'next'; +import { CaseStudyContent } from '@/components/CaseStudyContent'; +import { getPublicRoute } from '@/server/public'; +import { projectPath } from '@/content/contracts'; + +export async function projectMetadata(id: string, locale?: string): Promise { + const { portfolio, project } = await getPublicRoute([...(locale ? [locale] : []), 'projects', id]); + return { title: `${project!.title} | ${portfolio.metadata.title}`, description: project!.caseStudy.introduction, + alternates: { canonical: projectPath(portfolio.locale, id, portfolio.defaultLanguage), languages: Object.fromEntries(portfolio.languages.map(language => [language.code, projectPath(language.code, id, portfolio.defaultLanguage)])) } }; +} +export async function ProjectPage({ id, locale }: { id: string; locale?: string }) { + const { portfolio, project } = await getPublicRoute([...(locale ? [locale] : []), 'projects', id]); + const index = portfolio.projects.findIndex(item => item.id === id); + const previous = portfolio.projects[(index - 1 + portfolio.projects.length) % portfolio.projects.length]; + const next = portfolio.projects[(index + 1) % portfolio.projects.length]; + return ; +} diff --git a/src/app/PublicLayout.tsx b/src/app/PublicLayout.tsx new file mode 100644 index 0000000..5b92dd5 --- /dev/null +++ b/src/app/PublicLayout.tsx @@ -0,0 +1,21 @@ +import type { Metadata } from 'next'; +import { SiteRoot } from './SiteRoot'; +import { PortfolioPage } from './PortfolioPage'; +import { PortfolioRouteShell } from '@/components/PortfolioRouteShell'; +import { getPublicRoute } from '@/server/public'; +import { portfolioPath } from '@/content/contracts'; +import '@/styles/globals.css'; + +export async function publicMetadata(locale?: string): Promise { + const { portfolio } = await getPublicRoute(locale ? [locale] : []); + const canonical = portfolioPath(portfolio.locale, portfolio.defaultLanguage); + return { ...portfolio.metadata, alternates: { canonical, languages: Object.fromEntries(portfolio.languages.map(language => [language.code, portfolioPath(language.code, portfolio.defaultLanguage)])) } }; +} +export async function PublicLayout({ children, locale }: { children: React.ReactNode; locale?: string }) { + const { portfolio } = await getPublicRoute(locale ? [locale] : []); + return + + {children} + + ; +} diff --git a/src/app/SiteRoot.tsx b/src/app/SiteRoot.tsx index ee22ec8..19db69e 100644 --- a/src/app/SiteRoot.tsx +++ b/src/app/SiteRoot.tsx @@ -1,7 +1,6 @@ import type { ReactNode } from 'react'; import localFont from 'next/font/local'; import { ThemeProvider } from '@/context/ThemeContext'; -import type { Locale } from '@/i18n/content'; const themeScript = ` try { @@ -43,14 +42,16 @@ const peyda = localFont({ export function SiteRoot({ children, locale, + direction = 'ltr', }: { children: ReactNode; - locale: Locale; + locale: string; + direction?: 'rtl' | 'ltr'; }) { return ( diff --git a/src/app/global-not-found.tsx b/src/app/global-not-found.tsx index 707768f..f02d876 100644 --- a/src/app/global-not-found.tsx +++ b/src/app/global-not-found.tsx @@ -1,72 +1,20 @@ -import type { Metadata } from 'next'; -import localFont from 'next/font/local'; import Link from 'next/link'; import { ArrowLeft, ArrowRight } from 'lucide-react'; -import { messagesByLocale, type Locale } from '@/i18n/content'; +import { SiteRoot } from './SiteRoot'; +import { readPublicSnapshot } from '@/server/public'; +import { publicContent } from '@/server/content'; import '@/styles/globals.css'; import styles from './global-not-found.module.css'; -const themeAndLocaleScript = ` - try { - const root = document.documentElement; - const isPersian = /^\\/fa(?:\\/|$)/.test(window.location.pathname); - root.lang = isPersian ? 'fa' : 'en'; - root.dir = isPersian ? 'rtl' : 'ltr'; - const savedTheme = localStorage.getItem('portfolio-theme'); - root.dataset.theme = savedTheme === 'dark' ? 'dark' : 'light'; - } catch {} -`; - -const iranYekan = localFont({ - src: '../../public/fonts/IRANYekanXVF.woff2', - variable: '--font-body', - weight: '100 1000', - style: 'normal', - display: 'swap', - preload: false, - fallback: ['-apple-system', 'BlinkMacSystemFont', 'Segoe UI', 'sans-serif'], -}); - -export const metadata: Metadata = { - title: '404 | Amirhossein Khalili', - description: 'The requested page could not be found.', -}; - -function NotFoundContent({ locale }: { locale: Locale }) { - const copy = messagesByLocale[locale].notFound; - const isRtl = locale === 'fa'; - - return ( -
- -
- ); -} - -export default function GlobalNotFound() { - return ( - - -