Takip et

React ve CSS Grid ile Sonsuz Kaydırmalı Bir Resim Galerisi Nasıl Oluşturulur?

React ve CSS Grid ile Sonsuz Kaydırmalı Bir Resim Galerisi Nasıl Oluşturulur? Web uygulamalarında kullanıcı deneyimini zenginleştiren ve veri

React ve CSS Grid ile Sonsuz Kaydırmalı Bir Resim Galerisi Nasıl Oluşturulur?

Web uygulamalarında kullanıcı deneyimini zenginleştiren ve veri yoğun içerikleri daha yönetilebilir hale getiren modern tekniklerden biri, “sonsuz kaydırma” (infinite scroll) özelliğidir. Özellikle resim galerileri, ürün listeleri veya sosyal medya akışları gibi sürekli yeni içerik gösterilmesi gereken durumlarda sonsuz kaydırma, sayfalama (pagination) yöntemine kıyasla daha akıcı ve kesintisiz bir deneyim sunar. Kullanıcıların bir sonraki sayfa düğmesine tıklamak yerine, sadece aşağı kaydırarak yeni içeriğe ulaşması, etkileşimi artırır ve yükleme sürelerini algısal olarak azaltır.

Bu makalede, modern bir JavaScript kütüphanesi olan React ve güçlü bir CSS düzenleme aracı olan CSS Grid’i bir araya getirerek dinamik, duyarlı ve sonsuz kaydırmalı bir resim galerisi uygulamasının nasıl inşa edileceğini adım adım inceleyeceğiz. React’in bileşen tabanlı yapısı ve durum yönetimi yetenekleri, uygulamanın dinamik veri akışını kolayca yönetmemizi sağlarken, CSS Grid ise galeri düzenini esnek ve performant bir şekilde oluşturmamız için ideal bir araç olacaktır.

Uygulamamızın temel amacı, bir API’den resim verilerini çekmek, bu resimleri bir ızgara düzeninde sergilemek ve kullanıcı sayfanın sonuna yaklaştığında otomatik olarak daha fazla resim yükleyerek sonsuz kaydırma etkisini yaratmaktır. Bu süreçte, React’in useState ve useEffect kancalarını, olay dinleyicilerini ve modern CSS Grid özelliklerini derinlemesine ele alacağız. Performans ve kullanıcı deneyimi gibi önemli noktaları da göz önünde bulundurarak, sadece çalışan değil, aynı zamanda optimize edilmiş bir çözüm sunmayı hedefliyoruz.

### Sonsuz Kaydırma Nedir ve Neden Kullanılır?

Sonsuz kaydırma, bir web sayfasının sonuna gelindiğinde otomatik olarak yeni içerik yüklemesi prensibine dayanır. Bu, kullanıcıların bir sonraki sayfa düğmesine tıklamak zorunda kalmadan sürekli yeni içerik keşfetmesine olanak tanır. Kullanıcıların dikkatini dağıtmadan içeriğe odaklanmalarını sağlaması, özellikle mobil cihazlarda ekran alanının kısıtlı olduğu durumlarda büyük avantaj sağlar.

Avantajları:
* Gelişmiş Kullanıcı Deneyimi: Kesintisiz bir akış sunarak kullanıcı etkileşimini artırır.
* Daha Az Tıklama: Kullanıcıların sayfa geçişleri için tıklama yapmasını gerektirmez.
* Mobil Dostu: Küçük ekranlarda gezinmeyi kolaylaştırır.
* Daha Fazla İçerik Keşfi: Kullanıcıların potansiyel olarak daha fazla içeriği görmesini sağlar.

Dezavantajları (ve nasıl üstesinden gelineceği):
* Sayfa Altbilgisine Ulaşamama: Sonsuz içerik nedeniyle altbilgiye ulaşmak zorlaşabilir. Bu durum, önemli bilgileri (iletişim, gizlilik politikası vb.) yan menülere veya sabit bir başlığa yerleştirerek çözülebilir.
* Geri Dönüş Sorunları: Kullanıcılar sayfada çok aşağıya kaydıktan sonra geri döndüklerinde, önceki konumlarını kaybetme riskiyle karşılaşabilirler. Tarayıcı geçmişi API’leri veya durum yönetimi ile bu sorun hafifletilebilir.
* Performans Sorunları: Çok fazla DOM öğesi sayfa performansını düşürebilir. Sanallaştırma (virtualization) ve optimize edilmiş bileşenler ile bu sorunlar giderilebilir.

