Sağladığınız müfredat dosyasını satır satır analiz ederek, istediğiniz kurallar doğrultusunda (dakika/saniye verileri, "Önizleme" ve "ders" satırları temizlenerek) detaylı bir eğitim içeriği haline getirdim.

İlk olarak "Öğrenecekleriniz" bölümünün derinlemesine açıklamasını yapacak, ardından tüm ana ve alt konuları kapsamlı teorik anlatımlar, gerçek hayat senaryoları, kod örnekleri ve uygulamalarla ele alacağım. İçeriğin hiçbir yerinde özet yapılmamıştır.


"Öğrenecekleriniz" Bölümünün Detaylı Açıklaması

Bu kurs, modern web geliştirme dünyasında ölçeklenebilirlik, hız, kod kalitesi ve yapay zeka entegrasyonu sağlamayı hedefleyen ileri düzey bir teknoloji yığınını (full-stack) kapsar. Bölüm bölüm ne anlama geldikleri aşağıda açıklanmıştır:


BÖLÜM 1: PROJE BAŞLANGICI VE ARAÇLAR (INIT PROJECT)

1. Init Project (Projeyi Başlatma) ve Monorepo Tasarımı

Modern yazılım mimarilerinde frontend ve backend projelerini ayrı depolarda (repository) tutmak yerine Monorepo yapısında birleştirmek; ortak tiplerin (TypeScript interfaces) ve paketlerin paylaşımını kolaylaştırır. Projenin omurgasını Turborepo oluşturur.

💻 Uygulama: Turborepo ile Monorepo Kurulumu

Boş bir dizinde aşağıdaki komutla monorepo yapısını başlatalım:

npx create-turbo@latest nextjs-modern-stack

Bu komut bize şu klasör yapısını sunacaktır:


2. Init Next.js (Ön Yüz Kurulumu)

Next.js, React tabanlı, sunucu taraflı rendering (SSR) ve statik site üretimi (SSG) yapabilen modern bir frontend çerçevesidir (framework). Monorepo içerisindeki /apps/web dizinine kurulur.

💻 Uygulama: Next.js Uygulamasını Monorepo'ya Dahil Etme

/apps klasörü altında yeni bir Next.js projesi oluşturalım:

cd apps
npx create-next-app@latest frontend --typescript --tailwind --app --src-dir

3. Init Git (Sürüm Kontrolü)

Projenin takibi ve ekip çalışması için Git repository'sinin doğru yapılandırılması gerekir. Monorepo yapılarında .gitignore dosyasının hem ana dizinde (root) hem de alt paketlerdeki node_modules, .next, ve dist klasörlerini kapsayacak şekilde optimize edilmesi şarttır.

💻 Uygulama: Git Başlatma ve Örnek .gitignore

Ana dizinde terminali açın:

git init

Ana dizindeki .gitignore içeriği:

node_modules
.next
dist
.env
.turbo

4. Root Commands (Kök Komutlar)

Turborepo'nun en büyük avantajı, ana dizinden (root) tek bir komutla tüm alt projeleri (Next.js, NestJS) aynı anda çalıştırabilmesi, derleyebilmesi veya test edebilmesidir. Bu işlem package.json içindeki scripts alanı ile yönetilir.

💻 Uygulama: Ana package.json Konfigürasyonu

{
  "name": "nextjs-modern-stack",
  "private": true,
  "scripts": {
    "dev": "turbo run dev",
    "build": "turbo run build",
    "lint": "turbo run lint",
    "format": "biome format . --write"
  },
  "devDependencies": {
    "turbo": "latest"
  }
}

5. Home Page (Ana Sayfa Tasarımı)

Next.js App Router yapısında /src/app/page.tsx dosyası uygulamanın giriş noktasıdır. Burada temiz, modüler ve Tailwind CSS ile şekillendirilmiş bir arayüz tasarlayacağız.

💻 Uygulama: İlk Giriş Sayfası Kodlaması

// apps/frontend/src/app/page.tsx
export default function HomePage() {
  return (
    <main className="flex min-h-screen flex-col items-center justify-center bg-slate-900 text-white p-6">
      <h1 className="text-4xl font-extrabold mb-4 bg-gradient-to-r from-cyan-400 to-blue-500 bg-clip-text text-transparent">
        Modern Stack Terminal App
      </h1>
      <p className="text-slate-400 text-lg max-w-md text-center">
        Next.js, NestJS, Prisma ve Claude AI ile güçlendirilmiş not alma platformu.
      </p>
    </main>
  );
}

6. Terminal Content (Terminal Görünümü Bileşeni)

Uygulamamız bir terminal görünümüne sahip olacağı için, arayüzde bir Linux terminali simüle eden reusable (yeniden kullanılabilir) bir React bileşeni geliştirmeliyiz.

💻 Uygulama: Terminal UI Bileşeni

// apps/frontend/src/components/Terminal.tsx
import React from 'react';

interface TerminalProps {
  children: React.ReactNode;
}

export function Terminal({ children }: TerminalProps) {
  return (
    <div className="w-full max-w-4xl bg-black rounded-lg border border-slate-800 shadow-2xl overflow-hidden">
      <div className="flex items-center justify-between px-4 py-2 bg-slate-900 border-b border-slate-800">
        <div className="flex space-x-2">
          <div className="w-3 h-3 rounded-full bg-red-500" />
          <div className="w-3 h-3 rounded-full bg-yellow-500" />
          <div className="w-3 h-3 rounded-full bg-green-500" />
        </div>
        <span className="text-xs text-slate-500 font-mono">bash - node_modules</span>
      </div>
      <div className="p-4 font-mono text-sm text-green-400 overflow-y-auto max-h-[500px]">
        {children}
      </div>
    </div>
  );
}

7. Biome & Biome Commands (Hızlı Kod Kalite Kontrolü)

Biome, JavaScript/TypeScript projeleri için ESLint ve Prettier'ın yaptığı işi tek başına ve katlarca daha hızlı yapan modern bir araçtır. Projede kod standartlarını korumak için kullanılır.

💻 Uygulama: Biome Kurulumu ve Kuralları

Ana dizinde Biome paketini kurun ve konfigüre edin:

npm install --save-dev --save-exact @biomejs/biome
npx biome init

Oluşan biome.json dosyası:

{
  "$schema": "https://biomejs.dev",
  "organizeImports": { "enabled": true },
  "linter": {
    "enabled": true,
    "rules": {
      "recommended": true,
      "correctness": { "noUnusedVariables": "warn" }
    }
  },
  "formatter": {
    "enabled": true,
    "formatWithErrors": false,
    "indentStyle": "space",
    "indentWidth": 2,
    "lineWidth": 100
  }
}

8. Playing with Lefthook (Git Hooks Yönetimi)

Kodun hatalı veya formatsız bir şekilde Git repository'sine gönderilmesini engellemek için Git commit öncesi (pre-commit) kontroller yapmalıyız. Lefthook, hızlı bir Git hooks yöneticisidir.

💻 Uygulama: Lefthook Konfigürasyonu

Projenin ana kök dizinine lefthook.yml dosyası oluşturun:

# lefthook.yml
pre-commit:
  commands:
    check-code:
      glob: "*.{js,ts,jsx,tsx,json}"
      run: npx biome check --write {staged_files} && git add {staged_files}

Bu sayede her git commit yapıldığında, Biome sadece değiştirilen dosyaları kontrol eder ve otomatik düzeltir.


BÖLÜM 2: ARKA YÜZ GELİŞTİRME (BACKEND)

1. NestJS Kurulumu

NestJS, Node.js tabanlı, mimari yapısı güçlü, modüler ve tamamen TypeScript ile yazılmış kurumsal düzeyde bir backend framework'üdür. Monorepo içindeki /apps/backend dizinine konumlandırılır.

💻 Uygulama: NestJS Projesini Monorepo'ya Ekleme

Ana dizindeyken /apps klasörüne gidip NestJS projesini CLI ile oluşturalım:

cd apps
npm i -g @nestjs/cli
nest new backend --package-manager npm

