Skip to content
Open

Dam #183

Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .env.local.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
NEXT_PUBLIC_EMAILJS_SERVICE_ID=
NEXT_PUBLIC_EMAILJS_TEMPLATE_ID=
NEXT_PUBLIC_EMAILJS_PUBLIC_KEY=
3 changes: 2 additions & 1 deletion .vscode/settings.json
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
{
"typescript.tsdk": "node_modules/typescript/lib",
"typescript.enablePromptUseWorkspaceTsdk": true
"typescript.enablePromptUseWorkspaceTsdk": true,
"chat.disableAIFeatures": false
}
71 changes: 71 additions & 0 deletions README-ADMIN.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
# Decap CMS Admin Dashboard

This project uses **Decap CMS** (formerly Netlify CMS) to let you edit site content through a visual admin interface at `/admin`.

## How to Access

### Local Development
1. Run `npm run dev`
2. Visit `http://localhost:3000/admin`
3. You'll need to set up a local backend proxy (see "Local Backend" below)

### Production (Vercel/Netlify)
1. Deploy the site to Vercel or Netlify
2. Visit `https://your-site.com/admin`
3. Authenticate with GitHub

## Setup Requirements

### Option 1: GitHub OAuth App (Recommended for Vercel)

1. Go to GitHub Settings → Developer settings → OAuth Apps → **New OAuth App**
2. Fill in:
- **Application name**: `DAM CMS`
- **Homepage URL**: `https://your-site.com` (or `http://localhost:3000` for dev)
- **Authorization callback URL**: `https://api.github.com/oauth/authorize`
3. Click **Register application**
4. Copy the **Client ID** and generate a **Client Secret**
5. Add these as environment variables in your hosting dashboard:
- `GITHUB_CLIENT_ID`
- `GITHUB_CLIENT_SECRET`

### Option 2: Netlify Identity + Git Gateway (Recommended for Netlify)

1. In your Netlify site dashboard, go to **Identity** → **Enable Identity**
2. Go to **Settings & Usage** → **Services** → **Git Gateway** → **Enable Git Gateway**
3. In `public/admin/config.yml`, change the backend to:
```yaml
backend:
name: git-gateway
```

## What You Can Edit

| Collection | What it edits | File |
|------------|--------------|------|
| **Site Content (EN)** | Hero, About, Contact, Footer text | `messages/en.json` |
| **Site Content (AR)** | Arabic versions of above | `messages/ar.json` |
| **Services (EN)** | 6 service cards on homepage | `messages/en.json` → `servicesData` |
| **Services (AR)** | Arabic service cards | `messages/ar.json` → `servicesData` |
| **Projects (EN)** | 4 project cards on homepage | `messages/en.json` → `projectsData` |
| **Projects (AR)** | Arabic project cards | `messages/ar.json` → `projectsData` |

## How Publishing Works

- Every save in Decap CMS creates a **real Git commit** to your repository
- This triggers an automatic redeploy on Vercel/Netlify (takes ~1-2 minutes)
- Changes are **not instant** — there's a short delay while the site rebuilds

## Local Backend (for development)

For local testing, you can use Decap's local backend:

1. Install the local backend: `npx decap-server`
2. In `public/admin/config.yml`, uncomment: `local_backend: true`
3. Run both the dev server and the local backend simultaneously

## Important Notes

- **SVG icons** in the Services section are hardcoded in `components/features.tsx` — they cannot be edited via the CMS. Only the text (title, body) and image paths are editable.
- **Image paths** in the CMS should be relative to `/public/images/` (e.g., `/images/services/content-services.jpg`)
- The **Detailed Projects** collection (for the `/projects` page) is read-only from the CMS since the data is hardcoded in `app/[locale]/(default)/projects/page.tsx`
31 changes: 0 additions & 31 deletions app/(default)/layout.tsx

This file was deleted.

78 changes: 78 additions & 0 deletions app/[locale]/(default)/blog/[slug]/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
import Image from "next/image";
import Link from "next/link";
import { notFound } from "next/navigation";
import { remark } from "remark";
import html from "remark-html";
import { getAllPosts, getPostBySlug } from "@/utils/blog";

export async function generateStaticParams() {
const posts = getAllPosts();
return posts.map((post) => ({ slug: post.slug }));
}