Bu makalede, sonsuz kaydırmanın avantajlarından yararlanırken potansiyel dezavantajlarını da en aza indirmeye odaklanacağız.

### Proje Kurulumu ve Temel Yapı

Uygulamamızı geliştirmeye başlamadan önce, React projemizi oluşturalım. Bunun için create-react-app veya Vite gibi araçları kullanabiliriz. Biz burada daha hafif ve hızlı olduğu için Vite’ı tercih edeceğiz.

# Vite ile yeni bir React projesi oluşturma
npm create vite@latest infinite-scroll-gallery -- --template react

Proje dizinine git

cd infinite-scroll-gallery

Bağımlılıkları yükle

npm install

Projeyi başlat

npm run dev

Projemizi başlattıktan sonra, tarayıcınızda http://localhost:5173 (veya benzeri bir port) adresinde varsayılan React uygulamasını görmelisiniz. Şimdi projemizin ana dosyalarını düzenlemeye başlayalım. src klasörünün altında components adında bir klasör oluşturalım ve bileşenlerimizi buraya yerleştirelim.

src/
├── App.jsx
├── main.jsx
├── index.css
└── components/
    ├── GalleryGrid.jsx
    ├── ImageCard.jsx

App.jsx ana bileşenimiz olacak, GalleryGrid.jsx resimleri bir ızgarada gösterecek, ve ImageCard.jsx ise tek bir resmi temsil edecek.

### Resim Verilerini Çekme ve Durum Yönetimi

Sonsuz kaydırmalı bir galeri için en önemli adımlardan biri, resim verilerini bir API’den çekmek ve bu verileri React bileşeninin durumunda yönetmektir. Genellikle Unsplash, Pexels veya Pixabay gibi ücretsiz resim API’leri kullanılabilir. Bu örnekte, gerçek bir API yerine basit bir mock (sahte) API çağrısı yaparak konsepti açıklayacağız. Ancak gerçek bir uygulamada bir API anahtarı almanız ve API’nin dokümantasyonuna göre istek yapmanız gerekecektir.

App.jsx dosyamızı aşağıdaki gibi güncelleyelim:

// src/App.jsx
import React, { useState, useEffect, useCallback } from 'react';
import GalleryGrid from './components/GalleryGrid';
import './App.css'; // Global CSS dosyamız

const App = () => {
  const [images, setImages] = useState([]);
  const [page, setPage] = useState(1);
  const [loading, setLoading] = useState(false);
  const [hasMore, setHasMore] = useState(true); // Daha fazla resim olup olmadığını kontrol eder

  // Mock API çağrısı - Gerçek bir uygulamada bir API'ye istek atacaksınız
  const fetchImages = useCallback(async () => {
    if (loading || !hasMore) return; // Zaten yükleniyorsa veya daha fazla resim yoksa tekrar istek atma

    setLoading(true);
    console.log(Fetching images for page ${page}...);

    try {
      // Gerçek bir API çağrısı yerine setTimeout ile simüle ediyoruz
      const response = await new Promise(resolve => setTimeout(() => {
        const newImages = Array.from({ length: 15 }, (_, i) => ({
          id: (page - 1) * 15 + i,
          src: https://picsum.photos/id/${(page - 1) * 15 + i + 10}/400/300, // Picsum.photos'tan rastgele resimler
          alt: Resim ${(page - 1) * 15 + i + 10},
        }));
        resolve({ data: newImages });
      }, 1000)); // 1 saniye gecikme ile yükleme simülasyonu

      if (response.data.length === 0 || page >= 5) { // Örnek olarak 5 sayfadan sonra resim bitiyor
        setHasMore(false);
      } else {
        setImages(prevImages => [...prevImages, ...response.data]);
        setPage(prevPage => prevPage + 1);
      }
    } catch (error) {
      console.error("Resimler çekilirken bir hata oluştu:", error);
      // Hata durumunda kullanıcıya bilgi verilebilir
    } finally {
      setLoading(false);
    }
  }, [page, loading, hasMore]); // Bağımlılıkları ekliyoruz

  // Bileşen yüklendiğinde ve sayfa değiştiğinde resimleri çek
  useEffect(() => {
    fetchImages();
  }, []); // Sadece ilk render'da çağırıyoruz, scroll event'i ile tetiklenecek

  // Kaydırma olayını dinle
  useEffect(() => {
    const handleScroll = () => {
      // Sayfanın alt kısmına ne kadar yaklaştığımızı kontrol et
      const { scrollTop, clientHeight, scrollHeight } = document.documentElement;

      // Kullanıcı sayfanın %80'ine ulaştığında yeni resimleri çek
      if (scrollTop + clientHeight >= scrollHeight * 0.8 && !loading && hasMore) {
        fetchImages();
      }
    };

    window.addEventListener('scroll', handleScroll);
    return () => window.removeEventListener('scroll', handleScroll); // Temizleme fonksiyonu
  }, [loading, hasMore, fetchImages]); // Bağımlılıkları ekliyoruz

  return (
    
{loading &&

Resimler yükleniyor...

} {!hasMore && !loading && images.length > 0 && (

Daha fazla resim yok.

)} {!hasMore && !loading && images.length === 0 && (

Gösterilecek resim bulunamadı.

)}
); }; export default App;