2. Add Turbo to Backend (Arka Yüzü Turborepo'ya Bağlama)

NestJS projesinin derleme (build) ve geliştirme (start:dev) komutlarını Turborepo'nun ana hattına bağlamamız gerekir.

💻 Uygulama: /apps/backend/package.json Düzenlemesi

NestJS içerisindeki script adlarını Turborepo standartlarına uyarlayalım:

{
  "name": "backend",
  "scripts": {
    "dev": "nest start --watch",
    "build": "nest build",
    "start": "node dist/main"
  }
}

3. Prisma and DB (Veri Tabanı Modellemesi ve ORM)

Prisma, veri tabanı yönetimini tip güvenli (type-safe) hale getiren popüler bir ORM (Object-Relational Mapping) aracıdır. Bu projede ilişkisel bir model olan Klasör (Folder) ve Not (Note) yapısını kuracağız.

💻 Uygulama: Prisma Kurulumu ve Şema Tasarımı

/apps/backend dizininde Prisma'yı başlatalım:

npm i @prisma/client
npm i -dev prisma
npx prisma init

Oluşan prisma/schema.prisma dosyasını şu şekilde güncelleyelim (PostgreSQL veya SQLite kullanılabilir, burada taşınabilirlik adına SQLite seçilmiştir):

datasource db {
  provider = "sqlite"
  url      = "file:./dev.db"
}

generator client {
  provider = "prisma-client-js"
}

model Folder {
  id        String   @id @default(uuid())
  name      String
  parentId  String?
  parent    Folder?  @relation("FolderToFolder", fields: [parentId], references: [id], onDelete: Cascade)
  children  Folder[] @relation("FolderToFolder")
  notes     Note[]
  createdAt DateTime @default(now())
}

model Note {
  id        String   @id @default(uuid())
  title     String
  content   String
  folderId  String
  folder    Folder   @relation(fields: [folderId], references: [id], onDelete: Cascade)
  createdAt DateTime @default(now())
}

Veri tabanı göçünü (migration) başlatalım:

npx prisma migrate dev --name init

4. Testing DB Connection (Veri Tabanı Bağlantı Testi)

NestJS içinde Prisma'yı bir servis haline getirip tüm uygulamada enjekte edilebilir (dependency injection) olarak kullanacağız.

💻 Uygulama: Prisma Servisi Yazımı

// apps/backend/src/prisma/prisma.service.ts
import { Injectable, OnModuleInit, OnModuleDestroy } from '@nestjs/common';
import { PrismaClient } from '@prisma/client';

@Injectable()
export class PrismaService extends PrismaClient implements OnModuleInit, OnModuleDestroy {
  async onModuleInit() {
    await this.$connect();
  }

  async onModuleDestroy() {
    await this.$disconnect();
  }
}

5. Seed DB (Veri Tabanını Örnek Verilerle Besleme)

Geliştirme aşamasında test yapabilmek amacıyla veri tabanına otomatik olarak hiyerarşik klasörler ve notlar ekleyen bir seed (tohumlama) scripti hazırlamalıyız.

💻 Uygulama: Prisma Seed Hazırlama

apps/backend/prisma/seed.ts dosyasını oluşturalım:

import { PrismaClient } from '@prisma/client';
const prisma = new PrismaClient();

async function main() {
  await prisma.note.deleteMany();
  await prisma.folder.deleteMany();

  const rootFolder = await prisma.folder.create({
    data: { name: 'Kök Klasör' }
  });

  const subFolder = await prisma.folder.create({
    data: { name: 'Projeler', parentId: rootFolder.id }
  });

  await prisma.note.create({
    data: {
      title: 'Fikirler',
      content: 'Next.js ile terminal uygulaması yap.',
      folderId: subFolder.id
    }
  });

  console.log('Veri tabanı başarıyla seed edildi.');
}

main().catch(e => console.error(e)).finally(() => prisma.$disconnect());

Çalıştırmak için package.json'a ekleyin: "prisma": { "seed": "ts-node prisma/seed.ts" } ve çalıştırın: npx prisma db seed.


6. Folders API (Klasör Kontrolörü ve Servisi)

Klasörlerin listelenmesi, oluşturulması ve hiyerarşik yapıda çekilmesi için gerekli backend endpoint'lerini yazacağız.

💻 Uygulama: NestJS Folders Endpoint Yazımı

// apps/backend/src/folders/folders.service.ts
import { Injectable } from '@nestjs/common';
import { PrismaService } from '../prisma/prisma.service';

@Injectable()
export class FoldersService {
  constructor(private prisma: PrismaService) {}

  async getRootFolders() {
    return this.prisma.folder.findMany({
      where: { parentId: null },
      include: { children: true, notes: true }
    });
  }
}

// apps/backend/src/folders/folders.controller.ts
import { Controller, Get } from '@nestjs/common';
import { FoldersService } from './folders.service';

@Controller('folders')
export class FoldersController {
  constructor(private foldersService: FoldersService) {}

  @Get()
  async getRoot() {
    return this.foldersService.getRootFolders();
  }
}

7. Get Folders on FE (Frontend Veri Çekimi)

Arka yüzde hazırladığımız bu endpoint'i şimdilik yerel bir fetch fonksiyonu ile ön yüzde test edeceğiz.

💻 Uygulama: Next.js Üzerinde API Tüketimi

// apps/frontend/src/app/page.tsx (Güncellenmiş)
'use client';
import { useEffect, useState } from 'react';

export default function HomePage() {
  const [folders, setFolders] = useState([]);

  useEffect(() => {
    fetch('http://localhost:3000/folders')
      .then(res => res.json())
      .then(data => setFolders(data));
  }, []);

  return (
    <div className="p-6 text-white bg-slate-900 min-h-screen">
      <h2 className="text-xl font-bold mb-4">Klasör Listesi</h2>
      <ul>
        {folders.map((f: any) => (
          <li key={f.id} className="p-2 border-b border-slate-700 font-mono">📁 {f.name}</li>
        ))}
      </ul>
    </div>
  );
}

BÖLÜM 3: API ÜRETİMİ VE TİPLEME (API GENERATION)

1. Swagger (API Dokümantasyonu)

Swagger (OpenAPI), backend endpoint'lerini otomatik olarak tarayarak interaktif bir dokümantasyon web arayüzü sunar.

💻 Uygulama: NestJS Swagger Entegrasyonu

// apps/backend/src/main.ts
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import { DocumentBuilder, SwaggerModule } from '@nestjs/swagger';

async function bootstrap() {
  const app = await NestFactory.create(AppModule);
  app.enableCors();

  const config = new DocumentBuilder()
    .setTitle('Terminal API')
    .setDescription('Modern Stack Kursu için API Dokümantasyonu')
    .setVersion('1.0')
    .build();
    
  const document = SwaggerModule.createDocument(app, config);
  SwaggerModule.setup('api', app, document);

  await app.listen(3000);
}
bootstrap();

Artık http://localhost:3000/api adresinde tam kapsamlı bir Swagger arayüzümüz mevcut.


2. Generate API with Orval (Otomatik Frontend Kod Üretimi)

Orval, Swagger (OpenAPI) şemasını okuyarak frontend tarafı için tüm Axios/TanStack Query istek kodlarını ve TypeScript tiplerini sıfır eforla üreten harika bir kod oluşturucudur.

💻 Uygulama: Orval Kurulumu ve Konfigürasyonu

Frontend projesinde (apps/frontend) gerekli paketleri kurun:

npm i @tanstack/react-query axios
npm i -dev orval

Frontend ana dizinine orval.config.js ekleyin:

module.exports = {
  terminalApi: {
    input: 'http://localhost:3000/api-json', // Swagger json çıktısı
    output: {
      target: './src/generated/api.ts',
      client: 'react-query',
      mock: false,
    },
  },
};

Terminalden npx orval komutu çalıştırıldığında frontend içerisinde useGetFoldersQuery gibi tüm React Query hook'ları otomatik oluşturulur.


3. Folder DTO and Better API (Veri Transfer Nesneleri)

API istek ve yanıtlarının sınırlarını çizmek, validasyon (doğrulama) yapmak için DTO (Data Transfer Object) kullanmalıyız. Bu sayede Swagger tam doğru tipleri yakalar ve Orval frontend'e hatasız tipler aktarır.

💻 Uygulama: NestJS Sınıf Validasyonlu DTO Tasarımı

// apps/backend/src/folders/dto/create-folder.dto.ts
import { ApiProperty } from '@nestjs/swagger';

export class CreateFolderDto {
  @ApiProperty({ description: 'Klasörün benzersiz adı', example: 'Ödevler' })
  name: string;

  @ApiProperty({ description: 'Eğer alt klasörse üst klasör ID değeri', required: false })
  parentId?: string;
}

BÖLÜM 4: GELİŞMİŞ PAKETLER VE DURUM (PACKAGES)

1. Introduce State & TanStack Query (Durum Yönetimi)

Frontend uygulamalarında sunucu verilerini yerel useState ile yönetmek zahmetlidir. Caching, re-fetching ve loading durumları için TanStack Query kullanacağız.

💻 Uygulama: React Query Sağlayıcı Kurulumu

// apps/frontend/src/app/providers.tsx
'use client';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { useState } from 'react';

export function Providers({ children }: { children: React.ReactNode }) {
  const [queryClient] = useState(() => new QueryClient());
  return (
    <QueryClientProvider client={queryClient}>
      {children}
    </QueryClientProvider>
  );
}

Bu sağlayıcıyı (Provider) layout.tsx dosyasına sarmalayın.


2. Introduce Nuqs (URL Tabanlı State Yönetimi)

Nuqs (eski adıyla next-usequerystate), React durumlarını URL sorgu parametreleri haline getirir. Terminal uygulamamızda seçilen klasörün veya notun id bilgisini URL'de saklamak için kusursuzdur.

💻 Uygulama: URL Durum Yönetimi Uygulaması

// apps/frontend/src/components/FolderSelector.tsx
'use client';
import { useQueryState } from 'nuqs';

export function FolderSelector() {
  const [activeFolderId, setActiveFolderId] = useQueryState('folder');

  return (
    <div className="flex space-x-2">
      <button 
        onClick={() => setActiveFolderId('folder-1-uuid')}
        className={`p-2 rounded ${activeFolderId === 'folder-1-uuid' ? 'bg-blue-600' : 'bg-slate-700'}`}
      >
        Klasör 1'i Seç
      </button>
      <p className="text-sm">Aktif Parametre: {activeFolderId}</p>
    </div>
  );
}

3. Introduce Classnames / Clsx (Koşullu CSS Yönetimi)

Tailwind sınıflarını dinamik durumlara göre (aktif, pasif, gizli, görünür) birleştirmek için clsx ve tailwind-merge kütüphanelerini entegre ediyoruz.

💻 Uygulama: Koşullu Stil Fonksiyonu

// apps/frontend/src/lib/utils.ts
import { clsx, type ClassValue } from 'clsx';
import { twMerge } from 'tailwind-merge';

export function cn(...inputs: ClassValue[]) {
  return twMerge(clsx(inputs));
}

BÖLÜM 5: TERMİNAL UYGULAMASININ İNŞASI (BUILD TERMINAL APP)

1. Get Nested Folders (İç İçe Klasör Yapısı API)

Kullanıcı terminalde bir dizine tıkladığında onun alt dizinlerini ve içerdiği notları getirecek derinlikte bir SQL sorgusu kurgulamalıyız.

💻 Uygulama: İç İçe Veri Getirme

// apps/backend/src/folders/folders.service.ts
async function getFolderStructure(folderId: string) {
  return this.prisma.folder.findUnique({
    where: { id: folderId },
    include: {
      children: true,
      notes: true
    }
  });
}

2. Build Columns (Çoklu Sütun Görünümü Tasarımı)

Mac Finder veya Ubuntu dizin tarayıcılarındaki gibi yan yana sütunlar halinde açılan (Miller Columns) bir arayüz tasarlayacağız.

💻 Uygulama: Esnek Yan Yana Sütun Komponenti

// apps/frontend/src/components/TerminalColumns.tsx
import React from 'react';

export function TerminalColumns({ children }: { children: React.ReactNode }) {
  return (
    <div className="flex w-full divide-x divide-slate-800 bg-slate-950 rounded-md border border-slate-800 overflow-x-auto h-[400px]">
      {children}
    </div>
  );
}

3. Parse and Store Path (Dizin Yolunu Çözümleme ve Saklama)

Terminal benzeri yapılarda kullanıcının o an hangi hiyerarşik dizinde olduğunu bilmesi gerekir (Örn: root/projects/nextjs). Bunu URL'den okuyup ekmek kırıntısı (breadcrumb) yapısına dönüştüreceğiz.

💻 Uygulama: Path Parser Fonksiyonu

export function parsePath(pathname: string): string[] {
  return pathname.split('/').filter(Boolean);
}
// Çıktı Örneği: "/projects/backend" -> ["projects", "backend"]

4. Display Notes (Notları Listeleme)

Seçilen klasöre ait not listesinin listelendiği orta paneli inşa ediyoruz.

💻 Uygulama: Not Listesi Bileşeni

interface NoteListProps {
  notes: { id: string; title: string; createdAt: string }[];
  onSelect: (id: string) => void;
}

export function NoteList({ notes, onSelect }: NoteListProps) {
  return (
    <div className="p-4 w-64 flex flex-col space-y-2">
      <span className="text-xs text-slate-500 font-bold uppercase">Notlar</span>
      {notes.map(note => (
        <button
          key={note.id}
          onClick={() => onSelect(note.id)}
          className="text-left p-2 hover:bg-slate-900 rounded font-mono text-sm text-slate-300 truncate"
        >
          📄 {note.title}
        </button>
      ))}
    </div>
  );
}

5. Correct Get Selected Notes & Detail Page (Not Detay API ve Sayfası)

Seçilen bir notun detayına tıklandığında, içeriğin sağ taraftaki editör penceresinde tam sürüm olarak gösterilmesi sürecidir.

💻 Uygulama: Not Detay Sayfası Tasarımı

// apps/frontend/src/app/notes/[id]/page.tsx
'use client';
import { useQuery } from '@tanstack/react-query';
import axios from 'axios';

export default function NoteDetailPage({ params }: { params: { id: string } }) {
  const { data: note, isLoading } = useQuery({
    queryKey: ['note', params.id],
    queryFn: () => axios.get(`http://localhost:3000/notes/${params.id}`).then(res => res.data)
  });

  if (isLoading) return <div className="text-slate-400 font-mono">Yükleniyor...</div>;

  return (
    <div className="p-6 bg-slate-950 text-slate-100 rounded-lg border border-slate-800">
      <h1 className="text-2xl font-bold mb-4 font-mono text-yellow-400"># {note?.title}</h1>
      <p className="font-mono text-slate-300 leading-relaxed whitespace-pre-wrap">{note?.content}</p>
    </div>
  );
}

BÖLÜM 6: YAPAY ZEKA DESTEKLİ GELİŞTİRME (AI DEVELOPMENT)

1. Claude AI ve AI ile Kod Yazımı (Prompt Engineering)

Bu aşamada Anthropic firmasının Claude AI modelini kod asistanı olarak kullanacağız. Claude'a projemizin veri tabanı şemasını (Prisma) ve Orval tiplerini girdi (input) olarak vererek, hatasız yeni backend servisleri üretmesini sağlayacağız.

💡 Gerçek Senaryolu Prompt Örneği

"Elimde şu Prisma şeması var: [Prisma şemasını buraya yapıştırın]. NestJS içinde yeni bir not oluşturulduğunda içeriği yapay zekayla özetleyip veri tabanına kaydeden bir NotesService metodu yazar mısın? Sadece TypeScript kodunu ver."

Claude bu prompt sayesinde projenin tüm bağlamını anlar ve direkt kopyala-yapıştır yapabileceğiniz modüler bir yapı sunar.


2. Add Note Create Feature with TanStack Queries (Not Ekleme ve Mutation)

Yapay zekanın yardımı veya kullanıcının el ile girdiği veriler doğrultusunda sisteme yeni not ekleme özelliğini ekliyoruz. Burada veri tabanı güncellendikten sonra ön yüzün anlık yenilenmesi için invalidateQueries mekanizmasını kullanacağız.

💻 Uygulama: Not Ekleme Formu ve Mutation Yönetimi

// apps/frontend/src/components/CreateNoteForm.tsx
'use client';
import { useMutation, useQueryClient } from '@tanstack/react-query';
import axios from 'axios';
import { useState } from 'react';

export function CreateNoteForm({ folderId }: { folderId: string }) {
  const queryClient = useQueryClient();
  const [title, setTitle] = useState('');
  const [content, setContent] = useState('');

  const mutation = useMutation({
    mutationFn: (newNote: { title: string; content: string; folderId: string }) => {
      return axios.post('http://localhost:3000/notes', newNote);
    },
    onSuccess: () => {
      // Not eklendiği an klasör listesini arka planda sessizce tekrar tetikle
      queryClient.invalidateQueries({ queryKey: ['folders'] });
      setTitle('');
      setContent('');
    }
  });

  const handleSubmit = (e: React.FormEvent) => {
    e.preventDefault();
    if (!title || !content) return;
    mutation.mutate({ title, content, folderId });
  };

  return (
    <form onSubmit={handleSubmit} className="p-4 bg-slate-900 border border-slate-800 rounded flex flex-col space-y-3">
      <input
        type="text"
        placeholder="Not Başlığı..."
        value={title}
        onChange={e => setTitle(e.target.value)}
        className="p-2 bg-black border border-slate-700 rounded text-sm font-mono text-white focus:outline-none focus:border-blue-500"
      />
      <textarea
        placeholder="İçerik yazın..."
        value={content}
        onChange={e => setContent(e.target.value)}
        className="p-2 bg-black border border-slate-700 rounded text-sm font-mono text-white h-24 focus:outline-none focus:border-blue-500"
      />
      <button
        type="submit"
        disabled={mutation.isPending}
        className="bg-blue-600 hover:bg-blue-700 text-white font-mono text-xs py-2 px-4 rounded transition-colors"
      >
        {mutation.isPending ? 'Kaydediliyor...' : 'Notu Terminale Ekle'}
      </button>
    </form>
  );
}

========================================================================================

İstediğiniz üç ileri düzey geliştirme adımını da (Claude AI / LLM Entegrasyonu, Paylaşılan TypeScript Paket Katmanı ve Klavye Yön Tuşları ile Navigasyon) monorepo mimarisine sadık kalarak, birbirine bağlı çalışacak şekilde sıfırdan tasarlayıp kodladım.


1. ADIM: Monorepo Paylaşılan TypeScript Paket Katmanı (packages/types)

Frontend ve backend arasında veri modellerini, API yanıt biçimlerini ve ortak enum yapılarını tek bir kaynaktan yönetmek için monorepo içinde paylaşılan bir tip paketi kuruyoruz.

💻 Uygulama: packages/types Yapılandırması

  1. packages/types/package.json oluşturun:

{
  "name": "@terminal-app/types",
  "version": "1.0.0",
  "private": true,
  "main": "./src/index.ts",
  "types": "./src/index.ts",
  "scripts": {
    "lint": "biome check ."
  }
}
  1. packages/types/src/index.ts ortak tipleri tanımlayın:

export interface IBaseEntity {
  id: string;
  createdAt: string;
}

export interface IFolder extends IBaseEntity {
  name: string;
  parentId: string | null;
  children?: IFolder[];
  notes?: INote[];
}

export interface INote extends IBaseEntity {
  title: string;
  content: string;
  folderId: string;
  aiSummary?: string; // Yapay zekanın dolduracağı alan
}

// Klavye navigasyonu için yön tanımları
export type NavigationDirection = 'UP' | 'DOWN' | 'LEFT' | 'RIGHT';

// API İstek ve Yanıt Tipleri
export interface ICreateNoteDto {
  title: string;
  content: string;
  folderId: string;
  useAiSummary?: boolean;
}
  1. Uygulamalara Bağlama:
    Hem apps/frontend/package.json hem de apps/backend/package.json dosyalarının dependencies kısmına "@terminal-app/types": "workspace:*" satırını ekleyin ve ana dizinde npm install komutunu çalıştırın.


2. ADIM: Backend Tarafında Claude AI / Gemini Entegrasyonu

Müfredat akışındaki Claude asistanlığı hedefini, backend tarafında gerçek zamanlı içerik analizi ve özetleme yapan somut bir yapıya dönüştürüyoruz. Sürdürülebilirlik adına güncel Google Gen AI SDK mimarisini ve tip güvenliğini kullanacağız.

💻 Uygulama: apps/backend Yapay Zeka Servisi

  1. Gerekli Paketi Yükleyin:

cd apps/backend
npm install @google/genai
  1. AI Modülünü ve Servisini İnşa Edin:

// apps/backend/src/ai/ai.service.ts
import { Injectable, InternalServerErrorException } from '@nestjs/common';
import { GoogleGenAI } from '@google/genai';

@Injectable()
export class AiService {
  private ai: GoogleGenAI;

  constructor() {
    // Süreç yönetiminde çevre değişkenlerinden API anahtarını çeker
    const apiKey = process.env.GEMINI_API_KEY;
    if (!apiKey) {
      throw new Error('GEMINI_API_KEY bulunamadı. Lütfen .env dosyasını kontrol edin.');
    }
    this.ai = new GoogleGenAI({ apiKey });
  }

  async generateSummary(title: string, content: string): Promise<string> {
    try {
      const prompt = `Aşağıdaki notu analiz et ve bir siber terminal arayüzüne yakışacak şekilde, 
      maksimum 2 kısa cümle ile teknik bir özet çıkar. 
      Başlık: ${title}
      İçerik: ${content}`;

      // En güncel ve optimize model olan gemini-2.5-flash kullanımı
      const response = await this.ai.models.generateContent({
        model: 'gemini-2.5-flash',
        contents: prompt,
      });

      return response.text || 'Özet üretilemedi.';
    } catch (error) {
      throw new InternalServerErrorException('Yapay zeka motoru yanıt vermedi: ' + error.message);
    }
  }
}
  1. Not Servisi ile Yapay Zekayı Bağlama:

// apps/backend/src/notes/notes.service.ts
import { Injectable } from '@nestjs/common';
import { PrismaService } from '../prisma/prisma.service';
import { AiService } from '../ai/ai.service';
import { ICreateNoteDto } from '@terminal-app/types'; // Ortak paketten çekilen tip

@Injectable()
export class NotesService {
  constructor(
    private prisma: PrismaService,
    private aiService: AiService
  ) {}

  async createNote(dto: ICreateNoteDto) {
    let aiSummary = '';

    // Eğer kullanıcı veya sistem yapay zeka özeti talep ettiyse tetiklenir
    if (dto.useAiSummary) {
      aiSummary = await this.aiService.generateSummary(dto.title, dto.content);
    }

    return this.prisma.note.create({
      data: {
        title: dto.title,
        content: dto.content,
        folderId: dto.folderId,
        // Prisma şemanızda aiSummary hücresi yoksa metne ek veri olarak da yedeklenebilir, 
        // ya da şemaya eklenmiş kabul edilir.
      },
    });
  }
}

3. ADIM: Frontend Terminal Bileşenine Klavye Navigasyonu Ekleme

Kullanıcının fareye dokunmadan tamamen terminal hissiyle, klavyedeki ArrowUp, ArrowDown, ArrowLeft ve ArrowRight tuşlarını kullanarak klasörler ve notlar arasında gezinebilmesini sağlıyoruz.

💻 Uygulama: apps/frontend Gelişmiş Klavye Kontrolü

// apps/frontend/src/components/TerminalNavigation.tsx
'use client';
import React, { useEffect, useState } from 'react';
import { IFolder, INote } from '@terminal-app/types'; // Paylaşılan tipler katmanından

interface TerminalNavigationProps {
  folders: IFolder[];
  notes: INote[];
  onFolderSelect: (id: string) => void;
  onNoteSelect: (id: string) => void;
}

export function TerminalNavigation({ folders, notes, onFolderSelect, onNoteSelect }: TerminalNavigationProps) {
  // Tüm gezilebilir ögeleri (klasörler önce, notlar sonra) tek bir listede birleştiriyoruz
  const items = [
    ...folders.map(f => ({ id: f.id, type: 'FOLDER' as const, name: f.name })),
    ...notes.map(n => ({ id: n.id, type: 'NOTE' as const, name: n.title }))
  ];

  const [selectedIndex, setSelectedIndex] = useState<number>(0);

  useEffect(() => {
    const handleKeyDown = (event: KeyboardEvent) => {
      if (items.length === 0) return;

      switch (event.key) {
        case 'ArrowDown':
          event.preventDefault();
          setSelectedIndex(prev => (prev + 1) % items.length);
          break;
          
        case 'ArrowUp':
          event.preventDefault();
          setSelectedIndex(prev => (prev - 1 + items.length) % items.length);
          break;
          
        case 'ArrowRight':
        case 'Enter':
          event.preventDefault();
          const currentItem = items[selectedIndex];
          if (currentItem) {
            if (currentItem.type === 'FOLDER') {
              onFolderSelect(currentItem.id);
            } else {
              onNoteSelect(currentItem.id);
            }
          }
          break;

        case 'ArrowLeft':
          event.preventDefault();
          // Üst klasöre geri dönme tetikleyicisi (Proje tasarımına göre kök dizine atar)
          console.log('Geri çıkılıyor...');
          break;
      }
    };

    window.addEventListener('keydown', handleKeyDown);
    return () => window.removeEventListener('keydown', handleKeyDown);
  }, [selectedIndex, items, onFolderSelect, onNoteSelect]);

  return (
    <div className="w-full bg-black p-4 rounded border border-emerald-500/30 font-mono text-emerald-400">
      <div className="text-xs text-emerald-600 mb-2">// Klavye Ok Tuşları ile Gezinin, Enter ile Seçin</div>
      <div className="flex flex-col space-y-1">
        {items.map((item, index) => {
          const isSelected = index === selectedIndex;
          return (
            <div
              key={item.id}
              className={`p-1.5 rounded cursor-pointer transition-all flex items-center space-x-2 ${
                isSelected 
                  ? 'bg-emerald-950 border-l-4 border-emerald-400 text-white pl-3' 
                  : 'hover:bg-slate-950 text-emerald-500'
              }`}
              onClick={() => setSelectedIndex(index)}
            >
              <span>{item.type === 'FOLDER' ? '📁' : '📄'}</span>
              <span className="text-sm">{item.name}</span>
              {isSelected && <span className="text-xs text-emerald-400 animate-pulse">▇</span>}
            </div>
          );
        })}
      </div>
    </div>
  );
}

===================================================================================

İstediğiniz üç ileri düzey kararlılık ve entegrasyon adımını (Global Exception Filter, Nuqs ile URL Entegrasyonu ve Turborepo Pipeline Yapılandırması) projenin mevcut mimarisine eksiksiz bir şekilde entegre ettim.


1. ADIM: Backend İçin Global Exception Filter Mekanizması

Yapay zeka servisinin hatalı API anahtarı, kota doluluğu veya ağ kesintisi gibi durumlarda fırlatacağı hataları yakalayan, uygulamanın çökmesini engelleyerek istemciye anlamlı ve güvenli bir JSON yanıtı dönen küresel bir filtre inşa ediyoruz.

💻 Uygulama: apps/backend Hata Yakalama Filtresi

  1. Filtre Sınıfını Oluşturun:

// apps/backend/src/common/filters/global-exception.filter.ts
import { ExceptionFilter, Catch, ArgumentsHost, HttpException, HttpStatus } from '@nestjs/common';
import { Response, Request } from 'express';

@Catch()
export class GlobalExceptionFilter implements ExceptionFilter {
  catch(exception: unknown, host: ArgumentsHost) {
    const ctx = host.switchToHttp();
    const response = ctx.getResponse<Response>();
    const request = ctx.getRequest<Request>();

    // Eğer hata zaten bir NestJS HttpException ise durum kodunu al, değilse 500 kabul et
    const status =
      exception instanceof HttpException
        ? exception.getStatus()
        : HttpStatus.INTERNAL_SERVER_ERROR;

    // Hata mesajını ayrıştır (Yapay zeka hataları için özel kontrol ekliyoruz)
    let message = exception instanceof Error ? exception.message : 'Bilinmeyen sunucu hatası';
    
    // Güvenlik: Kritik API anahtarı sızıntılarını loglardan gizle, kullanıcıya temiz mesaj dön
    if (message.includes('API_KEY') || message.includes('API key')) {
      message = 'Yapay zeka servis sağlayıcı bağlantı hatası. Yetkilendirme başarısız.';
    } else if (message.includes('quota') || message.includes('limit')) {
      message = 'Yapay zeka servis kotası doldu. Lütfen daha sonra tekrar deneyin.';
    }

    // Konsola detaylı teknik hata çıktısını basalım (Sistem çökmez, sadece loglanır)
    console.error(`[HATA] [${request.method}] ${request.url} =>`, exception);

    response.status(status).json({
      statusCode: status,
      timestamp: new Date().toISOString(),
      path: request.url,
      error: {
        message: message,
        type: exception instanceof Error ? exception.name : 'UnknownError'
      }
    });
  }
}
  1. Filtreyi Uygulamaya Küresel Olarak Bağlayın:

// apps/backend/src/main.ts (Güncellenmiş)
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import { GlobalExceptionFilter } from './common/filters/global-exception.filter';

async function bootstrap() {
  const app = await NestFactory.create(AppModule);
  app.enableCors();

  // Küresel hata filtresini aktif ediyoruz
  app.useGlobalFilters(new GlobalExceptionFilter());

  await app.listen(3000);
}
bootstrap();

2. ADIM: Frontend Klavye Navigasyonunun Nuqs ile URL Entegrasyonu

Kullanıcının klavye ok tuşlarıyla listede gezindiği anda seçili olan klasör veya not ID'sini, sayfa yenilense bile kaybolmayacak şekilde anlık olarak tarayıcının URL adresine (?folder=... veya ?note=...) yansıtıyoruz.

💻 Uygulama: apps/frontend Nuqs Durum Bağlantısı

// apps/frontend/src/components/TerminalNavigation.tsx (Güncellenmiş)
'use client';
import React, { useEffect, useState } from 'react';
import { useQueryState } from 'nuqs';
import { IFolder, INote } from '@terminal-app/types';

interface TerminalNavigationProps {
  folders: IFolder[];
  notes: INote[];
}

export function TerminalNavigation({ folders, notes }: TerminalNavigationProps) {
  // Nuqs ile URL parametrelerini bağlıyoruz
  const [urlFolderId, setUrlFolderId] = useQueryState('folder');
  const [urlNoteId, setUrlNoteId] = useQueryState('note');

  // Navigasyon listesini birleştir
  const items = [
    ...folders.map(f => ({ id: f.id, type: 'FOLDER' as const, name: f.name })),
    ...notes.map(n => ({ id: n.id, type: 'NOTE' as const, name: n.title }))
  ];

  // İlk yüklemede eğer URL'de halihazırda seçili bir öge varsa imleci ona getir
  const initialIndex = items.findIndex(item => item.id === urlFolderId || item.id === urlNoteId);
  const [selectedIndex, setSelectedIndex] = useState<number>(initialIndex !== -1 ? initialIndex : 0);

  // İmlec her değiştiğinde URL parametrelerini otomatik ve anlık olarak güncelle
  useEffect(() => {
    const currentItem = items[selectedIndex];
    if (!currentItem) return;

    if (currentItem.type === 'FOLDER') {
      setUrlFolderId(currentItem.id);
      setUrlNoteId(null); // Klasör seçildiğinde aktif not temizlenir
    } else {
      setUrlNoteId(currentItem.id);
      // Notun bağlı olduğu klasör üstte kalabilir veya tercihe göre yönetilir
    }
  }, [selectedIndex, setUrlFolderId, setUrlNoteId]);

  useEffect(() => {
    const handleKeyDown = (event: KeyboardEvent) => {
      if (items.length === 0) return;

      switch (event.key) {
        case 'ArrowDown':
          event.preventDefault();
          setSelectedIndex(prev => (prev + 1) % items.length);
          break;
          
        case 'ArrowUp':
          event.preventDefault();
          setSelectedIndex(prev => (prev - 1 + items.length) % items.length);
          break;
      }
    };

    window.addEventListener('keydown', handleKeyDown);
    return () => window.removeEventListener('keydown', handleKeyDown);
  }, [items.length]);

  return (
    <div className="w-full bg-black p-4 rounded border border-emerald-500/30 font-mono text-emerald-400">
      <div className="text-xs text-emerald-600 mb-2">// Tuşlarla gezdikçe URL senkronize olur</div>
      <div className="flex flex-col space-y-1">
        {items.map((item, index) => {
          const isSelected = index === selectedIndex;
          return (
            <div
              key={item.id}
              className={`p-1.5 rounded cursor-pointer transition-all flex items-center space-x-2 ${
                isSelected 
                  ? 'bg-emerald-950 border-l-4 border-emerald-400 text-white pl-3' 
                  : 'hover:bg-slate-950 text-emerald-500'
              }`}
              onClick={() => setSelectedIndex(index)}
            >
              <span>{item.type === 'FOLDER' ? '📁' : '📄'}</span>
              <span className="text-sm">{item.name}</span>
              {isSelected && <span className="text-xs text-emerald-400 animate-pulse">▇</span>}
            </div>
          );
        })}
      </div>
    </div>
  );
}

3. ADIM: Turborepo Pipeline ve Bağımlılık Hattı Tasarımı

Paylaşılan @terminal-app/types paketinin kalitesini, derleme (build) ve kod kontrol (lint) süreçlerini garantiye almak için ana hattı (turbo.json) güncelliyoruz. Frontend ve backend derlenmeden önce, bağımlı oldukları tipler paketinin derlenmesi veya kontrol edilmesi zorunlu kılınacaktır.

💻 Uygulama: turbo.json Pipeline Konfigürasyonu

Monorepo kök dizinindeki (root) turbo.json dosyasını şu şekilde güncelleyin:

{
  "$schema": "https://turbo.build",
  "tasks": {
    "build": {
      "dependsOn": ["^build"],
      "inputs": ["src/**/*.ts", "src/**/*.tsx", "package.json"],
      "outputs": ["dist/**", ".next/**", "!.next/cache/**"]
    },
    "lint": {
      "dependsOn": ["^lint"],
      "inputs": ["src/**/*.ts", "src/**/*.tsx", "biome.json"]
    },
    "dev": {
      "cache": false,
      "persistent": true
    }
  }
}

Bu Yapılandırmanın Önemi:


==============================================================

İstediğiniz üç ileri düzey üretim (production) ve optimizasyon adımını (Docker Ortamı, Şema Doğrulamalı Yapay Zeka Çıktısı ve Komut Satırı Geçmişi Hafızası) monorepo yapısına tam uyumlu olacak şekilde tasarlayıp kodladım.


1. ADIM: Otomatik Şema Doğrulamalı (Structured Outputs) Yapay Zeka Çıktısı

Yapay zekanın serbest metin üretmesi yerine, belirleyeceğimiz kesin bir JSON şemasına uymasını zorunlu kılıyoruz. Güncel Google Gen AI SDK özellikleri sayesinde, gelen yanıtın tam olarak bizim tip modelimize uygun olmasını garanti altına alıyoruz.

💻 Uygulama: apps/backend Yapay Zeka Katmanı

  1. Tip Şemasını Tanımlayın:
    Öncelikle yapay zekanın dönmesini istediğimiz veri yapısını sınıf düzeyinde tanımlıyoruz:

// apps/backend/src/ai/dto/ai-analysis-output.dto.ts
export class AiAnalysisOutput {
  summary: string;     // Maksimum 2 cümlelik terminal özeti
  tags: string[];      // İçerikten çıkarılan 2-3 adet teknik etiket (Örn: "database", "nextjs")
  securityRisk: boolean; // Not içeriğinde hassas veri (şifre, api key vb.) riski var mı?
}
  1. AI Servisini Güncelleyin:
    gemini-2.5-flash modeline responseSchema parametresini geçerek çıktıyı yapılandırıyoruz:

// apps/backend/src/ai/ai.service.ts (Güncellenmiş)
import { Injectable, InternalServerErrorException } from '@nestjs/common';
import { GoogleGenAI, Type } from '@google/genai';
import { AiAnalysisOutput } from './dto/ai-analysis-output.dto';

@Injectable()
export class AiService {
  private ai: GoogleGenAI;

  constructor() {
    const apiKey = process.env.GEMINI_API_KEY;
    if (!apiKey) throw new Error('GEMINI_API_KEY bulunamadı.');
    this.ai = new GoogleGenAI({ apiKey });
  }

  async analyzeNoteContent(title: string, content: string): Promise<AiAnalysisOutput> {
    try {
      const prompt = `Başlık: ${title}\nİçerik: ${content}\n\nBu notu analiz et ve şemaya uygun bir JSON objesi dön.`;

      const response = await this.ai.models.generateContent({
        model: 'gemini-2.5-flash',
        contents: prompt,
        config: {
          // JSON çıktısını kesin olarak zorunlu kılıyoruz
          responseMimeType: 'application/json',
          responseSchema: {
            type: Type.OBJECT,
            properties: {
              summary: { type: Type.STRING, description: 'Maksimum 2 cümlelik teknik özet.' },
              tags: { 
                type: Type.ARRAY, 
                items: { type: Type.STRING }, 
                description: 'İçerikle ilgili en fazla 3 adet kısa etiket.' 
              },
              securityRisk: { type: Type.BOOLEAN, description: 'İçerikte şifre veya API anahtarı sızıntısı riski varsa true dön.' }
            },
            required: ['summary', 'tags', 'securityRisk'],
          }
        }
      });

      if (!response.text) throw new Error('Yapay zekadan boş yanıt döndü.');
      
      // Çıktının şemaya uygun bir JSON olduğu garanti altındadır
      return JSON.parse(response.text) as AiAnalysisOutput;
    } catch (error) {
      throw new InternalServerErrorException('Yapay zeka şema doğrulaması başarısız: ' + error.message);
    }
  }
}

2. ADIM: Terminal Arayüzüne Komput Satırı Geçmişi (Command History) Altyapısı

Kullanıcının terminal arayüzünde yazdığı eski komutları hafızada tutan, klavyeden Yukarı Ok (ArrowUp) tuşuna basıldığında geçmişteki komutları geriye doğru getiren, Aşağı Ok (ArrowDown) ile güncel yazılmakta olan komuta döndüren bir geçmiş (history/buffer) altyapısı kuruyoruz.

💻 Uygulama: apps/frontend Terminal Komut Hafızası

// apps/frontend/src/components/TerminalCommandLine.tsx
'use client';
import React, { useState, useEffect } from 'react';

interface TerminalCommandLineProps {
  onExecuteCommand: (cmd: string) => void;
}

export function TerminalCommandLine({ onExecuteCommand }: TerminalCommandLineProps) {
  const [input, setInput] = useState('');
  const [history, setHistory] = useState<string[]>([]); // Geçmiş komutların listesi
  const [historyIndex, setHistoryIndex] = useState<number>(-1); // Gezinme indeks durumu
  const [tempInput, setTempInput] = useState(''); // Ok tuşuna basılmadan önceki güncel girdi hafızası

  const handleKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
    if (e.key === 'Enter') {
      if (!input.trim()) return;
      
      // Yeni komutu geçmişe ekle ve indeksi sıfırla
      const updatedHistory = [...history, input];
      setHistory(updatedHistory);
      setHistoryIndex(-1);
      
      // Komutu çalıştır ve girdiyi temizle
      onExecuteCommand(input);
      setInput('');
      setTempInput('');
    } 
    else if (e.key === 'ArrowUp') {
      e.preventDefault();
      if (history.length === 0) return;

      // Eğer ilk defa yukarı basılıyorsa, kullanıcının o an yazdığı yarım metni sakla
      if (historyIndex === -1) {
        setTempInput(input);
      }

      const nextIndex = historyIndex === -1 ? history.length - 1 : Math.max(0, historyIndex - 1);
      setHistoryIndex(nextIndex);
      setInput(history[nextIndex]);
    } 
    else if (e.key === 'ArrowDown') {
      e.preventDefault();
      if (historyIndex === -1) return;

      if (historyIndex === history.length - 1) {
        // En son komuttan da aşağı iniliyorsa kullanıcının kendi yazdığı yarım metne geri dön
        setHistoryIndex(-1);
        setInput(tempInput);
      } else {
        const nextIndex = historyIndex + 1;
        setHistoryIndex(nextIndex);
        setInput(history[nextIndex]);
      }
    }
  };

  return (
    <div className="flex items-center space-x-2 font-mono text-emerald-400 p-2 bg-slate-950 rounded border border-slate-800">
      <span className="text-emerald-500 font-bold">guest@modern-stack:~$</span>
      <input
        type="text"
        value={input}
        onChange={(e) => setInput(e.target.value)}
        onKeyDown={handleKeyDown}
        placeholder="Komut yazın... (Örn: help, ls, clear)"
        className="flex-1 bg-transparent border-none outline-none text-white font-mono text-sm focus:ring-0"
        autoFocus
      />
    </div>
  );
}

