Ana içeriğe geç

Sözleşmeniz için bir kullanıcı arayüzü oluşturma

typescript
react
vite
wagmi
ön uç
Acemi
Ori Pomerantz
1 Kasım 2023
12 dakikalık okuma

Ethereum ekosisteminde ihtiyaç duyduğumuz bir özelliği buldunuz. Bunu uygulamak için akıllı sözleşmeleri ve hatta zincir dışında çalışan bazı ilgili kodları yazdınız. Bu harika! Maalesef, bir kullanıcı arayüzü olmadan herhangi bir kullanıcınız olmayacak ve son web sitesi yazdığınızda insanlar çevirmeli modem kullanıyordu ve JavaScript yeniydi.

Bu makale sizin için. Programlama bildiğinizi, hatta belki biraz JavaScript ve HTML bildiğinizi, ancak kullanıcı arayüzü becerilerinizin körelmiş ve güncelliğini yitirmiş olduğunu varsayıyorum. Bugünlerde işlerin nasıl yapıldığını görmeniz için birlikte basit ve modern bir uygulamayı inceleyeceğiz.

Bu neden önemli

Teoride, sözleşmelerinizle etkileşim kurmaları için insanların Etherscan (opens in a new tab) veya Blockscout (opens in a new tab) kullanmasını sağlayabilirsiniz. Bu, deneyimli Ethereum'cular için harika olacaktır. Ancak biz bir milyar insana daha (opens in a new tab) hizmet vermeye çalışıyoruz. Bu, harika bir kullanıcı deneyimi olmadan gerçekleşmez ve kullanıcı dostu bir arayüz bunun büyük bir parçasıdır.

Greeter uygulaması

Modern bir kullanıcı arayüzünün nasıl çalıştığının arkasında pek çok teori ve bunu açıklayan (opens in a new tab) birçok iyi site (opens in a new tab) var. Bu sitelerin yaptığı güzel işleri tekrarlamak yerine, yaparak öğrenmeyi tercih ettiğinizi varsayacağım ve oynayabileceğiniz bir uygulamayla başlayacağım. İşleri halletmek için yine de teoriye ihtiyacınız var ve buna da geleceğiz - sadece kaynak dosyadan kaynak dosyaya gideceğiz ve karşılaştıkça konuları tartışacağız.

Kurulum

  1. Gerekirse, Holesky blokzincirini (opens in a new tab) cüzdanınıza ekleyin ve test ETH'si alın (opens in a new tab).

  2. Github deposunu klonlayın.

    1git clone https://github.com/qbzzt/20230801-modern-ui.git
  3. Gerekli paketleri yükleyin.

    1cd 20230801-modern-ui
    2pnpm install
  4. Uygulamayı başlatın.

    1pnpm dev
  5. Uygulamanın gösterdiği URL'ye gidin. Çoğu durumda bu http://localhost:5173/ (opens in a new tab) adresidir.

  6. Sözleşme kaynak kodunu, Hardhat'in Greeter'ının biraz değiştirilmiş bir sürümünü bir blokzincir gezgininde (opens in a new tab) görebilirsiniz.

Dosya incelemesi

index.html

Bu dosya, betik dosyasını içeri aktaran bu satır dışında standart bir HTML basmakalıp kodudur.

1<script type="module" src="/src/main.tsx"></script>

src/main.tsx

Dosya uzantısı bize bu dosyanın, tür denetimini (opens in a new tab) destekleyen bir JavaScript uzantısı olan TypeScript (opens in a new tab) ile yazılmış bir React bileşeni (opens in a new tab) olduğunu söyler. TypeScript, JavaScript'e derlenir, bu nedenle istemci tarafında yürütme için kullanabiliriz.

1import '@rainbow-me/rainbowkit/styles.css'
2import { RainbowKitProvider } from '@rainbow-me/rainbowkit'
3import * as React from 'react'
4import * as ReactDOM from 'react-dom/client'
5import { WagmiConfig } from 'wagmi'
6import { chains, config } from './wagmi'

İhtiyacımız olan kütüphane kodunu içe aktarın.

1import { App } from './App'

Uygulamayı uygulayan React bileşenini içe aktarın (aşağıya bakın).