Kod Açıklaması:

* useState Kancaları:
* images: Çekilen tüm resimleri tutan bir dizi. Yeni resimler eklendikçe bu diziye eklenir.
* page: API’den hangi sayfanın çekileceğini belirten bir sayı. Her başarılı çekimden sonra artırılır.
* loading: Resim çekme işleminin devam edip etmediğini gösteren bir boolean. Bu, aynı anda birden fazla istek atılmasını engeller.
* hasMore: Daha fazla resim olup olmadığını gösteren bir boolean. API’den boş bir dizi geldiğinde veya belirli bir sayfa sınırına ulaşıldığında false olarak ayarlanır.
* fetchImages Fonksiyonu:
* useCallback ile sarmalanmıştır. Bu, fonksiyonun sadece bağımlılıkları değiştiğinde yeniden oluşturulmasını sağlayarak performansı artırır ve useEffect içindeki sonsuz döngüleri engeller.
* loading veya hasMore durumu false ise, gereksiz API çağrılarını önlemek için erken çıkar.
* setLoading(true) ile yükleme durumunu başlatır.
* setTimeout kullanarak 1 saniyelik bir gecikmeyle sahte resim verileri oluşturur. Gerçek bir uygulamada burada fetch veya axios ile bir API’ye HTTP isteği yapacaksınız.
* Gelen resimleri setImages ile mevcut resim dizisine ekler (prevImages kullanarak önceki duruma erişmek önemlidir).
* setPage ile sayfa numarasını bir artırır.
* Belirli bir sayfa sayısına ulaşıldığında veya API’den boş veri geldiğinde setHasMore(false) ile daha fazla resim olmadığını belirtir.
* finally bloğunda setLoading(false) ile yükleme durumunu sonlandırır.
* İlk useEffect (Boş Bağımlılık Dizisi):
* Bileşen ilk kez render edildiğinde fetchImages fonksiyonunu çağırarak ilk resim grubunu yükler.
* İkinci useEffect (Kaydırma Olayı Dinleyicisi):
* window.addEventListener('scroll', handleScroll) ile pencere kaydırma olayını dinler.
* handleScroll fonksiyonu, document.documentElement.scrollTop, clientHeight ve scrollHeight değerlerini kullanarak kullanıcının sayfanın altına ne kadar yaklaştığını hesaplar.
Kullanıcı sayfanın %80’ine ulaştığında (scrollTop + clientHeight >= scrollHeight 0.8), loading durumu false ise ve hasMore durumu true ise fetchImages fonksiyonunu çağırarak yeni resimler yükler.
* return () => window.removeEventListener('scroll', handleScroll); satırı, bileşen DOM’dan kaldırıldığında bellek sızıntılarını önlemek için olay dinleyicisini temizler. Bu, useEffect kancasının temizleme mekanizmasıdır.

### Bileşenlerin Oluşturulması: ImageCard ve GalleryGrid

Şimdi çekilen resimleri göstermek için ImageCard ve GalleryGrid bileşenlerini oluşturalım.