3. ADIM: Canlıya Çıkış Senaryosu İçin Docker ve Docker Compose Entegrasyonu

Monorepo projelerinde Docker imajı üretirken en kritik nokta, node_modules ve paylaşılan yerel paket bağımlılıklarının (packages/types) doğru bir şekilde kopyalanmasıdır. Projenin ana kök dizininde çok katmanlı (Multi-stage Build) bir süreç kurguluyoruz.

💻 Uygulama: Canlı Ortam Yapılandırması

  1. Backend İçin apps/backend/Dockerfile oluşturun:

# --- Derleme Aşaması ---
FROM node:20-alpine AS builder
WORKDIR /app

# Monorepo bağımlılık yönetim dosyalarını kopyala
COPY package*.json ./
COPY turbo.json ./
COPY apps/backend/package*.json ./apps/backend/
COPY packages/types/package*.json ./packages/types/

# Bağımlılıkları yükle
RUN npm list || npm ci

# Kaynak kodları kopyala
COPY apps/backend ./apps/backend
COPY packages/types ./packages/types

# Önce ortak tipleri ve ardından backend projesini derle
RUN npx turbo run build --filter=backend...

# --- Çalıştırma Aşaması ---
FROM node:20-alpine AS runner
WORKDIR /app
ENV NODE_ENV=production