1ReactDOM.createRoot(document.getElementById('root')!).render(

Kök React bileşenini oluşturun. render parametresi, hem HTML hem de JavaScript/TypeScript kullanan bir uzantı dili olan JSX (opens in a new tab)'tir. Buradaki ünlem işareti, TypeScript bileşenine şunu söyler: "document.getElementById('root') ifadesinin ReactDOM.createRoot için geçerli bir parametre olacağını bilmiyorsun, ama endişelenme - ben geliştiriciyim ve sana olacağını söylüyorum".

1 <React.StrictMode>

Uygulama, bir React.StrictMode bileşeninin (opens in a new tab) içine giriyor. Bu bileşen, React kütüphanesine, geliştirme sırasında yararlı olan ek hata ayıklama kontrolleri eklemesini söyler.

1 <WagmiConfig config={config}>

Uygulama ayrıca bir WagmiConfig bileşeninin (opens in a new tab) içindedir. wagmi (başaracağız) kütüphanesi (opens in a new tab), bir Ethereum merkeziyetsiz uygulaması yazmak için React UI tanımlarını viem kütüphanesi (opens in a new tab) ile birleştirir.

1 <RainbowKitProvider chains={chains}>

Ve son olarak, bir RainbowKitProvider bileşeni (opens in a new tab). Bu bileşen, oturum açmayı ve cüzdan ile uygulama arasındaki iletişimi yönetir.

1 <App />

Şimdi, kullanıcı arayüzünü gerçekten uygulayan uygulama bileşenine sahip olabiliriz. Bileşenin sonundaki />, XML standardına göre bu bileşenin içinde herhangi bir tanım olmadığını React'e bildirir.

1 </RainbowKitProvider>
2 </WagmiConfig>
3 </React.StrictMode>,
4)

Elbette diğer bileşenleri de kapatmamız gerekiyor.

src/App.tsx