#### ImageCard.jsx

Bu bileşen, tek bir resmi ve ilgili bilgilerini (varsa) göstermekten sorumludur.

// src/components/ImageCard.jsx
import React from 'react';
import './ImageCard.css'; // ImageCard için özel CSS

const ImageCard = ({ image }) => {
  return (
    
{image.alt} {/ İsteğe bağlı olarak resim başlığı, yazar bilgisi eklenebilir /} {/

{image.alt}

/}
); }; export default ImageCard;

ImageCard.css:

/ src/components/ImageCard.css /
.image-card {
  background-color: #f0f0f0;
  border-radius: 8px;
  overflow: hidden;
  box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
  display: flex;
  flex-direction: column;
  height: 100%; / Grid öğelerinin eşit yüksekliğe sahip olmasını sağlar /
}

.image-card img {
  width: 100%;
  height: 200px; / Sabit bir yükseklik veya aspect-ratio kullanabilirsiniz /
  object-fit: cover; / Resmi kutuya sığdırır, oranını korur /
  display: block;
}

.image-card .image-alt {
  padding: 10px;
  font-size: 0.9em;
  color: #555;
}

Kod Açıklaması:

* ImageCard bileşeni, image adında bir prop alır. Bu prop, resmin src (kaynak URL) ve alt (alternatif metin) gibi bilgilerini içeren bir nesnedir.
* etiketi, src ve alt özelliklerini kullanarak resmi görüntüler.
* loading="lazy" özelliği, resimlerin görünür alana (viewport) yakın olana kadar yüklenmesini geciktirerek performansı artırır. Bu, özellikle sonsuz kaydırmalı galerilerde çok sayıda resim olduğunda önemlidir.
* CSS, her bir resim kartının hoş bir görünüme sahip olmasını sağlar. object-fit: cover; resmi kırparak belirlenen alana sığdırır ve oranını korur.

#### GalleryGrid.jsx

Bu bileşen, çekilen tüm ImageCard bileşenlerini bir CSS Grid düzeninde düzenler.

// src/components/GalleryGrid.jsx
import React from 'react';
import ImageCard from './ImageCard';
import './GalleryGrid.css'; // GalleryGrid için özel CSS

const GalleryGrid = ({ images }) => {
  return (
    
{images.map(image => ( ))}
); }; export default GalleryGrid;

GalleryGrid.css:

/ src/components/GalleryGrid.css /
.gallery-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); / Duyarlı ızgara /
  gap: 20px; / Izgara öğeleri arasındaki boşluk /
  padding: 20px;
  max-width: 1200px; / Maksimum genişlik /
  margin: 0 auto; / Ortala /
}

/ Küçük ekranlar için medya sorgusu /
@media (max-width: 768px) {
  .gallery-grid {
    grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));
    gap: 15px;
    padding: 15px;
  }
}

@media (max-width: 480px) {
  .gallery-grid {
    grid-template-columns: 1fr; / Tek sütun /
    gap: 10px;
    padding: 10px;
  }
}

Kod Açıklaması:

* GalleryGrid bileşeni, images adında bir dizi prop alır.
* images.map() fonksiyonu kullanılarak dizideki her bir resim nesnesi için bir ImageCard bileşeni oluşturulur.
* key prop’u, React’in liste öğelerini verimli bir şekilde güncellemesi için benzersiz bir değer olmalıdır (burada image.id kullanıldı).
* CSS Grid özellikleri gallery-grid sınıfına uygulanır:
* display: grid;: Öğelerin bir ızgara düzeninde yerleştirilmesini sağlar.
* grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));: Bu, en önemli kısımdır.
* repeat(): Belirtilen deseni tekrarlar.
* auto-fill: Mevcut alana sığabilecek kadar sütun oluşturur.
* minmax(280px, 1fr): Her sütunun minimum genişliği 280 piksel, maksimum genişliği ise kullanılabilir alanın bir fraksiyonu (1fr) kadar olmasını sağlar. Bu sayede ızgara, ekran boyutuna göre otomatik olarak sütun sayısını ayarlar ve duyarlı bir yapı kazanır.
* gap: 20px;: Izgara öğeleri arasında 20 piksel boşluk bırakır.
* Medya sorguları (@media) kullanılarak farklı ekran boyutları için ızgara düzeni optimize edilir. Özellikle mobil cihazlarda tek sütunlu bir düzen tercih edilebilir.