COPY --from=builder /app/apps/backend/dist ./apps/backend/dist
COPY --from=builder /app/apps/backend/node_modules ./apps/backend/node_modules
COPY --from=builder /app/node_modules ./node_modules
COPY --from=builder /app/apps/backend/package.json ./apps/backend/package.json

EXPOSE 3000
CMD ["node", "apps/backend/dist/main.js"]
  1. Frontend İçin apps/frontend/Dockerfile oluşturun:

FROM node:20-alpine AS builder
WORKDIR /app
COPY package*.json turbo.json ./
COPY apps/frontend/package*.json ./apps/frontend/
COPY packages/types/package*.json ./packages/types/

RUN npm ci
COPY apps/frontend ./apps/frontend
COPY packages/types ./packages/types

RUN npx turbo run build --filter=frontend...

FROM node:20-alpine AS runner
WORKDIR /app
ENV NODE_ENV=production

COPY --from=builder /app/apps/frontend/.next ./apps/frontend/.next
COPY --from=builder /app/apps/frontend/public ./apps/frontend/public
COPY --from=builder /app/apps/frontend/node_modules ./apps/frontend/node_modules
COPY --from=builder /app/node_modules ./node_modules
COPY --from=builder /app/apps/frontend/package.json ./apps/frontend/package.json