export default async function BlogPostPage({
params,
}: {
params: Promise<{ locale: string; slug: string }>;
}) {
const { locale, slug } = await params;
const isArabic = locale === "ar";
const post = getPostBySlug(slug);

if (!post) {
notFound();
}

const processedContent = await remark().use(html).process(post.content);
const contentHtml = processedContent.toString();

return (
<div className="pt-24 md:pt-32">
<article className="mx-auto max-w-4xl px-4 pb-24 sm:px-6">
{/* Back link */}
<Link
href={`/${locale}/blog`}
className="mb-8 inline-flex items-center gap-2 text-sm font-semibold text-royal transition hover:text-royal-light dark:text-gold dark:hover:text-gold-light"
>
<svg className="h-4 w-4 rotate-180" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
<path d="M5 12h14M12 5l7 7-7 7" strokeLinecap="round" strokeLinejoin="round" />
</svg>
{isArabic ? "العودة إلى المدونة" : "Back to blog"}
</Link>

{/* Cover image */}
<div className="relative h-64 w-full overflow-hidden rounded-3xl sm:h-80 md:h-96">
<Image
src={post.cover_image}
alt={isArabic ? post.title_ar : post.title_en}
fill
className="object-cover"
priority
sizes="(max-width: 768px) 100vw, (max-width: 1200px) 80vw, 900px"
/>
</div>

{/* Meta */}
<div className="mt-8">
<span className="text-sm font-semibold uppercase tracking-[0.3em] text-royal dark:text-gold">
{new Date(post.date).toLocaleDateString(isArabic ? "ar" : "en", {
year: "numeric",
month: "long",
day: "numeric",
})}
</span>
<h1 className="mt-3 font-nacelle text-3xl font-semibold text-navy dark:text-paper md:text-4xl">
{isArabic ? post.title_ar : post.title_en}
</h1>
</div>

{/* Content */}
<div
className="prose prose-lg mt-8 max-w-none text-navy/80 dark:text-paper/80 prose-headings:text-navy dark:prose-headings:text-paper prose-a:text-royal dark:prose-a:text-gold"
style={{ direction: isArabic ? "rtl" : "ltr" }}
dangerouslySetInnerHTML={{ __html: contentHtml }}
/>
</article>
</div>
);
}
95 changes: 95 additions & 0 deletions app/[locale]/(default)/blog/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
import Image from "next/image";
import Link from "next/link";
import { getAllPosts } from "@/utils/blog";