### Global CSS Dosyası

Uygulamamızın genel görünümünü ayarlamak için App.css dosyamızı güncelleyelim (veya index.css kullanabiliriz).

/ src/App.css veya src/index.css /
body {
  margin: 0;
  font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
    'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
    sans-serif;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  background-color: #f8f8f8;
  color: #333;
}

.App {
  text-align: center;
  padding-bottom: 50px; / Yükleme göstergesi için alt boşluk /
}

h1 {
  color: #2c3e50;
  margin-top: 30px;
  margin-bottom: 30px;
}

.loading-indicator,
.no-more-images,
.no-images-found {
  margin: 20px auto;
  padding: 10px 20px;
  background-color: #e0f7fa;
  color: #00796b;
  border-radius: 5px;
  max-width: 300px;
  font-weight: bold;
}

.no-more-images {
  background-color: #fff3e0;
  color: #e65100;
}

.no-images-found {
  background-color: #ffebee;
  color: #c62828;
}

Bu CSS, uygulamanın genel font ayarlarını, arka plan rengini ve yükleme/mesaj göstergelerinin stilini belirler.

### Performans ve Kullanıcı Deneyimi İyileştirmeleri

Sonsuz kaydırmalı galerilerde performans ve kullanıcı deneyimi kritik öneme sahiptir. İşte uygulamanızı daha iyi hale getirecek bazı ek ipuçları:

Debounce veya Throttle Kullanımı

Kaydırma olayları çok sık tetiklenir ve bu da handleScroll fonksiyonunun gereğinden fazla çalışmasına neden olabilir. Bu durumu önlemek için debounce veya throttle tekniklerini kullanmak faydalıdır.

* Debounce: Bir olay tetiklendiğinde, belirli bir süre içinde başka bir tetikleme olmazsa fonksiyonu çalıştırır. Örneğin, kullanıcı kaydırmayı durdurduktan 200ms sonra fonksiyon çalışır.
* Throttle: Bir olay tetiklendiğinde, belirli bir zaman aralığında fonksiyonun yalnızca bir kez çalışmasını sağlar. Örneğin, her 200ms’de bir fonksiyon çalışır, daha sık tetiklenmez.

Bu örnekte, handleScroll içindeki kontrol (!loading && hasMore) zaten gereksiz fetchImages çağrılarını engellediği için basit bir uygulama için yeterli olabilir. Ancak çok karmaşık hesaplamalar yapan bir handleScroll fonksiyonunuz varsa, bir debounce yardımcı fonksiyonu eklemek iyi bir fikir olabilir.

// Debounce yardımcı fonksiyonu örneği
const debounce = (func, delay) => {
  let timeout;
  return function(...args) {
    const context = this;
    clearTimeout(timeout);
    timeout = setTimeout(() => func.apply(context, args), delay);
  };
};

// useEffect içinde kullanımı
useEffect(() => {
  const debouncedHandleScroll = debounce(handleScroll, 200); // 200ms debounce
  window.addEventListener('scroll', debouncedHandleScroll);
  return () => window.removeEventListener('scroll', debouncedHandleScroll);
}, [loading, hasMore, fetchImages]);

Resim Optimizasyonu ve Lazy Loading

* loading="lazy": Daha önce ImageCard bileşeninde kullandığımız loading="lazy" özelliği, tarayıcının resimleri sadece görünür alana (viewport) yakın olduklarında yüklemesini sağlar. Bu, başlangıç yükleme süresini önemli ölçüde azaltır.
* Duyarlı Resimler (srcset): Farklı ekran boyutları ve çözünürlükler için uygun boyutlarda resimler sunmak, bant genişliğinden tasarruf sağlar ve yükleme hızını artırır. ... yapısı kullanılabilir.
* Resim Sıkıştırma: Sunucu tarafında resimleri optimize edilmiş formatlarda (WebP gibi) ve uygun sıkıştırma oranlarında sunmak, dosya boyutlarını küçültür.

Kullanıcı Geri Bildirimi