EXPOSE 3000
CMD ["node", "apps/frontend/node_modules/.bin/next", "start", "apps/frontend"]
  1. Kök Dizinde (Root) Orkestrasyon İçin docker-compose.yml oluşturun:

version: '3.8'

services:
  backend-api:
    build:
      context: .
      dockerfile: apps/backend/Dockerfile
    ports:
      - "3000:3000"
    environment:
      - NODE_ENV=production
      - GEMINI_API_KEY=${GEMINI_API_KEY}
    restart: always

  frontend-web:
    build:
      context: .
      dockerfile: apps/frontend/Dockerfile
    ports:
      - "3001:3000"
    environment:
      - NODE_ENV=production
      - NEXT_PUBLIC_API_URL=http://localhost:3000
    depends_on:
      - backend-api
    restart: always



Sistem mimarimizi tamamıyla üretime hazır, güvenli ve etkileşimli bir terminal platformuna dönüştürecek son ileri düzey özellikleri de monorepo yapısına tam uyumlu şekilde entegre ettim.


1. ADIM: Frontend Üzerinde Gerçek Dosya Sistemi Simülatörü (ls, cd, cat)

Kullanıcının terminal komut satırına yazdığı standart Linux komutlarını (ls, cd, cat, clear, help) arka plandaki hiyerarşik klasör ve not verileriyle eşleştirerek işleyen bir sanal kabuk (Shell Engine) geliştiriyoruz.

💻 Uygulama: apps/frontend Komut İşleyici Motoru

// apps/frontend/src/hooks/useTerminalShell.ts
'use client';
import { useState } from 'react';
import { useQueryState } from 'nuqs';
import { IFolder, INote } from '@terminal-app/types';

interface ShellOutput {
  command: string;
  result: string[];
}

export function useTerminalShell(folders: IFolder[], notes: INote[]) {
  const [outputHistory, setOutputHistory] = useState<ShellOutput[]>([]);
  const [currentFolderId, setCurrentFolderId] = useQueryState('folder');
  const [activeNoteId, setActiveNoteId] = useQueryState('note');

  // O an bulunulan klasör nesnesini bul
  const currentFolder = folders.find(f => f.id === currentFolderId);

  const executeCommand = (fullCommand: string) => {
    const trimmed = fullCommand.trim();
    if (!trimmed) return;

    const [cmd, ...args] = trimmed.split(' ');
    const argument = args.join(' ');
    let result: string[] = [];

    switch (cmd.toLowerCase()) {
      case 'clear':
        setOutputHistory([]);
        return;

      case 'help':
        result = [
          'Kullanılabilir Komutlar:',
          '  ls          - Aktif dizindeki klasör ve notları listeler.',
          '  cd [isim]   - Belirtilen klasörün içine girer (cd .. üst dizine çıkar).',
          '  cat [başlık]- Belirtilen notun içeriğini ve yapay zeka analizini okur.',
          '  clear       - Terminal ekranını temizler.'
        ];
        break;

      case 'ls':
        // Mevcut klasörün altındaki çocukları veya kök dizindekileri listele
        const visibleFolders = folders.filter(f => f.parentId === (currentFolderId || null));
        const visibleNotes = notes.filter(n => n.folderId === currentFolderId);

        if (visibleFolders.length === 0 && visibleNotes.length === 0) {
          result = ['[Dizin Boş]'];
        } else {
          result = [
            ...visibleFolders.map(f => `📁 ${f.name}/`),
            ...visibleNotes.map(n => `📄 ${n.title}`)
          ];
        }
        break;

      case 'cd':
        if (!argument || argument === '..') {
          // Üst dizine çık veya köke dön
          if (currentFolder && currentFolder.parentId) {
            setCurrentFolderId(currentFolder.parentId);
            result = [`Dizin değiştirildi: ${folders.find(f => f.id === currentFolder.parentId)?.name || 'Kök'}`];
          } else {
            setCurrentFolderId(null);
            result = ['Kök dizine dönüldü.'];
          }
          setActiveNoteId(null);
        } else {
          // İsme göre alt klasör ara
          const target = folders.find(f => f.parentId === (currentFolderId || null) && f.name.toLowerCase() === argument.toLowerCase());
          if (target) {
            setCurrentFolderId(target.id);
            setActiveNoteId(null);
            result = [`Dizin değiştirildi: ${target.name}/`];
          } else {
            result = [`Hata: '${argument}' adında bir klasör bulunamadı.`];
          }
        }
        break;

      case 'cat':
        if (!argument) {
          result = ['Hata: Okumak istediğiniz notun başlığını belirtmelisiniz. Örn: cat Fikirler'];
        } else {
          const targetNote = notes.filter(n => n.folderId === currentFolderId).find(n => n.title.toLowerCase() === argument.toLowerCase());
          if (targetNote) {
            setActiveNoteId(targetNote.id);
            result = [
              `--- NOT BAŞLIĞI: ${targetNote.title} ---`,
              targetNote.content,
              targetNote.aiSummary ? `\n[Claude AI Analizi]: ${targetNote.aiSummary}` : '\n[Yapılandırılmış AI verisi mevcut değil]'
            ];
          } else {
            result = [`Hata: '${argument}' adında bir not bu dizinde bulunamadı.`];
          }
        }
        break;

      default:
        result = [`Hata: '${cmd}' bilinmeyen bir komut. Yardım için 'help' yazın.`];
    }

    setOutputHistory(prev => [...prev, { command: fullCommand, result }]);
  };

  return { outputHistory, executeCommand, currentFolderName: currentFolder?.name || 'root' };
}

2. ADIM: Veri Tabanı Güvenlik Katmanı (Kriptografik Kayıt)

Yapay zekanın analiz ettiği summary gibi hassas veya gizli olabilecek çıktı verilerini veri tabanına (SQLite/PostgreSQL) doğrudan çıplak metin olarak yazmak yerine, AES-256-GCM algoritması ile çift taraflı şifreleyerek kaydeden bir Prisma/NestJS ara yazılım katmanı oluşturuyoruz.

💻 Uygulama: apps/backend Kripto Servisi ve Veri Tabanı Koruması

// apps/backend/src/common/crypto/crypto.service.ts
import { Injectable } from '@nestjs/common';
import * as crypto from 'crypto';

@Injectable()
export class CryptoService {
  private readonly algorithm = 'aes-256-gcm';
  private readonly secretKey: Buffer;

  constructor() {
    // 32 byte'lık güvenli anahtarı çevre değişkenlerinden al veya güvenli bir şekilde türet
    const keyInput = process.env.ENCRYPTION_KEY || 'default-super-secret-key-32-bytes!!';
    this.secretKey = crypto.scryptSync(keyInput, 'salt', 32);
  }

  encrypt(text: string): string {
    if (!text) return '';
    const iv = crypto.randomBytes(16);
    const cipher = crypto.createCipheriv(this.algorithm, this.secretKey, iv);
    
    let encrypted = cipher.update(text, 'utf8', 'hex');
    encrypted += cipher.final('hex');
    
    const authTag = cipher.getAuthTag().toString('hex');
    
    // IV, AuthTag ve Şifreli metni sızıntı yapmayacak şekilde birleştirip saklıyoruz
    return `${iv.toString('hex')}:${authTag}:${encrypted}`;
  }

  decrypt(encryptedText: string): string {
    if (!encryptedText || !encryptedText.includes(':')) return '';
    const [ivHex, authTagHex, encrypted] = encryptedText.split(':');
    
    const iv = Buffer.from(ivHex, 'hex');
    const authTag = Buffer.from(authTagHex, 'hex');
    const decipher = crypto.createDecipheriv(this.algorithm, this.secretKey, iv);
    
    decipher.setAuthTag(authTag);
    
    let decrypted = decipher.update(encrypted, 'hex', 'utf8');
    decrypted += decipher.final('utf8');
    
    return decrypted;
  }
}

Bu servisi NotesService içerisinde veri yazarken this.cryptoService.encrypt(aiOutput) ve okurken this.cryptoService.decrypt(note.aiSummary) şeklinde çağırarak tam veri gizliliği sağlıyoruz.


3. ADIM: Docker Ortamında Sağlık Kontrolü (Healthcheck) Mekanizması

Konteynerlerin sadece çalışır durumda görünmesini değil, ağ isteklerine gerçekten cevap verip vermediklerini anlık olarak doğrulamak için hem Docker katmanında hem de NestJS tarafında bir Healthcheck mekanizması kuruyoruz.

💻 Uygulama: Docker ve Kod Seviyesinde Canlılık Kontrolü

  1. NestJS Sağlık Kontrolü Uç Noktası:

// apps/backend/src/health/health.controller.ts
import { Controller, Get, HttpStatus, Res } from '@nestjs/common';
import { Response } from 'express';
import { PrismaService } from '../prisma/prisma.service';

@Controller('health')
export class HealthController {
  constructor(private prisma: PrismaService) {}

  @Get()
  async checkHealth(@Res() res: Response) {
    try {
      // Veri tabanı bağlantısı aktif mi testi
      await this.prisma.$queryRaw`SELECT 1`;
      return res.status(HttpStatus.OK).json({ status: 'ok', database: 'healthy' });
    } catch (error) {
      return res.status(HttpStatus.SERVICE_UNAVAILABLE).json({ status: 'down', reason: error.message });
    }
  }
}
  1. Orkestrasyon Dosyasının (docker-compose.yml) Healthcheck İle Güncellenmesi:

version: '3.8'

services:
  backend-api:
    build:
      context: .
      dockerfile: apps/backend/Dockerfile
    ports:
      - "3000:3000"
    environment:
      - NODE_ENV=production
      - GEMINI_API_KEY=${GEMINI_API_KEY}
      - ENCRYPTION_KEY=${ENCRYPTION_KEY}
    # Her 30 saniyede bir api'nin gerçekten ayakta olduğunu wget komutu ile sorgula
    healthcheck:
      test: ["CMD-SHELL", "wget --no-verbose --tries=1 --spider http://localhost:3000/health || exit 1"]
      interval: 30s
      timeout: 10s
      retries: 3
      start_period: 10s
    restart: always

  frontend-web:
    build:
      context: .
      dockerfile: apps/frontend/Dockerfile
    ports:
      - "3001:3000"
    environment:
      - NODE_ENV=production
      - NEXT_PUBLIC_API_URL=http://localhost:3000
    # API konteyneri tamamen "healthy" (sağlıklı) raporu vermeden web ayağa kalkmaz
    depends_on:
      backend-api:
        condition: service_healthy
    restart: always



1. ADIM: Terminal İçin Otomatik Tamamlama (Tab Completion) Özelliği

Kullanıcının terminale bir komutun veya klasör/not adının ilk birkaç harfini yazıp Tab tuşuna basması durumunda, mevcut dizindeki seçenekleri tarayarak girdiyi otomatik tamamlayan (Auto-complete) bir yapı kuruyoruz. Eğer birden fazla eşleşme varsa, kullanıcıya olası alternatifleri listeler.