export default async function BlogPage({
params,
}: {
params: Promise<{ locale: string }>;
}) {
const { locale } = await params;
const isArabic = locale === "ar";
const posts = getAllPosts();

return (
<div className="pt-24 md:pt-32">
{/* Hero section */}
<section className="relative overflow-hidden">
<div className="pointer-events-none absolute -left-40 -top-40 -z-10 h-[600px] w-[600px] rounded-full opacity-20 blur-3xl"
style={{ background: "radial-gradient(circle, var(--color-royal), transparent 70%)" }}
/>

<div className="mx-auto max-w-6xl px-4 py-16 sm:px-6 md:py-24">
<div className="mx-auto max-w-3xl text-center">
<span className="text-sm font-semibold uppercase tracking-[0.3em] text-royal dark:text-gold">
{isArabic ? "المدونة" : "Blog"}
</span>
<h1 className="mt-3 font-nacelle text-4xl font-semibold text-navy dark:text-paper md:text-5xl">
{isArabic ? "آخر المنشورات" : "Latest posts"}
</h1>
<p className="mt-4 text-lg leading-8 text-navy/75 dark:text-paper/75">
{isArabic
? "أحدث الأخبار والرؤى من فريق دام."
: "Latest news and insights from the DAM team."}
</p>
</div>
</div>
</section>

{/* Blog posts grid */}
<section className="mx-auto max-w-6xl px-4 pb-24 sm:px-6">
{posts.length === 0 ? (
<div className="py-20 text-center">
<p className="text-lg text-navy/60 dark:text-paper/60">
{isArabic ? "لا توجد منشورات بعد." : "No posts yet."}
</p>
</div>
) : (
<div className="grid gap-8 md:grid-cols-2">
{posts.map((post) => (
<Link
key={post.slug}
href={`/${locale}/blog/${post.slug}`}
className="group rounded-3xl border border-line overflow-hidden bg-white shadow-sm transition-all hover:border-royal/30 hover:shadow-xl dark:border-line-dark dark:bg-navy-deep/80 dark:hover:border-gold/30"
>
<div className="relative h-52 w-full overflow-hidden bg-gradient-to-br from-royal/20 to-gold/20 dark:from-gold/10 dark:to-royal/10">
<Image
src={post.cover_image}
alt={isArabic ? post.title_ar : post.title_en}
fill
className="object-cover transition-all duration-700 group-hover:scale-110"
sizes="(max-width: 768px) 100vw, 50vw"
/>
<div className="absolute inset-0 bg-gradient-to-t from-black/40 via-transparent to-transparent" />
<div className="absolute bottom-4 left-4 right-4">
<span className="inline-block rounded-full bg-white/90 px-3 py-1 text-sm font-medium text-navy backdrop-blur">
{new Date(post.date).toLocaleDateString(isArabic ? "ar" : "en", {
year: "numeric",
month: "long",
day: "numeric",
})}
</span>
</div>
</div>
<div className="p-6">
<h2 className="text-xl font-semibold text-navy dark:text-paper">
{isArabic ? post.title_ar : post.title_en}
</h2>
<p className="mt-3 text-sm leading-7 text-navy/70 dark:text-paper/70">
{isArabic ? post.excerpt_ar : post.excerpt_en}
</p>
<span className="mt-6 inline-flex items-center gap-2 text-sm font-semibold text-royal transition hover:text-royal-light dark:text-gold dark:hover:text-gold-light group/link">
{isArabic ? "اقرأ المزيد" : "Read more"}
<svg className="h-4 w-4 transition-transform group-hover/link:translate-x-1" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
<path d="M5 12h14M12 5l7 7-7 7" strokeLinecap="round" strokeLinejoin="round" />
</svg>
</span>
</div>
</Link>
))}
</div>
)}
</section>
</div>
);
}
47 changes: 47 additions & 0 deletions app/[locale]/(default)/layout.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
"use client";

import { useEffect, ReactNode } from "react";
import { useParams } from "next/navigation";
import { NextIntlClientProvider } from "next-intl";
import AOS from "aos";
import "aos/dist/aos.css";
import Header from "@/components/ui/header";
import Footer from "@/components/ui/footer";
import PageBackground from "@/components/ui/page-background";

// Import messages statically so they're available at runtime
import enMessages from "@/messages/en.json";
import arMessages from "@/messages/ar.json";

const messagesMap = {
en: enMessages,
ar: arMessages,
};

export default function DefaultLayout({ children }: { children: ReactNode }) {
const params = useParams<{ locale?: string }>();
const locale = params?.locale === "ar" ? "ar" : "en";
const dir = locale === "ar" ? "rtl" : "ltr";
const messages = messagesMap[locale];

useEffect(() => {
AOS.init({
once: true,
disable: "phone",
duration: 600,
easing: "ease-out-sine",
});
}, []);

return (
<NextIntlClientProvider locale={locale} messages={messages} timeZone="Africa/Cairo">
<div dir={dir} lang={locale} className="min-h-screen">
<PageBackground>
<Header />
<main className="relative flex grow flex-col">{children}</main>
<Footer />
</PageBackground>
</div>
</NextIntlClientProvider>
);
}
11 changes: 6 additions & 5 deletions app/(default)/page.tsx → app/[locale]/(default)/page.tsx
Original file line number Diff line number Diff line change
@@ -1,15 +1,16 @@
export const metadata = {
title: "Home - Open PRO",
description: "Page description",
};

import PageIllustration from "@/components/page-illustration";
import Hero from "@/components/hero-home";
import Workflows from "@/components/workflows";
import Features from "@/components/features";
import Testimonials from "@/components/testimonials";
import Cta from "@/components/cta";

export const metadata = {
title: "DAM | Business Development & Enablement",
description:
"DAM is a multidisciplinary business development partner delivering content, digital solutions, brand identity, training, HR enablement, and tender management.",
};

export default function Home() {
return (
<>
Expand Down
Loading