* Yükleme Göstergeleri: loading durumu true olduğunda bir spinner veya “Yükleniyor…” mesajı göstermek, kullanıcının uygulamanın çalıştığını anlamasını sağlar.
* “Daha Fazla Resim Yok” Mesajı: hasMore durumu false olduğunda bu mesajı göstermek, kullanıcının daha fazla içerik beklememesini sağlar.
* Hata Mesajları: API çağrısı sırasında bir hata oluştuğunda kullanıcıya bilgilendirici bir mesaj göstermek, uygulamanın sağlamlığını artırır.

Erişilebilirlik (Accessibility – A11y)

* alt Nitelikleri: Tüm etiketlerinde açıklayıcı alt nitelikleri kullanmak, ekran okuyucular kullanan görme engelli kullanıcılar için önemlidir. Bu, aynı zamanda SEO için de faydalıdır.
* Klavye Navigasyonu: Galeri öğelerinin klavye ile gezilebilir olmasını sağlamak (tab index vb. kullanarak), fare kullanamayan kullanıcılar için önemlidir.

Sanallaştırma (Virtualization)

Çok büyük galerilerde (binlerce resim), DOM’da çok sayıda öğe bulundurmak performansı ciddi şekilde etkileyebilir. Sanallaştırma (veya pencereleme – windowing), sadece görünür alandaki (veya görünür alana yakın) öğeleri render ederek bu sorunu çözer. React-Window veya React-Virtualize gibi kütüphaneler bu amaçla kullanılabilir. Ancak bu, daha karmaşık bir konudur ve bu makalenin kapsamının dışındadır, ancak çok büyük veri setleri için akılda tutulması gereken bir çözümdür.

### Sonuç

Bu makalede, React ve CSS Grid kullanarak nasıl duyarlı ve sonsuz kaydırmalı bir resim galerisi oluşturulacağını adım adım öğrendik. React’in durum yönetimi (useState) ve yaşam döngüsü (useEffect) kancalarını kullanarak dinamik veri çekme ve kaydırma olaylarını yakalama mekanizmasını kurduk. CSS Grid’in güçlü grid-template-columns özelliği sayesinde, farklı ekran boyutlarına uyum sağlayan esnek ve otomatik bir ızgara düzeni oluşturduk.

Uygulamamızın temel taşları şunlardı:
* App.jsx: Ana uygulama bileşeni, durum yönetimi, API çağrısı simülasyonu ve kaydırma olayını dinleme.
* GalleryGrid.jsx: Resim kartlarını CSS Grid düzeninde düzenleyen kapsayıcı bileşen.
* ImageCard.jsx: Tek bir resmi gösteren ve lazy loading özelliğini kullanan bileşen.

Ayrıca, performans, kullanıcı deneyimi ve erişilebilirlik gibi önemli konulara değinerek, uygulamanızı daha sağlam ve kullanıcı dostu hale getirecek iyileştirmelerden bahsettik. Debounce/throttle, resim optimizasyonu ve kullanıcı geri bildirimi, modern web uygulamalarının vazgeçilmezleridir.

Bu rehber, sonsuz kaydırmalı galeriler oluşturmak için sağlam bir temel sunmaktadır. Gerçek projelerde, mock API yerine gerçek bir resim API’si (Unsplash, Pexels vb.) entegre edebilir, hata yönetimi ve kullanıcı arayüzü geri bildirimlerini daha detaylı hale getirebilirsiniz. React ve CSS Grid’in birleşimi, bu tür dinamik ve görsel açıdan zengin uygulamaları geliştirirken size büyük esneklik ve güç sağlayacaktır. Artık kendi sonsuz kaydırma galerilerinizi oluşturmak için gerekli bilgi ve araçlara sahipsiniz!

Yorumlar
İçeriği beğendiniz mi? Bir tartışma başlatın veya görüşlerinizi paylaşın.
Yorum Yaz

Bir yanıt yazın

E-posta adresiniz yayınlanmayacak. Gerekli alanlar * ile işaretlenmişlerdir

E-posta Bülteni
Yazılım Topluluğuna Katılın
En son güncellemeleri, yaratıcı ipuçlarını ve özel kaynakları doğrudan e-posta kutunuza alın. Tasarım ve inovasyonun geleceğini birlikte keşfedelim.