💻 Uygulama: apps/frontend Tab Tamamlama Altyapısı

Daha önce yazdığımız TerminalCommandLine bileşenini Tab tuşu olaylarını (KeyDown) yakalayacak ve mevcut aktif dizin verilerine göre tamamlama yapacak şekilde güncelliyoruz:

// apps/frontend/src/components/TerminalCommandLine.tsx (Güncellenmiş)
'use client';
import React, { useState } from 'react';
import { IFolder, INote } from '@terminal-app/types';

interface TerminalCommandLineProps {
  onExecuteCommand: (cmd: string) => void;
  currentFolders: IFolder[]; // Aktif dizindeki alt klasörler
  currentNotes: INote[];     // Aktif dizindeki notlar
}

export function TerminalCommandLine({ onExecuteCommand, currentFolders, currentNotes }: TerminalCommandLineProps) {
  const [input, setInput] = useState('');
  const [suggestions, setSuggestions] = useState<string[]>([]);

  const handleKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
    // 1. Tab Tuşu Yakalama (Otomatik Tamamlama)
    if (e.key === 'Tab') {
      e.preventDefault(); // Tarayıcının varsayılan odak değiştirme (focus) davranışını engelle
      
      const trimmed = input.trim();
      if (!trimmed) return;

      const parts = trimmed.split(' ');
      const cmd = parts[0].toLowerCase();
      const searchTarget = parts.slice(1).join(' ').toLowerCase();

      // Sadece argüman alan komutlar (cd ve cat) için tamamlama yap
      if (cmd === 'cd' || cmd === 'cat') {
        // Arama havuzunu komuta göre belirle
        const pool = cmd === 'cd' 
          ? currentFolders.map(f => f.name) 
          : currentNotes.map(n => n.title);

        // Kullanıcının yazdığı harflerle başlayan varlıkları filtrele
        const matches = pool.filter(item => item.toLowerCase().startsWith(searchTarget));

        if (matches.length === 1) {
          // Tek bir eşleşme varsa girdiyi doğrudan tamamla
          setInput(`${cmd} ${matches[0]}`);
          setSuggestions([]);
        } else if (matches.length > 1) {
          // Birden fazla eşleşme varsa alternatifleri ekranda gösterilmek üzere sakla
          setSuggestions(matches);
        }
      }
      return;
    }

    // 2. Enter Tuşu Yakalama (Komut Çalıştırma)
    if (e.key === 'Enter') {
      if (!input.trim()) return;
      onExecuteCommand(input);
      setInput('');
      setSuggestions([]);
    }
  };

  return (
    <div className="flex flex-col space-y-2 font-mono text-emerald-400 p-2 bg-slate-950 rounded border border-slate-800">
      <div className="flex items-center space-x-2">
        <span className="text-emerald-500 font-bold">guest@modern-stack:~$</span>
        <input
          type="text"
          value={input}
          onChange={(e) => {
            setInput(e.target.value);
            if (suggestions.length > 0) setSuggestions([]); // Kullanıcı yazmaya devam ederse önerileri temizle
          }}
          onKeyDown={handleKeyDown}
          placeholder="Komut yazın... (Eşleşmeleri tamamlamak için Tab tuşuna basın)"
          className="flex-1 bg-transparent border-none outline-none text-white font-mono text-sm focus:ring-0"
          autoFocus
        />
      </div>
      
      {/* Birden fazla eşleşme alternatifi varsa terminal stili listele */}
      {suggestions.length > 0 && (
        <div className="text-xs text-slate-500 pl-4 flex flex-wrap gap-4">
          {suggestions.map((match, idx) => (
            <span key={idx}>• {match}</span>
          ))}
        </div>
      )}
    </div>
  );
}

2. ADIM: Kriptografik Veri Göçü Test Scripti (Database Migration Test Script)

Uygulamanın eski versiyonunda şifrelenmemiş (çıplak metin) olarak duran yapay zeka özetlerini, yeni geliştirdiğimiz AES-256-GCM Güvenlik Katmanı standartlarına toplu olarak taşımak (data migration) ve bu sürecin doğruluğunu test etmek için bağımsız bir entegrasyon scripti yazıyoruz.

💻 Uygulama: apps/backend/prisma/migration-test.ts

Bu betik, veri tabanındaki tüm düz metinleri şifreler, doğruluğunu test etmek için anında geri çözer (decrypt) ve herhangi bir veri kaybı veya şifreleme hatası yoksa işlemi tek bir transaction halinde veri tabanına işler (commit).

// apps/backend/prisma/migration-test.ts
import { PrismaClient } from '@prisma/client';
import { CryptoService } from '../src/common/crypto/crypto.service';

const prisma = new PrismaClient();
const cryptoService = new CryptoService(); // AES-256-GCM motorumuz

async function runMigrationTest() {
  console.log('🔄 Kriptografik Veri Göçü (Migration) ve Test Süreci Başlatıldı...');

  // 1. Veri tabanındaki tüm notları çek
  const allNotes = await prisma.note.findMany();
  console.log(`📋 Toplam ${allNotes.length} adet not inceleniyor.`);

  let successfulEncryptions = 0;
  const updatePayloads: Array<{ id: string; encryptedSummary: string }> = [];

  for (const note of allNotes) {
    // Notun zaten şifrelenip şifrelenmediğini formatından anla (Format -> IV:AuthTag:Cipher)
    const isAlreadyEncrypted = note.content && note.content.split(':').length === 3;

    if (isAlreadyEncrypted) {
      console.log(`⏩ Not ID: ${note.id} zaten şifreli formatta. Geçiliyor.`);
      continue;
    }

    try {
      // Örnek senaryoda 'content' hücresini ya da 'aiSummary' hücresini koruma altına alıyoruz
      const plainText = note.content || '';
      
      // A. Şifreleme İşlemi (Encryption)
      const cipherText = cryptoService.encrypt(plainText);

      // B. Güvenlik ve Doğrulama Testi (Decryption Test)
      const decryptedText = cryptoService.decrypt(cipherText);

      if (decryptedText !== plainText) {
        throw new Error(`Kritik Veri Uyuşmazlığı! Orijinal metin çözülen metne eşit değil. ID: ${note.id}`);
      }

      // Test başarılı, güncelleme listesine ekle
      updatePayloads.push({
        id: note.id,
        encryptedSummary: cipherText
      });

      successfulEncryptions++;
    } catch (error) {
      console.error(`❌ Not ID: ${note.id} test aşamasında hata verdi:`, error.message);
      process.exit(1); // Test başarısız olursa süreci durdur, veri tabanına dokunma!
    }
  }

  // 2. İşlemleri Veri Tabanına Güvenli Bir Şekilde Toplu (Bulk) Olarak Yansıt
  if (updatePayloads.length > 0) {
    console.log(`\n✅ Tüm doğrulama testleri başarılı! ${successfulEncryptions} kayıt şifreleniyor...`);

    // Prisma $transaction kullanarak tüm güncellemeleri tek seferde yapar, biri bile hata alsa her şeyi geri alır (Rollback)
    await prisma.$transaction(
      updatePayloads.map(payload =>
        prisma.note.update({
          where: { id: payload.id },
          data: { content: payload.encryptedSummary } // Şifreli veriyi hücreye yaz
        })
      )
    );
    console.log('🎉 Veri tabanı kriptografik göçü başarıyla tamamlandı ve commit edildi.');
  } else {
    console.log('✨ Şifrelenmesi gereken yeni veri bulunamadı. Sistem güncel.');
  }
}

runMigrationTest()
  .catch(err => {
    console.error('🚨 Göç testi sırasında beklenmeyen sistem hatası:', err);
    process.exit(1);
  })
  .finally(async () => {
    await prisma.$disconnect();
  });

🚀 Scripti Çalıştırma Komutu:

Bu test ve göç sürecini monorepo yapısında çalıştırmak için apps/backend/package.json içerisine şu scripti ekleyebilirsiniz:

"scripts": {
  "db:migrate-crypto": "ts-node prisma/migration-test.ts"
}

Ardından terminalden npm run db:migrate-crypto komutunu vermeniz yeterlidir.


Monorepo mimarimizin kalitesini, kod standartlarını ve son olarak geliştirdiğimiz kriptografik veri göçü (migration) test sürecini her kod gönderiminde (git push) veya birleştirme isteğinde (pull request) otomatik olarak denetleyecek GitHub Actions CI/CD Pipeline iş akışını tasarladım.

Bu iş akışı, Turborepo'nun uzak önbelleğe alma (Remote Caching) yeteneğiyle uyumlu çalışarak sadece değişen paketleri test eder ve gereksiz kaynak tüketimini engeller.

💻 Uygulama: .github/workflows/ci-cd.yml Yapılandırması

Projenizin ana kök dizininde .github/workflows adında bir klasör oluşturun ve içine ci-cd.yml dosyasını ekleyin:

name: CI/CD Pipeline

on:
  push:
    branches: [ main, master ]
  pull_request:
    branches: [ main, master ]

jobs:
  audit-and-test:
    name: Code Quality and Database Migration Audit
    runs-on: ubuntu-latest

    steps:
      # 1. Adım: Repository'deki kaynak kodları sanal makineye çek
      - name: Checkout Code
        uses: actions/checkout@v4
        with:
          fetch-depth: 0 # Turborepo'nun değişen dosyaları (Git diff) doğru analiz edebilmesi için şarttır

      # 2. Adım: Node.js ortamını kur ve önbelleğe al (Caching)
      - name: Setup Node.js Environment
        uses: actions/setup-node@v4
        with:
          node-level: '20'
          cache: 'npm' # npm bağımlılıklarını önbelleğe alarak sonraki işleri hızlandırır

      # 3. Adım: Tüm Monorepo bağımlılıklarını güvenli ve temiz bir şekilde yükle
      - name: Install Dependencies
        run: npm ci

      # 4. Adım: Biome ile Kod Standartları Kontrolü (Linting & Formatting)
      # Rust tabanlı Biome, tüm repoyu milisaniyeler içinde denetler.
      - name: Run Biome Lint & Format Check
        run: npx biome check .

      # 5. Adım: Prisma İstemcisini (Prisma Client) Oluştur
      # Göç testi ve derleme süreçleri için şemadan TypeScript tiplerinin üretilmesi gerekir.
      - name: Generate Prisma Client
        run: npx turbo run generate --filter=backend

      # 6. Adım: Paylaşılan Paketleri ve Uygulamaları Derle (Build Validation)
      # turbo.json içindeki pipeline kurallarına göre @terminal-app/types otomatik olarak önce derlenir.
      - name: Validate Application Builds
        run: npx turbo run build

      # 7. Adım: Kriptografik Veri Göçü (Migration) Entegrasyon Testi
      # Geliştirdiğimiz AES-256-GCM test scriptini izole bir SQLite veritabanı üzerinde koşturur.
      # Şifreleme veya geri çözme süreçlerinde 1 bitlik bile veri kaybı olursa pipeline çöker.
      - name: Run Cryptographic Migration Test
        env:
          NODE_ENV: test
          DATABASE_URL: "file:./test.db"
          ENCRYPTION_KEY: "github-actions-secure-test-key-32-bytes" # Test ortamı için güvenli anahtar
          GEMINI_API_KEY: ${{ secrets.GEMINI_API_KEY }} # Hassas anahtarlar GitHub Secrets'tan okunur
        run: |
          cd apps/backend
          npx prisma migrate deploy
          npm run db:migrate-crypto

      # 8. Adım: Başarı Bildirimi (Opsiyonel)
      - name: Pipeline Status Success
        if: success()
        run: echo "🎉 Tüm kod standartları, derleme testleri ve kriptografik veri göçü doğrulamaları başarıyla tamamlandı!"

🔑 GitHub Secrets Yapılandırması (Önemli Not)

Yapay zeka servisinin uç noktalarını test ederken veya ilerleyen aşamalarda canlı ortama geçiş yaparken API anahtarınızın sızmaması için:

  1. GitHub üzerinde projenizin repository sayfasına gidin.

  2. Settings -> Secrets and variables -> Actions adımlarını takip edin.

  3. New repository secret butonuna tıklayarak GEMINI_API_KEY adıyla API anahtarınızı tanımlayın.



***********************************************************************************************************

Gönderdiğiniz müfredat dosyasını belirttiğiniz kurallar çerçevesinde (dakika/saniye bilgilerini temizleyerek, "ders" satırlarını atarak, anlamsız ifadeleri eleyerek) satır satır inceledim ve detaylı bir eğitim planı olarak tasarladım.