1import { ConnectButton } from '@rainbow-me/rainbowkit'
2import { useAccount } from 'wagmi'
3import { Greeter } from './components/Greeter'
4
5export function App() {

Bu, bir React bileşeni oluşturmanın standart yoludur - her işlenmesi gerektiğinde çağrılan bir işlev tanımlayın. Bu işlevin genellikle en üstünde TypeScript veya JavaScript kodu bulunur, ardından JSX kodunu döndüren bir return ifadesi gelir.

1 const { isConnected } = useAccount()

Burada, bir cüzdan aracılığıyla bir blokzincire bağlı olup olmadığımızı kontrol etmek için useAccount (opens in a new tab) kullanıyoruz.

Geleneksel olarak, React'ta use... olarak adlandırılan işlevler, bir tür veri döndüren kancalardır (opens in a new tab). Bu tür kancaları kullandığınızda, bileşeniniz yalnızca verileri almakla kalmaz, aynı zamanda bu veriler değiştiğinde bileşen güncellenmiş bilgilerle yeniden oluşturulur.

1 return (
2 <>

Bir React bileşeninin JSX'i tek bir bileşen döndürmek zorundadır. Birden çok bileşenimiz olduğunda ve bunları "doğal olarak" saran bir şeyimiz olmadığında, boş bir bileşen kullanırız (<> ... </>) onları tek bir bileşen haline getirmek için.

1 <h1>Greeter</h1>
2 <ConnectButton />

ConnectButton bileşenini (opens in a new tab) RainbowKit'ten alıyoruz. Bağlı olmadığımızda, cüzdanları açıklayan ve hangisini kullandığınızı seçmenize izin veren bir kalıp açan bir Cüzdan Bağla düğmesi verir. Bağlandığımızda, kullandığımız blokzinciri, hesap adresimizi ve ETH bakiyemizi görüntüler. Ağı değiştirmek veya bağlantıyı kesmek için bu ekranları kullanabiliriz.

1 {isConnected && (

Gerçek JavaScript'i (veya JavaScript'e derlenecek TypeScript'i) bir JSX'e eklememiz gerektiğinde, parantez ({}) kullanırız.

a && b sözdizimi, [a ? için kısadır. b : a](https://www.w3schools.com/react/react_es6_ternary.asp). Yani, adoğruysabolarak değerlendirilir, aksi takdirdea olarak değerlendirilir (false, 0` vb. olabilir). Bu, React'e bir bileşenin yalnızca belirli bir koşul yerine getirildiğinde görüntülenmesi gerektiğini söylemenin kolay bir yoludur.

Bu durumda, kullanıcıyı Greeter yalnızca kullanıcı bir blokzincire bağlıysa göstermek istiyoruz.

1 <Greeter />
2 )}
3 </>
4 )
5}

src/components/Greeter.tsx

Bu dosya, kullanıcı arayüzü işlevselliğinin çoğunu içerir. Normalde birden çok dosyada olacak tanımları içerir, ancak bu bir öğretici olduğu için program, performans veya bakım kolaylığından ziyade ilk seferde anlaşılması kolay olacak şekilde optimize edilmiştir.

1import { useState, ChangeEventHandler } from 'react'
2import { useNetwork,
3 useReadContract,
4 usePrepareContractWrite,
5 useContractWrite,
6 useContractEvent
7 } from 'wagmi'

Bu kütüphane fonksiyonlarını kullanıyoruz. Yine, kullanıldıkları yerde aşağıda açıklanmıştır.

1import { AddressType } from 'abitype'

abitype kütüphanesi (opens in a new tab) bize AddressType (opens in a new tab) gibi çeşitli Ethereum veri türleri için TypeScript tanımları sağlar.

1let greeterABI = [
2 .
3 .
4 .
5] as const // greeterABI

Greeter sözleşmesi için ABI. Sözleşmeleri ve kullanıcı arayüzünü aynı anda geliştiriyorsanız, normalde bunları aynı depoya koyar ve Solidity derleyicisi tarafından oluşturulan ABI'yi uygulamanızda bir dosya olarak kullanırsınız. Ancak, sözleşme zaten geliştirildiği ve değişmeyeceği için burada bu gerekli değildir.

1type AddressPerBlockchainType = {
2 [key: number]: AddressType
3}

TypeScript güçlü bir şekilde yazılmıştır. Greeter sözleşmesinin farklı zincirlerde dağıtıldığı adresi belirtmek için bu tanımı kullanırız. Anahtar bir sayıdır (chainId) ve değer bir AddressType'tır (bir adres).

1const contractAddrs: AddressPerBlockchainType = {
2 // Holesky
3 17000: '0x432d810484AdD7454ddb3b5311f0Ac2E95CeceA8',
4
5 // Sepolia
6 11155111: '0x7143d5c190F048C8d19fe325b748b081903E3BF0'
7}

Desteklenen iki ağdaki sözleşmenin adresi: Holesky (opens in a new tab) ve Sepolia (opens in a new tab).

Not: Aslında Redstone Holesky için üçüncü bir tanım var, aşağıda açıklanacaktır.

1type ShowObjectAttrsType = {
2 name: string,
3 object: any
4}

Bu tür, ShowObject bileşenine (daha sonra açıklanacaktır) bir parametre olarak kullanılır. Hata ayıklama amacıyla görüntülenen nesnenin adını ve değerini içerir.

1type ShowGreetingAttrsType = {
2 greeting: string | undefined
3}

Herhangi bir zamanda, ya selamlamanın ne olduğunu biliyor olabiliriz (çünkü onu blokzincirden okuduk) ya da bilmiyor olabiliriz (çünkü henüz almadık). Bu nedenle, bir dize veya hiçbir şey olabilen bir türe sahip olmak yararlıdır.

Greeter bileşeni
1const Greeter = () => {

Sonunda bileşeni tanımlayacağız.

1 const { chain } = useNetwork()

wagmi (opens in a new tab) sayesinde kullandığımız zincir hakkındaki bilgiler. Bu bir kanca (use...) olduğu için, bu bilgi her değiştiğinde bileşen yeniden çizilir.

1 const greeterAddr = chain && contractAddrs[chain.id]

Greeter sözleşmesinin adresi, zincire göre değişir (ve zincir bilgimiz yoksa veya bu sözleşmenin olmadığı bir zincirdeysek undefined olur).

1 const readResults = useReadContract({
2 address: greeterAddr,
3 abi: greeterABI,
4 functionName: "greet" , // No arguments
5 watch: true
6 })

useReadContract kancası (opens in a new tab), bir sözleşmeden bilgi okur. Kullanıcı arayüzünde readResults'ı genişleterek tam olarak hangi bilgileri döndürdüğünü görebilirsiniz. Bu durumda, selamlama değiştiğinde bilgilendirilmek için bakmaya devam etmesini istiyoruz.

Not: Selamlamanın ne zaman değiştiğini bilmek ve bu şekilde güncelleme yapmak için setGreeting olaylarını (opens in a new tab) dinleyebiliriz. Ancak, daha verimli olsa da, her durumda geçerli olmayacaktır. Kullanıcı farklı bir zincire geçtiğinde selamlama da değişir, ancak bu değişikliğe bir olay eşlik etmez. Kodun bir kısmı olayları dinlerken, diğeri zincir değişikliklerini belirlemek için kullanılabilir, ancak bu, sadece watch parametresini (opens in a new tab) ayarlamaktan daha karmaşık olurdu.

1 const [ newGreeting, setNewGreeting ] = useState("")

React'in useState kancası (opens in a new tab), değeri bileşenin bir oluşturulmasından diğerine devam eden bir durum değişkeni belirtmemizi sağlar. Başlangıç değeri, bu durumda boş dize olan parametredir.

useState kancası iki değer içeren bir liste döndürür:

  1. Durum değişkeninin geçerli değeri.
  2. Gerektiğinde durum değişkenini değiştirmek için bir işlev. Bu bir kanca olduğu için, her çağrıldığında bileşen yeniden oluşturulur.

Bu durumda, kullanıcının ayarlamak istediği yeni selamlama için bir durum değişkeni kullanıyoruz.

1 const greetingChange : ChangeEventHandler<HTMLInputElement> = (evt) =>
2 setNewGreeting(evt.target.value)

Bu, yeni selamlama giriş alanı değiştiğinde olay işleyicisidir. Tür, ChangeEventHandler<HTMLInputElement> (opens in a new tab), bunun bir HTML giriş öğesinin değer değişikliği için bir işleyici olduğunu belirtir. <HTMLInputElement> kısmı, bunun bir genel tür (opens in a new tab) olması nedeniyle kullanılır.

1 const preparedTx = usePrepareContractWrite({
2 address: greeterAddr,
3 abi: greeterABI,
4 functionName: 'setGreeting',
5 args: [ newGreeting ]
6 })
7 const workingTx = useContractWrite(preparedTx.config)

Bu, istemci perspektifinden bir blokzincir işlemini gönderme sürecidir:

  1. eth_estimateGas (opens in a new tab) kullanarak işlemi blokzincirdeki bir düğüme gönderin.
  2. Düğümden bir yanıt bekleyin.
  3. Yanıt alındığında, kullanıcıdan işlemi cüzdan aracılığıyla imzalamasını isteyin. Bu adım, düğüm yanıtı alındıktan sonra gerçekleşmek zorundadır, çünkü kullanıcıya işlemi imzalamadan önce işlemin gaz maliyeti gösterilir.
  4. Kullanıcının onaylamasını bekleyin.
  5. İşlemi bu kez eth_sendRawTransaction (opens in a new tab) kullanarak tekrar gönderin.

Adım 2'nin algılanabilir bir süre alması muhtemeldir; bu süre zarfında kullanıcılar, komutlarının kullanıcı arayüzü tarafından gerçekten alınıp alınmadığını ve neden zaten işlemi imzalamaları istenmediğini merak ederler. Bu kötü bir kullanıcı deneyimi (UX) yaratır.

Çözüm, hazırlık kancalarını (opens in a new tab) kullanmaktır. Bir parametre her değiştiğinde, düğüme hemen eth_estimateGas isteğini gönderin. Ardından, kullanıcı işlemi gerçekten göndermek istediğinde (bu durumda Selamlamayı güncelle'ye basarak), gaz maliyeti bilinir ve kullanıcı cüzdan sayfasını hemen görebilir.

1 return (

Şimdi nihayet döndürülecek gerçek HTML'yi oluşturabiliriz.

1 <>
2 <h2>Greeter</h2>
3 {
4 !readResults.isError && !readResults.isLoading &&
5 <ShowGreeting greeting={readResults.data} />
6 }
7 <hr />

Bir ShowGreeting bileşeni oluşturun (aşağıda açıklanmıştır), ancak yalnızca selamlama blokzincirden başarıyla okunduysa.

1 <input type="text"
2 value={newGreeting}
3 onChange={greetingChange}
4 />

Bu, kullanıcının yeni bir selamlama ayarlayabileceği giriş metin alanıdır. Kullanıcı bir tuşa her bastığında, setNewGreeting'i çağıran greetingChange'i çağırırız. setNewGreeting, useState kancasından geldiği için, Greeter bileşeninin yeniden oluşturulmasına neden olur. Bunun anlamı şudur:

  • Yeni selamlamanın değerini korumak için değer belirtmemiz gerekiyor, çünkü aksi takdirde varsayılan olan boş dizeye geri dönerdi.
  • usePrepareContractWrite, newGreeting her değiştiğinde çağrılır, bu da hazırlanan işlemde her zaman en son newGreeting'e sahip olacağı anlamına gelir.
1 <button disabled={!workingTx.write}
2 onClick={workingTx.write}
3 >
4 Selamlamayı güncelle
5 </button>

workingTx.write yoksa, selamlama güncellemesini göndermek için gerekli bilgileri hâlâ bekliyoruz demektir, bu nedenle düğme devre dışı bırakılır. Bir workingTx.write değeri varsa, bu, işlemi göndermek için çağrılacak fonksiyondur.

1 <hr />
2 <ShowObject name="readResults" object={readResults} />
3 <ShowObject name="preparedTx" object={preparedTx} />
4 <ShowObject name="workingTx" object={workingTx} />
5 </>
6 )
7}

Son olarak, ne yaptığımızı görmenize yardımcı olması için kullandığımız üç nesneyi gösterin:

  • readResults
  • preparedTx
  • workingTx
ShowGreeting bileşeni

Bu bileşen gösterir

1const ShowGreeting = (attrs : ShowGreetingAttrsType) => {

Bir bileşen işlevi, bileşenin tüm özniteliklerini içeren bir parametre alır.

1 return <b>{attrs.greeting}</b>
2}
ShowObject bileşeni

Bilgi amaçlı olarak, önemli nesneleri (readResults selamlamayı okumak için ve preparedTx ve workingTx oluşturduğumuz işlemler için) göstermek için ShowObject bileşenini kullanırız.

1const ShowObject = (attrs: ShowObjectAttrsType ) => {
2 const keys = Object.keys(attrs.object)
3 const funs = keys.filter(k => typeof attrs.object[k] == "function")
4 return <>
5 <details>

Kullanıcı arayüzünü tüm bilgilerle doldurmak istemiyoruz, bu nedenle bunları görüntülemeyi veya kapatmayı mümkün kılmak için bir ayrıntılar (opens in a new tab) etiketi kullanıyoruz.

1 <summary>{attrs.name}</summary>
2 <pre>
3 {JSON.stringify(attrs.object, null, 2)}

Alanların çoğu JSON.stringify (opens in a new tab) kullanılarak görüntülenir.

1 </pre>
2 { funs.length > 0 &&
3 <>
4 Functions:
5 <ul>

İstisna, JSON standardının (opens in a new tab) bir parçası olmayan işlevlerdir, bu nedenle ayrı olarak görüntülenmeleri gerekir.

1 {funs.map((f, i) =>

JSX içinde, { küme parantezleri } içindeki kod, JavaScript olarak yorumlanır. Daha sonra, ( normal parantezler ) içindeki kod, tekrar JSX olarak yorumlanır.

1 (<li key={i}>{f}</li>)
2 )}

React, DOM Ağacındaki (opens in a new tab) etiketlerin ayrı tanımlayıcılara sahip olmasını gerektirir. Bu, aynı etiketin alt öğelerinin (bu durumda, sırasız liste (opens in a new tab)) farklı anahtar özniteliklerine ihtiyaç duyduğu anlamına gelir.

1 </ul>
2 </>
3 }
4 </details>
5 </>
6}

Çeşitli HTML etiketlerini sonlandırın.

Son export
1export { Greeter }

Greeter bileşeni, uygulama için dışa aktarmamız gereken bileşendir.

src/wagmi.ts

Son olarak, WAGMI ile ilgili çeşitli tanımlar src/wagmi.ts içindedir. Burada her şeyi açıklamayacağım, çünkü çoğu değiştirmeniz gerekmeyecek basmakalıp bir koddur.

Buradaki kod, github'daki (opens in a new tab) kodla tam olarak aynı değil, çünkü makalenin ilerleyen bölümlerinde başka bir zincir ekliyoruz (Redstone Holesky (opens in a new tab)).

1import { getDefaultWallets } from '@rainbow-me/rainbowkit'
2import { configureChains, createConfig } from 'wagmi'
3import { holesky, sepolia } from 'wagmi/chains'

Uygulamanın desteklediği blokzincirleri içe aktarın. Desteklenen zincirlerin listesini viem github'da (opens in a new tab) görebilirsiniz.

1import { publicProvider } from 'wagmi/providers/public'
2
3const walletConnectProjectId = 'c96e690bb92b6311e8e9b2a6a22df575'

WalletConnect (opens in a new tab) kullanabilmek için uygulamanız için bir proje kimliğine ihtiyacınız var. cloud.walletconnect.com (opens in a new tab) adresinden alabilirsiniz.

1const { chains, publicClient, webSocketPublicClient } = configureChains(
2 [ holesky, sepolia ],
3 [
4 publicProvider(),
5 ],
6)
7
8const { connectors } = getDefaultWallets({
9 appName: 'My wagmi + RainbowKit App',
10 chains,
11 projectId: walletConnectProjectId,
12})
13
14export const config = createConfig({
15 autoConnect: true,
16 connectors,
17 publicClient,
18 webSocketPublicClient,
19})
20
21export { chains }
Tümünü göster

Başka bir blokzincir ekleme

Bu günlerde çok sayıda L2 ölçeklendirme çözümü var ve viem'in henüz desteklemediği bazılarını desteklemek isteyebilirsiniz. Bunu yapmak için src/wagmi.ts dosyasını değiştirin. Bu talimatlar, Redstone Holesky (opens in a new tab) nasıl ekleneceğini açıklar.

  1. Viem'den defineChain türünü içe aktarın.

    1import { defineChain } from 'viem'
  2. Ağ tanımını ekleyin.

    1const redstoneHolesky = defineChain({
    2 id: 17_001,
    3 name: 'Redstone Holesky',
    4 network: 'redstone-holesky',
    5 nativeCurrency: {
    6 decimals: 18,
    7 name: 'Ether',
    8 symbol: 'ETH',
    9 },
    10 rpcUrls: {
    11 default: {
    12 http: ['https://rpc.holesky.redstone.xyz'],
    13 webSocket: ['wss://rpc.holesky.redstone.xyz/ws'],
    14 },
    15 public: {
    16 http: ['https://rpc.holesky.redstone.xyz'],
    17 webSocket: ['wss://rpc.holesky.redstone.xyz/ws'],
    18 },
    19 },
    20 blockExplorers: {
    21 default: { name: 'Explorer', url: 'https://explorer.holesky.redstone.xyz' },
    22 },
    23})
    Tümünü göster
  3. Yeni zinciri configureChains çağrısına ekleyin.

    1 const { chains, publicClient, webSocketPublicClient } = configureChains(
    2 [ holesky, sepolia, redstoneHolesky ],
    3 [ publicProvider(), ],
    4 )
  4. Uygulamanın yeni ağdaki sözleşmelerinizin adresini bildiğinden emin olun. Bu durumda, src/components/Greeter.tsx dosyasını değiştiriyoruz:

    1const contractAddrs : AddressPerBlockchainType = {
    2 // Holesky
    3 17000: '0x432d810484AdD7454ddb3b5311f0Ac2E95CeceA8',
    4
    5 // Redstone Holesky
    6 17001: '0x4919517f82a1B89a32392E1BF72ec827ba9986D3',
    7
    8 // Sepolia
    9 11155111: '0x7143d5c190F048C8d19fe325b748b081903E3BF0'
    10}
    Tümünü göster

Sonuç

Elbette, Greeter için bir kullanıcı arayüzü sağlamakla gerçekten ilgilenmiyorsunuz. Kendi sözleşmeleriniz için bir kullanıcı arayüzü oluşturmak istiyorsunuz. Kendi uygulamanızı oluşturmak için şu adımları uygulayın:

  1. Bir wagmi uygulaması oluşturmayı belirtin.

    1pnpm create wagmi
  2. Uygulamayı adlandırın.

  3. React çerçevesini seçin.

  4. Vite varyantını seçin.

  5. Rainbow kit ekleyebilirsiniz (opens in a new tab).

Şimdi gidin ve sözleşmelerinizi tüm dünyada kullanılabilir hale getirin.

Çalışmalarımdan daha fazlası için buraya bakın (opens in a new tab).

Sayfanın son güncellenmesi: 3 Mart 2026

Bu rehber yararlı oldu mu?