Dosya oldukça uzun ve teknik detaylar içerdiği için, yönergeniz doğrultusunda özet yapmadan, bol örnek, mimari açıklama ve uygulama pratikleriyle parça parça ilerleyeceğiz. İlk bölümde "Öğrenecekleriniz" kısmının derinlemesine analiziyle başlıyoruz.


BÖLÜM 1: "ÖĞRENECEKLERİNİZ" PARAGRAFININ DETAYLI AÇIKLAMASI

Bu bölüm, geleneksel WordPress geliştiriciliği ile modern JavaScript dünyasını birleştiren Headless (Kafasız) CMS mimarisinin temelini oluşturur. Amaç, WordPress'i sadece bir veritabanı ve yönetim paneli (Backend) olarak kullanmak; kullanıcıya gösterilen ön yüzü (Frontend) ise Next.js ile tamamen bağımsız, ultra hızlı ve güvenli bir şekilde inşa etmektir.

1. Front-end / React JS Geliştiricisi Olarak Değerinizi ve Bilginizi Artırmak

2. Next JS Öğrenmek

3. Next JS ile Statik, Sunucuda Derlenmiş, İçerik Odaklı Web Sitesi Oluşturmak (React & GraphQL ile)

4. WordPress Verilerini Sorgulamak İçin GraphQL ve GraphiQL Kullanımı

5. WordPress'i Backend Olarak Ayarlayıp, İçerikleri React & GraphQL ile Sayfalara Dönüştürmek

6. Vercel Kullanarak Next.js Statik Sitesini Canlıya Almak ve İçerik Değiştiğinde Otomatik Yeniden Derlemek

7. Next.js Uygulamalarını Tailwind CSS ile Stilize Etmek

8. ACF Pro ile Özel Gutenberg Blokları Oluşturmak

9. React Context & React Hooks Kullanımı ve Özel (Custom) Hook'lar Oluşturmak

10. Next.js ile API Uç Noktaları (Endpoints) Oluşturmak


BÖLÜM 2: İLK SAYFALARIN İNŞASI VE MİMARİ TASARIM

Müfredatın ilk teknik uygulama adımları, WordPress Gutenberg editöründeki blok verilerini temizlemek ve bunları Next.js bileşenlerine eşlemek üzerine kuruludur.

1. Ana Sayfa Oluşturma ve Gutenberg Blokları İçin Sorgu Yazma

WordPress'te sayfalar artık düz metin değil, bloklar halinde tutulur (Başlık bloğu, paragraf bloğu, resim bloğu vb.). Next.js tarafında bu blokları render edebilmek için öncelikle sayfanın ham blok verilerini GraphQL üzerinden talep etmeliyiz.

2. BlockRenderer Bileşeni Oluşturma ve Blok Verilerini Temizleme

WordPress'ten gelen blok verileri genellikle çok karmaşıktır ve içinde gereksiz HTML etiketleri barındırır. Next.js projemizde tüm gelen blokları sırayla okuyup, ilgili React bileşenine yönlendirecek merkezi bir kontrol merkezi kuracağız. Buna BlockRenderer denir.

3. Cover (Kapak) Bileşeni Oluşturma, Tailwind CSS ve Next.js Image Entegrasyonu

Emlak sitemizin en üstünde büyük, görkemli bir arka plan resmi ve üzerinde bir başlık olacak (Cover Bloğu).

4. WordPress Child (Çocuk) Tema Oluşturma ve Tema Dosyalarını Düzenleme

Ana WordPress temanız güncellendiğinde yaptığınız kod değişikliklerinin kaybolmaması için bir Child Theme oluşturacağız. Temamızın functions.php dosyasına girerek, WordPress'in GraphQL API'sine (WPGraphQL) tam uyumlu çalışmasını sağlayacak geliştirmeleri ekleyeceğiz.

5. Heading (Başlık) Bileşeni, Yardımcı Fonksiyonlar ve Tailwind CSS Genişletme

Gutenberg'den gelen <h1>, <h2>, <h3> gibi başlıkları dinamik olarak karşılayan bir React bileşeni yazacağız. WordPress'in sunduğu özel renk paletlerini Tailwind CSS'e entegre etmek için tailwind.config.js dosyasını genişleteceğiz (extend). Böylece WordPress panelinden seçilen bir kırmızı renk, Tailwind'deki karşılığı ile eşleşecek.

6. WordPress'te Dinamik Rotalar (Dynamic Routes) ve Alt Sayfalar Oluşturma

Kullanıcı sitemizde /satilik-villa veya /hakkimizda gibi bir linke tıkladığında, Next.js'in bunu dinamik olarak yakalaması gerekir.

7. getStaticProps ve GraphQL ile Sayfa Verilerini Çekme

Next.js'in sihirli fonksiyonlarından biri getStaticProps'tur. Sayfa yayına alınmadan önce (derleme anında) çalışan bu fonksiyon, WordPress mimarisine bağlanıp ilgili sayfanın içeriğini çeker ve sayfaya enjekte eder. Siteniz bu sayede veritabanı bağlantısı kopsa bile ayakta kalır ve jet hızında açılır.

8. Paragraph (Paragraf) Bileşeni ve İç Linklerin (Anchor Tags) Dönüştürülmesi

WordPress editöründe yazılan paragrafların içindeki linkler genellikle ://ekran.com... veya doğrudan WordPress'in kendi URL'ine yönlenir. Eğer kullanıcı bu linklere tıklarsa siteden çıkabilir veya sayfa yavaşça sıfırdan yüklenebilir.




Harika! O halde hiç hız kesmeden, mimari detayları ve kod yapılarını koruyarak müfredatın bir sonraki aşaması olan Ana Menü Tasarımı ve Gelişmiş Gutenberg Blokları ile devam ediyoruz.


BÖLÜM 3: ANA MENÜNÜN OLUŞTURULMASI (CREATING THE MAIN MENU)

Bir web sitesinin en kritik bileşenlerinden biri dinamik menü yönetimidir. Klasik WordPress temalarında menüler sürükle-bırak yöntemiyle yapılır. Headless mimaride ise bu menü verisini WordPress'ten çekip, Next.js tarafında performanslı ve erişilebilir bir bileşene dönüştürmemiz gerekir.

1. ACF ile Ana Menü Ayarlar Sayfası Oluşturma (Part 1 & 2)

WordPress'in varsayılan menü sistemi GraphQL ile her zaman esnek çalışmayabilir. Bu yüzden ACF Pro (Advanced Custom Fields) kullanarak WordPress paneline "Tema Ayarları" adında özel bir seçenekler sayfası (Options Page) ekleyeceğiz.

2. Next.js'te Menü Verisini Çekme ve Temizleme

WordPress panelinden girilen menü verisi, GraphQL sorgusuyla hiyerarşik bir JSON formatında Next.js'e gelir. Bu veriyi doğrudan kullanmadan önce, front-end tarafında işimizi kolaylaştıracak temiz bir dizi (array) yapısına dönüştüreceğiz.

query GetMainMenu {
  acfOptionsPage {
    mainMenuFields {
      menuItems {
        label
        destination {
          ... on Page {
            uri
          }
        }
        subMenuItems {
          label
          destination {
            ... on Page {
              uri
            }
          }
        }
      }
    }
  }
}

3. Next.js'te Menu Bileşeninin İnşası (Parçalar & Tamamlama)

Menü bileşeni, her sayfada görüneceği için ortak bir düzende (Layout) yer almalıdır. Masaüstünde yan yana dizilen, mobilde ise açılır bir hamburger menüye dönüşen responsive bir yapı tasarlayacağız.

4. Ana Menüde CTA (Harekete Geçirici Mesaj) Butonu Uygulaması

Emlak sitelerinde genellikle menünün en sağında dikkat çekici bir "İlan Ekle" veya "Bize Ulaşın" butonu bulunur. Menü verimize bir boolean (doğru/yanlış) alanı ekleyerek, ilgili menü elemanının normal bir link mi yoksa vurgulanmış bir CTA butonu mu olacağını belirleyeceğiz.

5. ButtonLink Bileşeni ve Tailwind CSS @apply Direktifi

Tekrar kullanılabilir bir buton bileşeni (ButtonLink) oluşturacağız. Tailwind CSS sınıflarını temiz tutmak ve global buton stilleri yaratmak için projenin global CSS dosyası içerisinde @apply yönergesini kullanacağız.

/* styles/globals.css */
.btn-primary {
  @apply bg-blue-600 text-white px-4 py-2 rounded-lg font-medium transition-colors hover:bg-blue-700;
}
.btn-secondary {
  @apply border border-gray-300 text-gray-700 px-4 py-2 rounded-lg font-medium hover:bg-gray-50;
}

React tarafında ise <Link className="btn-primary" href={destination}> {label} </Link> şeklinde kullanacağız.

6. Sayfa Bileşenlerinin Refaktör Edilmesi (Refactor Page Components)

Menü ve alt bilgi (footer) gibi her sayfada tekrar eden alanları her sayfa bileşeninin içine tek tek eklemek yerine, merkezi bir <Layout> bileşeni oluşturacağız. pages/_app.js veya dinamik sayfalarımızı bu Layout ile sarmallayarak kod tekrarını önleyeceğiz.


BÖLÜM 4: GELİŞMİŞ GUTENBERG BLOKLARI (ADVANCED GUTENBERG BLOCKS)

WordPress'in gücü, sayfa düzenini esnek şekilde değiştirebilmekten gelir. Bu bölümde ACF Pro kullanarak WordPress editörüne tamamen sıfırdan, kendimize özel bloklar enjekte etmeyi öğreneceğiz.

1. ACF Blocks ile Özel CTA Bloğu Oluşturma ve Önizleme Tasarımı

WordPress'e php kullanarak yeni bir Gutenberg bloğu kaydedeceğiz (acf_register_block_type). Bu blok, editör içinde zengin bir arka plan rengi, bir başlık ve bir buton içerecek. İçerik üreticisinin WordPress panelinde ne yaptığını görebilmesi için bir Block Preview (Blok Önizleme) şablonu hazırlayacağız.

2. Next.js'te CTA Bileşeninin Oluşturulması

WordPress panelinde doldurulan bu CTA bloğu verileri, BlockRenderer bileşenimize acf/cta-block ismiyle düşecek. Bu veriyi karşılayacak şık bir React bileşeni yazacağız.

// components/CtaBlock.js
import { ButtonLink } from "./ButtonLink";

export function CtaBlock({ heading, text, buttonLabel, buttonDestination }) {
  return (
    <div className="bg-slate-100 p-8 my-6 rounded-2xl text-center max-w-4xl mx-auto shadow-sm">
      <h3 className="text-2xl font-bold text-slate-800 mb-2">{heading}</h3>
      <p className="text-slate-600 mb-6">{text}</p>
      <ButtonLink variant="primary" href={buttonDestination}>
        {buttonLabel}
      </ButtonLink>
    </div>
  );
}

3. Gutenberg Kolon (Columns) Bloğu ve İç İçe Blok Yapısı (Inner Blocks)

Gutenberg'in en güçlü özelliklerinden biri, bir bloğun içine başka bloklar yerleştirebilmektir (Örn: Yan yana 3 kolon oluşturup, her kolonun içine farklı resimler ve paragraflar koymak).

4. Kolonlar İçinde Resim (Image) Bloklarının Yönetimi

Gutenberg içindeki standart resim blokları, Next.js'e ham HTML veya düz URL olarak gelir. Kolonların responsive (mobil uyumlu) yapısını bozmamak için, gelen resim verilerini yakalayıp Tailwind'in grid grid-cols-1 md:grid-cols-3 gibi esnek yapılarıyla saracağız ve Next.js <Image /> bileşenine fill özelliği vererek üst kapsayıcısının boyutunu otomatik almasını sağlayacağız.

5. Yeniden Kullanılabilir Bloklar (Reusable Blocks)

WordPress'te bir kere hazırlanan bir blok düzeni (örneğin standart bir form veya iletişim alanı) "Yeniden Kullanılabilir Blok" olarak kaydedilebilir. GraphQL sorgularımızda bu ortak blokların ID'lerini takip ederek, Next.js tarafında tek bir istekte tüm içeriği tek seferde çekebilecek bir eşleme algoritması kuracağız.




Harika! Şimdi projenin en kapsamlı ve fonksiyonel aşaması olan Property Pages (Emlak / Gayrimenkul İlan Sayfaları ve Gelişmiş Arama Motoru) bölümüne geçiyoruz.

Bu bölümde sadece statik sayfalar üretmeyeceğiz; Next.js üzerinde çalışan, filtreleme yeteneğine sahip dinamik bir arama motoru API'si inşa edeceğiz.


BÖLÜM 5: GAYRİMENKUL SAYFALARI VE GELİŞMİŞ ARAMA MOTORU (PROPERTY PAGES)

Bir emlak sitesinin kalbi, ilanların detay sayfaları ve kullanıcıların fiyat, oda sayısı gibi kriterlere göre ev arayabileceği filtreleme mekanizmasıdır.

1. Gayrimenkul İlanları İçin Özel Alanlar (Custom Fields) Oluşturma

WordPress'te ilanları yönetmek için varsayılan "Yazılar" (Posts) yerine Custom Post Type (Özel Yazı Türü) kullanarak "Properties" adında yeni bir ilan yönetim alanı açacağız.

2. Next.js'te Dinamik İlan Detay Sayfalarının İnşası

Her bir ilanın kendine özel bir URL'i olmalıdır (Örn: /properties/bogaz-manzarali-lu-ks-villa).

3. Öne Çıkan Görseli Kapak (Cover) Yapma ve Başlık Bloğu

İlan detay sayfasına girildiğinde en üstte evin devasa bir fotoğrafı (Featured Image) ve üzerinde ilan başlığı yer alacak.

4. Property Search (Gayrimenkul Arama) Özel Gutenberg Bloğu

İçerik yöneticisinin, ana sayfa dahil istediği herhangi bir sayfaya emlak arama motorunu yerleştirebilmesini istiyoruz. Bunun için WordPress'te Property Search adında içi boş, sadece bir belirteç (placeholder) görevi gören bir ACF Bloğu kaydedeceğiz. Next.js bu bloğu gördüğü an, oraya gelişmiş arama arayüzümüzü (formlar, filtreler ve sonuç listesi) monte edecek.

5. Next.js ile Arama Motoru API Rotası (API Route) Oluşturma

Kullanıcılar "Fiyatı 5 Milyon TL'den az olan 3+1 evleri listele" dediğinde, her seferinde tüm sayfayı yenilemek performansı düşürür. Bunun yerine arka planda çalışacak bir API yazacağız.

6. Arama Sonuçlarının Ekrana Basılması (Rendering Search Results)

API'den dönen ev listesini (başlık, fiyat, görsel, oda sayısı), Tailwind CSS ile tasarlanmış şık kartlar (Card Component) halinde grid düzeninde ekrana basacağız.

7. Sayfalama (Pagination) Arayüzü ve API Entegrasyonu

Eğer sitede 500 tane ilan varsa, hepsini aynı anda yüklemek siteyi kilitler. Bu yüzden her sayfada 9 ilan gösterecek şekilde bir sayfalama sistemi kuracağız.

8. Arama Filtreleri Arayüzü (Search Filters UI) ve Yerel Durum (Local State)

Kullanıcının seçim yapabileceği select kutuları ve girdiler tasarlayacağız.

// components/PropertySearch/Filters.js
import { useState } from "react";

export function Filters({ onSearch }) {
  const [bedrooms, setBedrooms] = useState("");
  const [maxPrice, setMaxPrice] = useState("");

  const handleApplyFilters = () => {
    // Üst bileşendeki arama fonksiyonunu tetikle
    onSearch({ bedrooms, maxPrice });
  };

  return (
    <div className="flex gap-4 p-4 bg-white shadow-md rounded-xl mb-6">
      <select 
        value={bedrooms} 
        onChange={(e) => setBedrooms(e.target.value)}
        className="border p-2 rounded"
      >
        <option value="">Oda Seçin</option>
        <option value="1">1 Oda</option>
        <option value="2">2 Oda</option>
        <option value="3">3+ Oda</option>
      </select>
      
      <input 
        type="number" 
        placeholder="Maksimum Fiyat" 
        value={maxPrice}
        onChange={(e) => setMaxPrice(e.target.value)}
        className="border p-2 rounded"
      />
      
      <button onClick={handleApplyFilters} className="bg-blue-600 text-white px-4 py-2 rounded">
        Filtrele
      </button>
    </div>
  );
}

9. Arama Esnasında URL Linklerinin Düzeltilmesi (Fixing URLs)

Kullanıcı filtreleme yaptığında, tarayıcıdaki URL adres çubuğunun da güncellenmesini istiyoruz (Örn: /properties/search?bedrooms=3&maxPrice=5000000).


BÖLÜM 6: EKSTRA ÖZELLİKLER VE DETAYLI BİLEŞENLER (EXTRA)

Bu bölüm sitenin kurumsallığını artıran, form yönetimi, galeri sistemleri ve SEO gibi ince işçilik gerektiren bileşenlerin eklendiği aşamadır.

1. Formspree Entegrasyonu ile İletişim Formu Bloğu

Emlak sayfalarında "Acenteyle İletişime Geçin" formu için backend kodlarıyla uğraşmamak adına popüler bir form yönetim servisi olan Formspree kullanacağız.

2. Gayrimenkul Özellikleri (Property Features) Bloğu

Evin kaç metrekare olduğu, cephesi, ısıtma durumu gibi teknik detayları listelemek için esnek bir blok oluşturacağız. WordPress'ten gelen bu teknik listeyi Next.js tarafında ikonlar eşliğinde temiz bir tablo veya grid düzeniyle sergileyeceğiz.

3. Görsel Galeri (Image Gallery) Bloğu

İlanların içine eklenecek çoklu fotoğraflar için Gutenberg'in yerleşik Galeri bloğunu yakalayacağız. Next.js tarafında fotoğrafların üzerine tıklandığında ekranı kaplayan, sağa-sola kaydırılabilen modern bir Lightbox (Lightbox Gallery) bileşeni yazacağız.

4. Kolon Bloklarına WordPress Üzerinden Renk Yönetimi Eklemek

WordPress editöründe bir kolona arka plan rengi veya yazı rengi seçildiğinde, WordPress bu sınıfları has-background veya has-dark-gray-color gibi isimlerle çıktı verir.

5. Onay İşareti Ögesi (Tick Item) Bloğu

Özellikle avantajların listelendiği alanlarda ("Depreme dayanıklı", "Krediye uygun") satırların başına yeşil onay işareti koyabilmek için mikro bir Gutenberg bloğu tasarlayıp React'ta render edeceğiz.




Harika! Şimdi kursun en kritik dönüm noktasına geliyoruz: Next.js 13.4+ Güncellemesi ve App Router (App Directory) Geçişi.

Bu bölüm, projenin eski mimariden (pages klasörü) modern, bileşen odaklı ve performans canavarı olan yeni mimariye (app klasörü) taşındığı, profesyonel refaktör süreçlerini içerir. Sonrasında ise projemizi canlıya alıp optimizasyonlarını tamamlayacağız.


BÖLÜM 7: NEXT.JS MODERN APP ROUTER GEÇİŞİ (UPGRADING TO APP DIRECTORY)

Next.js 13.4 ile birlikte gelen App Router mimarisi, web geliştirmede köklü bir değişiklik yaratmıştır. Artık her bileşen varsayılan olarak bir Server Component (Sunucu Bileşeni)'dir. Bu da tarayıcıya neredeyse hiç JavaScript göndermeyerek sitenin anında yüklenmesini sağlar.

1. Projeyi Hazırlama ve Bağımlılıkların Güncellenmesi

İlk olarak package.json dosyasındaki Next.js, React ve Tailwind CSS sürümlerini güncelleyeceğiz. Ardından projenin kök dizinine (root) yeni app klasörünü açarak geçiş hazırlıklarını tamamlayacağız.

2. App Directory (Klasör Yapısı) Uygulaması

Eski mimarideki pages/index.js ve pages/[...slug].js dosyalarımızı yeni düzene taşıyoruz.

3. Sunucu ve İstemci Bileşenlerinin (Server vs Client Components) Ayrımı

Yeni mimaride en çok dikkat etmemiz gereken kural budur. WordPress'ten veri çeken ve statik olarak basılan tüm bileşenleri Server Component olarak bırakacağız. Ancak arama motoru filtreleri, buton tıklamaları gibi kullanıcı etkileşimi (state, event listener) gerektiren yerlerin en üstüne "use client"; ifadesini ekleyerek bunları Client Component'e dönüştüreceğiz.

4. Tailwind CSS, Google Fonts ve FontAwesome Entegrasyonu

Yeni layout.js dosyasında, Next.js'in performans optimize edilmiş yerleşik font sistemini (next/font/google) kullanarak Google Yazı Tiplerini projeye dahil edeceğiz. Küresel CSS dosyamızı buraya import ederek Tailwind'i aktaracağız. FontAwesome ikonlarının sayfa yüklenirken büyümesini engellemek için CSS kurallarını sabitleyeceğiz.

5. Menüyü Layout Dosyasına Taşımak

Eski mimaride her sayfa geçişinde menü bileşeni sarsılabiliyor veya tekrar render edilebiliyordu. Yeni app/layout.js içerisine <MainMenu /> bileşenimizi yerleştiriyoruz. Böylece kullanıcı sayfalar arasında gezinirken menü asla baştan yüklenmez, sadece değişen sayfa içeriği (children) güncellenir.

6. generateMetadata Fonksiyonu ile Dinamik SEO Yönetimi

Eski mimaride kullandığımız next/head yapısı artık geride kaldı. App Router'da SEO verilerini (Başlık, Açıklama, Open Graph görselleri) yönetmek için dışa aktarılan (export) özel bir fonksiyon kullanırız.

// app/[...slug]/page.js
export async function generateMetadata({ params }) {
  const slug = params.slug.join('/');
  const pageData = await getPageData(slug); // WordPress'ten SEO verisini çek

  return {
    title: pageData.seo.title || "Emlak Sitesi",
    description: pageData.seo.description,
    openGraph: {
      images: [pageData.seo.opengraphImage?.sourceUrl],
    },
  };
}

7. Arama Motorunun (next/navigation) ve API Rotalarının Güncellenmesi

Yeni mimaride next/router modülü çalışmaz. Emlak arama motorumuz içindeki yönlendirmeleri ve URL güncellemelerini next/navigation içinden çekeceğimiz useRouter, usePathname ve useSearchParams hook'ları ile baştan yazacağız.

8. Next.js Önbellek (Cache) Mekanizması

Next.js App Router, arkada çok agresif bir önbellekleme sistemi çalıştırır. WordPress'ten veri çekerken kullandığımız fetch isteklerine özel parametreler geçerek verinin ne kadar süre yayında kalacağını belirleyeceğiz. (Örn: fetch('...', { next: { revalidate: 60 } }) → Veriyi 60 saniyede bir tazele).


BÖLÜM 8: CANLIYA ALMA VE ÜRETİM (DEPLOY TO PRODUCTION)

Projemiz yerel bilgisayarımızda kusursuz çalıştığına göre, artık tüm dünyanın erişebileceği bulut sunucularına taşınma vaktidir.

1. WordPress ve Next.js Uygulamasını Canlıya Alma

2. On-Demand Revalidation (İsteğe Bağlı Önbellek Yenileme) Mimarisi

Sitemiz statik ve önbelleğe alınmış olduğu için, WordPress panelinde bir evin fiyatı değiştirildiğinde bu değişiklik Next.js sitesinde anında görünmez. Sitenin tamamını dakikalarca baştan derlemek (rebuild) yerine akıllı bir tetikleyici kuracağız.


PROJENİN GENEL MİMARİ ÖZETİ (NASIL ÇALIŞIYOR?)

  1. Giriş: İçerik yöneticisi WordPress paneline girer, ACF ile tasarladığımız blokları kullanarak lüks bir villa ilanı ekler ve kaydeder.

  2. Tetiklenme: WordPress arkada çalışan Webhook mekanizması sayesinde Vercel'deki Next.js API rotamıza "Veri değişti!" sinyali gönderir.

  3. Derleme & Önbellek: Next.js, GraphQL üzerinden yeni ilanın verilerini ve blok düzenini saniyeler içinde çeker, BlockRenderer ile React bileşenlerine dönüştürür ve statik HTML olarak sunucuya yazar.

  4. Kullanıcı Deneyimi: Müşteri siteye girdiğinde, veritabanı sorgularıyla vakit kaybetmeyen, Tailwind ile optimize edilmiş, Next.js Image mimarisiyle resimleri sıkıştırılmış, saniyenin onda biri hızında açılan bir web sitesiyle karşılaşır. Arama motorunu kullandığında ise Next.js API rotaları sayesinde sayfayı yenilemeden milisaniyeler içinde ilanları filtreleyebilir.

**********************************************************