diff --git a/package.json b/package.json
index 36814104896..dc345340896 100644
--- a/package.json
+++ b/package.json
@@ -83,6 +83,7 @@
"react-lite-youtube-embed": "^2.4.0",
"react-select": "5.8.0",
"reading-time": "^1.5.0",
+ "recharts": "^2.13.3",
"remark-gfm": "^3.0.1",
"swiper": "^11.1.10",
"tailwind-merge": "^2.3.0",
diff --git a/public/content/developers/docs/scaling/optimistic-rollups/index.md b/public/content/developers/docs/scaling/optimistic-rollups/index.md
index 66105d22400..6f63404d13e 100644
--- a/public/content/developers/docs/scaling/optimistic-rollups/index.md
+++ b/public/content/developers/docs/scaling/optimistic-rollups/index.md
@@ -253,12 +253,6 @@ More of a visual learner? Watch Finematics explain optimistic rollups:
-### Use Optimistic rollups {#use-optimistic-rollups}
-
-Multiple implementations of Optimistic rollups exist that you can integrate into your dapps:
-
-
-
## Further reading on optimistic rollups
- [How do optimistic rollups work (The Complete guide)](https://www.alchemy.com/overviews/optimistic-rollups)
diff --git a/public/content/developers/docs/scaling/zk-rollups/index.md b/public/content/developers/docs/scaling/zk-rollups/index.md
index 01f04c472e9..0e0332e408c 100644
--- a/public/content/developers/docs/scaling/zk-rollups/index.md
+++ b/public/content/developers/docs/scaling/zk-rollups/index.md
@@ -222,11 +222,6 @@ Watch Finematics explain ZK-rollups:
-### Use ZK-rollups {#use-zk-rollups}
-
-Multiple implementations of ZK-rollups exist that you can integrate into your dapps:
-
-
## Who is working on a zkEVM? {#zkevm-projects}
diff --git a/public/content/translations/cs/developers/docs/scaling/optimistic-rollups/index.md b/public/content/translations/cs/developers/docs/scaling/optimistic-rollups/index.md
index b23ece70a06..1eec9550cb1 100644
--- a/public/content/translations/cs/developers/docs/scaling/optimistic-rollups/index.md
+++ b/public/content/translations/cs/developers/docs/scaling/optimistic-rollups/index.md
@@ -253,11 +253,6 @@ Učíte se spíše vizuálně? Podívejte se na video od Finematics, které vysv
-### Použití optimistických rollupů {#use-optimistic-rollups}
-
-Existuje několik implementací optimistických rollupů, které můžete integrovat do svých dappek:
-
-
## Další čtení o optimistických rollupech
diff --git a/public/content/translations/cs/developers/docs/scaling/zk-rollups/index.md b/public/content/translations/cs/developers/docs/scaling/zk-rollups/index.md
index 6120233ef79..20d2c3d3ca4 100644
--- a/public/content/translations/cs/developers/docs/scaling/zk-rollups/index.md
+++ b/public/content/translations/cs/developers/docs/scaling/zk-rollups/index.md
@@ -222,11 +222,6 @@ Podívejte se na vysvětlení ZK-rollupů od Finematics:
-### Použití ZK-rollupů {#use-zk-rollups}
-
-Existuje několik implementací ZK-rollupů, které můžete integrovat do svých dappek:
-
-
## Kdo pracuje na zkEVM? {#zkevm-projects}
diff --git a/public/content/translations/de/developers/docs/scaling/optimistic-rollups/index.md b/public/content/translations/de/developers/docs/scaling/optimistic-rollups/index.md
index 1427bbfb8e1..188ba708d95 100644
--- a/public/content/translations/de/developers/docs/scaling/optimistic-rollups/index.md
+++ b/public/content/translations/de/developers/docs/scaling/optimistic-rollups/index.md
@@ -43,12 +43,6 @@ Sehen Sie, wie Finematics optimistische Rollups erklärt:
-### Optimistische Rollups verwenden {#use-optimistic-rollups}
-
-Es gibt mehrere Implementierungen von optimistischen Rollups, die Sie in Ihre dApps integrieren können:
-
-
-
**Optimistische Rollups verstehen**
- [Der Leitfaden zu Arbitrum](https://newsletter.banklesshq.com/p/the-essential-guide-to-arbitrum)
diff --git a/public/content/translations/de/developers/docs/scaling/zk-rollups/index.md b/public/content/translations/de/developers/docs/scaling/zk-rollups/index.md
index 7d5295598e2..48f06c230bd 100644
--- a/public/content/translations/de/developers/docs/scaling/zk-rollups/index.md
+++ b/public/content/translations/de/developers/docs/scaling/zk-rollups/index.md
@@ -34,12 +34,6 @@ Sehen Sie, wie Finematics ZK-Rollups erklärt:
-### ZK-Rollups verwenden {#use-zk-rollups}
-
-Es gibt mehrere Implementierungen von ZK-Rollups, die Sie in Ihre dApps integrieren können:
-
-
-
**ZK-Rollups verstehen**
- [Was sind Zero-Knowledge Rollups?](https://coinmarketcap.com/alexandria/glossary/zero-knowledge-rollups)
diff --git a/public/content/translations/es/developers/docs/scaling/optimistic-rollups/index.md b/public/content/translations/es/developers/docs/scaling/optimistic-rollups/index.md
index 7c0207724a9..73b587c69ae 100644
--- a/public/content/translations/es/developers/docs/scaling/optimistic-rollups/index.md
+++ b/public/content/translations/es/developers/docs/scaling/optimistic-rollups/index.md
@@ -253,12 +253,6 @@ Se espera que la introducción de la [fragmentación de datos](/roadmap/dankshar
-### Uso de rollups optimistas {#use-optimistic-rollups}
-
-Existen múltiples implementaciones de rollups optimistas que puede integrar en sus dApps:
-
-
-
## Bibliografía para profundizar sobre los rollups optimistas
- [¿Cómo funcionan los rollups optimistas? (La guía completa)](https://www.alchemy.com/overviews/optimistic-rollups)
diff --git a/public/content/translations/es/developers/docs/scaling/zk-rollups/index.md b/public/content/translations/es/developers/docs/scaling/zk-rollups/index.md
index e7125456dfa..ded2f19fb86 100644
--- a/public/content/translations/es/developers/docs/scaling/zk-rollups/index.md
+++ b/public/content/translations/es/developers/docs/scaling/zk-rollups/index.md
@@ -222,12 +222,6 @@ Vea una explicación de Finematics de los ZK-rollups:
-### Utilice los ZK-rollups {#use-zk-rollups}
-
-Existen múltiples implementaciones de ZK-rollups que pueden integrarse en sus dApps:
-
-
-
## ¿Quién está trabajando en un zkEVM? {#zkevm-projects}
Los proyectos que trabajan en zkEVM incluyen:
diff --git a/public/content/translations/fr/developers/docs/scaling/optimistic-rollups/index.md b/public/content/translations/fr/developers/docs/scaling/optimistic-rollups/index.md
index 35685d4f3f2..83c617fe414 100644
--- a/public/content/translations/fr/developers/docs/scaling/optimistic-rollups/index.md
+++ b/public/content/translations/fr/developers/docs/scaling/optimistic-rollups/index.md
@@ -253,12 +253,6 @@ Davantage qu'un apprenant visuel ? Regardez Finematics expliquer les rollups opt
-### Utiliser des rollups optimistes {#use-optimistic-rollups}
-
-Plusieurs implémentations de rollups optimistes existent, que vous pouvez intégrer dans vos dApps :
-
-
-
## Autres lectures sur les rollups optimistes
- [Comment fonctionnent les rollups optimistes (Le guide complet)](https://www.alchemy.com/overviews/optimistic-rollups)
diff --git a/public/content/translations/fr/developers/docs/scaling/zk-rollups/index.md b/public/content/translations/fr/developers/docs/scaling/zk-rollups/index.md
index f146a1efd0d..b12f2afd10c 100644
--- a/public/content/translations/fr/developers/docs/scaling/zk-rollups/index.md
+++ b/public/content/translations/fr/developers/docs/scaling/zk-rollups/index.md
@@ -222,12 +222,6 @@ Regardez la vidéo de Finematics qui explique les rollups ZK :
-### Utiliser les rollups ZK {#use-zk-rollups}
-
-Il existe un grand nombre d'implémentations de rollups ZK que vous pouvez intégrer dans vos dApps :
-
-
-
## Qui travaille sur une zkEVM ? {#zkevm-projects}
Les projets fonctionnant sur les zkEVM comprennent :
diff --git a/public/content/translations/hu/developers/docs/scaling/optimistic-rollups/index.md b/public/content/translations/hu/developers/docs/scaling/optimistic-rollups/index.md
index 522182424e0..441912d26df 100644
--- a/public/content/translations/hu/developers/docs/scaling/optimistic-rollups/index.md
+++ b/public/content/translations/hu/developers/docs/scaling/optimistic-rollups/index.md
@@ -253,12 +253,6 @@ Az [adat sharding](/roadmap/danksharding/) bevezetése az Ethereumban várhatóa
-### Optimista összevont tranzakciók használata {#use-optimistic-rollups}
-
-Az optimista összevont tranzakcióknak többféle megvalósítása létezik, amelyeket integrálhat a dappjaiba:
-
-
-
## További információk az optimista összevont tranzakciókról
- [Hogyan működnek az optimista összevont tranzakciók (teljes útmutató)](https://www.alchemy.com/overviews/optimistic-rollups)
diff --git a/public/content/translations/hu/developers/docs/scaling/zk-rollups/index.md b/public/content/translations/hu/developers/docs/scaling/zk-rollups/index.md
index 4caff0bbfd0..9e95a34dc42 100644
--- a/public/content/translations/hu/developers/docs/scaling/zk-rollups/index.md
+++ b/public/content/translations/hu/developers/docs/scaling/zk-rollups/index.md
@@ -222,12 +222,6 @@ Nézze meg a videót, melyben a Finematics elmagyarázza a ZK összevont tranzak
-### ZK összevont tranzakciók használata {#use-zk-rollups}
-
-A ZK összevont tranzakcióknak többféle megvalósítása létezik, amelyeket integrálhat a dappjaiba:
-
-
-
## Kik dolgoznak zkEVM-en? {#zkevm-projects}
Többek között az alábbi projektek dolgoznak zkEVM-en:
diff --git a/public/content/translations/it/developers/docs/scaling/optimistic-rollups/index.md b/public/content/translations/it/developers/docs/scaling/optimistic-rollups/index.md
index 4c90a2e9f40..a1f130cbcc4 100644
--- a/public/content/translations/it/developers/docs/scaling/optimistic-rollups/index.md
+++ b/public/content/translations/it/developers/docs/scaling/optimistic-rollups/index.md
@@ -253,12 +253,6 @@ Preferisci un approccio visivo all'apprendimento? Guarda Finematics spiegare i r
-### Utilizzo dei rollup ottimistici {#use-optimistic-rollups}
-
-Esistono molteplici implementazioni dei rollup ottimistici, che puoi integrare nelle tue dapp:
-
-
-
## Ulteriori letture sui rollup ottimistici
- [Come funzionano gli Optimistic Rollup (La guida completa)](https://www.alchemy.com/overviews/optimistic-rollups)
diff --git a/public/content/translations/it/developers/docs/scaling/zk-rollups/index.md b/public/content/translations/it/developers/docs/scaling/zk-rollups/index.md
index 5d883acfd10..c80610d5c15 100644
--- a/public/content/translations/it/developers/docs/scaling/zk-rollups/index.md
+++ b/public/content/translations/it/developers/docs/scaling/zk-rollups/index.md
@@ -222,12 +222,6 @@ Guarda Finematics spiegare i rollup ZK:
-### Utilizzo dei rollup ZK {#use-zk-rollups}
-
-Esistono molteplici implementazioni dei rollup ZK che puoi integrare nelle tue dapp:
-
-
-
## Chi sta lavorando a una zkEVM? {#zkevm-projects}
I progetti che stanno lavorando alle zkEVM includono:
diff --git a/public/content/translations/ja/developers/docs/scaling/optimistic-rollups/index.md b/public/content/translations/ja/developers/docs/scaling/optimistic-rollups/index.md
index 7ff79449457..ecce9937e82 100644
--- a/public/content/translations/ja/developers/docs/scaling/optimistic-rollups/index.md
+++ b/public/content/translations/ja/developers/docs/scaling/optimistic-rollups/index.md
@@ -253,12 +253,6 @@ ii. オプティミスティック・ロールアップを使用するデベロ
-### オプティミスティック・ロールアップの使用方法 {#use-optimistic-rollups}
-
-Dappに統合可能な、既存のオプティミスティック・ロールアップの実装が提供されています:
-
-
-
## オプティミスティック・ロールアップに関する参考文献
- [オプティミスティック・ロールアップの仕組み(完全ガイド)](https://www.alchemy.com/overviews/optimistic-rollups)
diff --git a/public/content/translations/ja/developers/docs/scaling/zk-rollups/index.md b/public/content/translations/ja/developers/docs/scaling/zk-rollups/index.md
index 120c81f5396..5011a270aaf 100644
--- a/public/content/translations/ja/developers/docs/scaling/zk-rollups/index.md
+++ b/public/content/translations/ja/developers/docs/scaling/zk-rollups/index.md
@@ -222,12 +222,6 @@ FinematicsによるZKロールアップの説明動画をご覧ください:
-### ゼロ知識ロールアップの活用方法 {#use-zk-rollups}
-
-現在、Dappに統合可能ないくつかのZKロールアップの実装が提供されています:
-
-
-
## zkEVMの開発プロジェクト {#zkevm-projects}
現在、zkEVMの開発に取り組んでいるプロジェクトとしては、以下が挙げられます:
diff --git a/public/content/translations/pt-br/developers/docs/scaling/optimistic-rollups/index.md b/public/content/translations/pt-br/developers/docs/scaling/optimistic-rollups/index.md
index 212d611ba53..c4f12acb624 100644
--- a/public/content/translations/pt-br/developers/docs/scaling/optimistic-rollups/index.md
+++ b/public/content/translations/pt-br/developers/docs/scaling/optimistic-rollups/index.md
@@ -253,12 +253,6 @@ Você é o tipo de pessoa que aprende mais com recursos visuais? Assista aos Fin
-### Usar optimistic rollups {#use-optimistic-rollups}
-
-Há múltiplas implementações de optimistic rollups que você pode integrar aos seus dapps:
-
-
-
## Leitura adicional sobre optimistic rollups
- [Como funcionam os optimistic rollups (o guia completo)](https://www.alchemy.com/overviews/optimistic-rollups)
diff --git a/public/content/translations/pt-br/developers/docs/scaling/zk-rollups/index.md b/public/content/translations/pt-br/developers/docs/scaling/zk-rollups/index.md
index 9b1cff5b71b..d63c9e7b62d 100644
--- a/public/content/translations/pt-br/developers/docs/scaling/zk-rollups/index.md
+++ b/public/content/translations/pt-br/developers/docs/scaling/zk-rollups/index.md
@@ -222,12 +222,6 @@ Assista ao Finematics explicando ZK-rollups:
-### Usar ZK-rollups {#use-zk-rollups}
-
-Existem várias implementações de ZK-rollups que você pode integrar aos seus dapps:
-
-
-
## Quem está trabalhando em zkEVMs? {#zkevm-projects}
Os projetos que trabalham em zkEVMs incluem:
diff --git a/public/content/translations/ro/developers/docs/scaling/optimistic-rollups/index.md b/public/content/translations/ro/developers/docs/scaling/optimistic-rollups/index.md
index 158c1cb1c23..cdd9c52cfd0 100644
--- a/public/content/translations/ro/developers/docs/scaling/optimistic-rollups/index.md
+++ b/public/content/translations/ro/developers/docs/scaling/optimistic-rollups/index.md
@@ -41,12 +41,6 @@ Urmăriți cum explică Finematics rollup-urile optimistic:
-### Utilizarea rollup-urilor Optimistic {#use-optimistic-rollups}
-
-Există mai multe implementări ale rollup-urilor Optimistic pe care le puteți integra în aplicațiile dvs. descentralizate:
-
-
-
**Optimistic rollups reading**
- [Ghidul esențial pentru Arbitrum](https://newsletter.banklesshq.com/p/the-essential-guide-to-arbitrum)
diff --git a/public/content/translations/ro/developers/docs/scaling/zk-rollups/index.md b/public/content/translations/ro/developers/docs/scaling/zk-rollups/index.md
index 9882296f195..f51aaa24332 100644
--- a/public/content/translations/ro/developers/docs/scaling/zk-rollups/index.md
+++ b/public/content/translations/ro/developers/docs/scaling/zk-rollups/index.md
@@ -32,12 +32,6 @@ Urmăriți Finematics explicând ZK-rollup-urile:
-### Utilizarea ZK-rollup-urilor {#use-zk-rollups}
-
-Există numeroase implementări ale ZK-rollup-urilor pe care le puteți integra în aplicațiile dvs. descentralizate:
-
-
-
**ZK-rollups reading**
- [Ce sunt rollup-urile Zero-Knowledge?](https://coinmarketcap.com/alexandria/glossary/zero-knowledge-rollups)
diff --git a/public/content/translations/sl/developers/docs/scaling/optimistic-rollups/index.md b/public/content/translations/sl/developers/docs/scaling/optimistic-rollups/index.md
index 7ccc0d50c04..cee5f52c95d 100644
--- a/public/content/translations/sl/developers/docs/scaling/optimistic-rollups/index.md
+++ b/public/content/translations/sl/developers/docs/scaling/optimistic-rollups/index.md
@@ -43,12 +43,6 @@ Oglejte si, kako Finematics razložijo optimistične zvitke:
-### Uporaba optimističnih zvitkov {#use-optimistic-rollups}
-
-Obstaja več implementacij optimističnih zvitkov, ki jih lahko integrirate v svoje dappe:
-
-
-
**Gradivo za branje o optimističnih zvitkih**
- [Ključni vodnik za Arbitrum](https://newsletter.banklesshq.com/p/the-essential-guide-to-arbitrum)
diff --git a/public/content/translations/sl/developers/docs/scaling/zk-rollups/index.md b/public/content/translations/sl/developers/docs/scaling/zk-rollups/index.md
index 116235d2341..d1e49b1dc12 100644
--- a/public/content/translations/sl/developers/docs/scaling/zk-rollups/index.md
+++ b/public/content/translations/sl/developers/docs/scaling/zk-rollups/index.md
@@ -34,12 +34,6 @@ Oglejte si, kako pri Finematics razložijo ZK-zvitke:
-### Uporaba ZK-zvitkov {#use-zk-rollups}
-
-Obstaja več implementacij ZK-zvitkov, ki jih lahko integrirate v svoje dappe:
-
-
-
**Materiali za branje o ZK-zvitkih**
- [Kaj so zvitki brez znanja?](https://coinmarketcap.com/alexandria/glossary/zero-knowledge-rollups)
diff --git a/public/content/translations/tr/developers/docs/scaling/optimistic-rollups/index.md b/public/content/translations/tr/developers/docs/scaling/optimistic-rollups/index.md
index ed94bbf22f5..203414ae18a 100644
--- a/public/content/translations/tr/developers/docs/scaling/optimistic-rollups/index.md
+++ b/public/content/translations/tr/developers/docs/scaling/optimistic-rollups/index.md
@@ -253,12 +253,6 @@ Görerek öğrenmeyi mi tercih ediyorsunuz? Finematics'in iyimser toplamalar hak
-### İyimser toplamaları kullanın {#use-optimistic-rollups}
-
-Merkeziyetsiz uygulamalarınıza entegre edebileceğiniz birden çok İyimser toplama uygulaması mevcuttur:
-
-
-
## İyimser toplamalara dair daha fazlası
- [İyimser toplamalar nasıl çalışır? (Tam klavuz)](https://www.alchemy.com/overviews/optimistic-rollups)
diff --git a/public/content/translations/tr/developers/docs/scaling/zk-rollups/index.md b/public/content/translations/tr/developers/docs/scaling/zk-rollups/index.md
index a87a18d2887..6b6e32e4ccd 100644
--- a/public/content/translations/tr/developers/docs/scaling/zk-rollups/index.md
+++ b/public/content/translations/tr/developers/docs/scaling/zk-rollups/index.md
@@ -222,12 +222,6 @@ Finematics'in ZK-toplaması hakkındaki açıklamasını izleyin:
-### ZK toplamalarını kullanın {#use-zk-rollups}
-
-Merkeziyetsiz uygulamalarınıza entegre edebileceğiniz birden çok ZK toplamaları uygulaması mevcuttur:
-
-
-
## zkEVM üzerinde kimler çalışıyor? {#zkevm-projects}
Şunlar zkEVM'ler üzerinde çalışan projeler arasındadır:
diff --git a/public/content/translations/zh-tw/developers/docs/scaling/optimistic-rollups/index.md b/public/content/translations/zh-tw/developers/docs/scaling/optimistic-rollups/index.md
index c547ae35aa8..7ee79f6b04f 100644
--- a/public/content/translations/zh-tw/developers/docs/scaling/optimistic-rollups/index.md
+++ b/public/content/translations/zh-tw/developers/docs/scaling/optimistic-rollups/index.md
@@ -253,12 +253,6 @@ ii. 使用樂觀卷軸的開發者和專案團隊可以利用以太坊的基礎
-### 使用樂觀卷軸 {#use-optimistic-rollups}
-
-樂觀卷軸存在多種實作,可供你整合到去中心化應用程式:
-
-
-
## 有關樂觀卷軸的延伸閲讀
- [樂觀卷軸如何運作(完整指引)](https://www.alchemy.com/overviews/optimistic-rollups)
diff --git a/public/content/translations/zh-tw/developers/docs/scaling/zk-rollups/index.md b/public/content/translations/zh-tw/developers/docs/scaling/zk-rollups/index.md
index 1799f2f5b78..bec5f900cd9 100644
--- a/public/content/translations/zh-tw/developers/docs/scaling/zk-rollups/index.md
+++ b/public/content/translations/zh-tw/developers/docs/scaling/zk-rollups/index.md
@@ -222,12 +222,6 @@ ZK-STARK 對於量子電腦也是安全的,而 ZK-SNARK 中使用的橢圓曲
-### 使用零知識證明卷軸 {#use-zk-rollups}
-
-零知識證明卷軸存在多種實作,可供你整合到去中心化應用程式:
-
-
-
## 零知識以太坊虛擬機上有哪些項目? {#zkevm-projects}
零知識以太坊虛擬機上運作的專案包括:
diff --git a/public/content/translations/zh/developers/docs/scaling/optimistic-rollups/index.md b/public/content/translations/zh/developers/docs/scaling/optimistic-rollups/index.md
index b6f72deca75..8647f011d6e 100644
--- a/public/content/translations/zh/developers/docs/scaling/optimistic-rollups/index.md
+++ b/public/content/translations/zh/developers/docs/scaling/optimistic-rollups/index.md
@@ -253,12 +253,6 @@ ii. 使用乐观卷叠的开发者和项目团队可以利用以太坊的基础
-### 使用乐观卷叠 {#use-optimistic-rollups}
-
-乐观重叠有多种实现,你可以将其整合到你的去中心化应用程序中:
-
-
-
## 阅读关于乐观卷叠的更多信息
- [乐观卷叠如何工作(完整指南)](https://www.alchemy.com/overviews/optimistic-rollups)
diff --git a/public/content/translations/zh/developers/docs/scaling/zk-rollups/index.md b/public/content/translations/zh/developers/docs/scaling/zk-rollups/index.md
index 32d806db688..51c6265df38 100644
--- a/public/content/translations/zh/developers/docs/scaling/zk-rollups/index.md
+++ b/public/content/translations/zh/developers/docs/scaling/zk-rollups/index.md
@@ -222,12 +222,6 @@ ZK-STARK 对于量子计算机也是安全的,而 ZK-SNARK 中使用的椭圆
-### 使用零知识卷叠 {#use-zk-rollups}
-
-零知识卷叠有多种实现方式,你可以将其整合到自己的去中心化应用程序中:
-
-
-
## 零知识以太坊虚拟机上有哪些项目? {#zkevm-projects}
零知识以太坊虚拟机上运行的项目包括:
diff --git a/public/images/layer-2/arbitrum.jpg b/public/images/layer-2/arbitrum.jpg
new file mode 100644
index 00000000000..6b290757e8b
Binary files /dev/null and b/public/images/layer-2/arbitrum.jpg differ
diff --git a/public/images/layer-2/aztec.png b/public/images/layer-2/aztec.png
deleted file mode 100644
index 949c6bb9221..00000000000
Binary files a/public/images/layer-2/aztec.png and /dev/null differ
diff --git a/public/images/layer-2/blast.png b/public/images/layer-2/blast.png
new file mode 100644
index 00000000000..40f86848b06
Binary files /dev/null and b/public/images/layer-2/blast.png differ
diff --git a/public/images/layer-2/boba.png b/public/images/layer-2/boba.png
deleted file mode 100644
index 22ea33930fd..00000000000
Binary files a/public/images/layer-2/boba.png and /dev/null differ
diff --git a/public/images/layer-2/dydx.png b/public/images/layer-2/dydx.png
deleted file mode 100644
index 9cc0406017e..00000000000
Binary files a/public/images/layer-2/dydx.png and /dev/null differ
diff --git a/public/images/layer-2/ethereum.png b/public/images/layer-2/ethereum.png
new file mode 100644
index 00000000000..bd45c2a1daf
Binary files /dev/null and b/public/images/layer-2/ethereum.png differ
diff --git a/public/images/layer-2/ethereumecosystem.png b/public/images/layer-2/ethereumecosystem.png
deleted file mode 100644
index ab64df748d3..00000000000
Binary files a/public/images/layer-2/ethereumecosystem.png and /dev/null differ
diff --git a/public/images/layer-2/hero.png b/public/images/layer-2/hero.png
deleted file mode 100644
index c3a165d415b..00000000000
Binary files a/public/images/layer-2/hero.png and /dev/null differ
diff --git a/public/images/layer-2/layer-2-walking.png b/public/images/layer-2/layer-2-walking.png
new file mode 100644
index 00000000000..698f5c7a128
Binary files /dev/null and b/public/images/layer-2/layer-2-walking.png differ
diff --git a/public/images/layer-2/learn-hero.png b/public/images/layer-2/learn-hero.png
new file mode 100644
index 00000000000..d413549923e
Binary files /dev/null and b/public/images/layer-2/learn-hero.png differ
diff --git a/public/images/layer-2/linea.png b/public/images/layer-2/linea.png
new file mode 100644
index 00000000000..2f7f68b97f0
Binary files /dev/null and b/public/images/layer-2/linea.png differ
diff --git a/public/images/layer-2/loopring.png b/public/images/layer-2/loopring.png
deleted file mode 100644
index 7a35c3e3c68..00000000000
Binary files a/public/images/layer-2/loopring.png and /dev/null differ
diff --git a/public/images/layer-2/metis-dark.png b/public/images/layer-2/metis-dark.png
deleted file mode 100644
index 6ad3f1b7dfb..00000000000
Binary files a/public/images/layer-2/metis-dark.png and /dev/null differ
diff --git a/public/images/layer-2/metis-light.png b/public/images/layer-2/metis-light.png
deleted file mode 100644
index 2cb6972b87b..00000000000
Binary files a/public/images/layer-2/metis-light.png and /dev/null differ
diff --git a/public/images/layer-2/mode.png b/public/images/layer-2/mode.png
new file mode 100644
index 00000000000..b67791df3f7
Binary files /dev/null and b/public/images/layer-2/mode.png differ
diff --git a/public/images/layer-2/scroll.png b/public/images/layer-2/scroll.png
new file mode 100644
index 00000000000..87d9c76e16b
Binary files /dev/null and b/public/images/layer-2/scroll.png differ
diff --git a/public/images/layer-2/zkspace.png b/public/images/layer-2/zkspace.png
deleted file mode 100644
index 842887e18ac..00000000000
Binary files a/public/images/layer-2/zkspace.png and /dev/null differ
diff --git a/public/images/layer-2/zksync.png b/public/images/layer-2/zksync.png
deleted file mode 100644
index af1aa6febe1..00000000000
Binary files a/public/images/layer-2/zksync.png and /dev/null differ
diff --git a/public/images/layer-2/zksyncEra.jpg b/public/images/layer-2/zksyncEra.jpg
new file mode 100644
index 00000000000..a939234c768
Binary files /dev/null and b/public/images/layer-2/zksyncEra.jpg differ
diff --git a/src/components/Callout.tsx b/src/components/Callout.tsx
index ee4c6e397bd..3834cd6f0b1 100644
--- a/src/components/Callout.tsx
+++ b/src/components/Callout.tsx
@@ -1,21 +1,23 @@
import { useTranslation } from "next-i18next"
-import { Center, Flex, type FlexProps } from "@chakra-ui/react"
import type { TranslationKey } from "@/lib/types"
import Emoji from "@/components/Emoji"
-import { Image, type ImageProps } from "@/components/Image"
-import OldHeading from "@/components/OldHeading"
-import Text from "@/components/OldText"
+import { type ImageProps, TwImage } from "@/components/Image"
-export type CalloutProps = FlexProps & {
+import { cn } from "@/lib/utils/cn"
+
+export type CalloutProps = {
children?: React.ReactNode
image?: ImageProps["src"]
emoji?: string
alt?: string
- titleKey: TranslationKey
- descriptionKey: TranslationKey
+ titleKey?: TranslationKey
+ descriptionKey?: TranslationKey
+ title?: string
+ description?: string
className?: string
+ headerClassName?: string
}
const Callout = ({
@@ -24,48 +26,67 @@ const Callout = ({
alt,
titleKey,
descriptionKey,
+ title,
+ description,
children,
className,
- ...rest
+ headerClassName,
}: CalloutProps) => {
const { t } = useTranslation("common")
return (
-
{image && (
-
-
-
+
+
+
)}
-
+
{emoji &&
}
-
- {t(titleKey)}
-
-
- {t(descriptionKey)}
-
+ {titleKey && (
+
+ {t(titleKey)}
+
+ )}
+ {title && (
+
+ {title}
+
+ )}
+ {descriptionKey && (
+
+ {t(descriptionKey)}
+
+ )}
+ {description && (
+
+ {description}
+
+ )}
{children}
-
-
+
+
)
}
diff --git a/src/components/DataTable/index.tsx b/src/components/DataTable/index.tsx
index d8ee7490d14..38a0bcc47ba 100644
--- a/src/components/DataTable/index.tsx
+++ b/src/components/DataTable/index.tsx
@@ -1,4 +1,4 @@
-import { Fragment, useEffect, useRef, useState } from "react"
+import { FC, Fragment, useEffect, useRef, useState } from "react"
import {
ColumnDef,
flexRender,
@@ -16,11 +16,26 @@ import {
TableRow,
} from "@/components/ui/Table"
+import { trackCustomEvent } from "@/lib/utils/matomo"
+
type DataTableProps = TableProps & {
columns: ColumnDef[]
data: TData[]
- subComponent?: React.FC
- noResultsComponent?: React.FC
+ subComponent?: FC
+ noResultsComponent?: FC
+ allDataLength: number
+ setMobileFiltersOpen?: (open: boolean) => void
+ activeFiltersCount: number
+ meta?: Record
+ matomoEventCategory: string
+}
+
+export type TableMeta = {
+ setMobileFiltersOpen: (open: boolean) => void
+ allDataLength: number
+ dataLength: number
+ activeFiltersCount: number
+ [key: string]: string | number | ((open: boolean) => void)
}
const DataTable = ({
@@ -28,25 +43,68 @@ const DataTable = ({
data,
subComponent,
noResultsComponent,
+ allDataLength,
+ setMobileFiltersOpen,
+ activeFiltersCount,
+ meta,
+ matomoEventCategory,
...props
}: DataTableProps) => {
const [isVisible, setIsVisible] = useState(true)
const [currentData, setCurrentData] = useState(data)
+ const [expanded, setExpanded] = useState({})
+ const previousExpandedRef = useRef({})
const previousDataRef = useRef(data)
const table = useReactTable({
data: currentData,
columns,
- getRowCanExpand: () => true,
+ state: {
+ expanded,
+ },
+ onExpandedChange: setExpanded,
+ getRowCanExpand: (row) => {
+ const rowData = row.original as { canExpand?: boolean }
+ return rowData.canExpand !== undefined ? rowData.canExpand : true
+ },
getCoreRowModel: getCoreRowModel(),
getExpandedRowModel: getExpandedRowModel(),
+ meta: {
+ ...meta,
+ allDataLength,
+ dataLength: data.length,
+ setMobileFiltersOpen,
+ activeFiltersCount,
+ } as TableMeta,
})
+ useEffect(() => {
+ const prev = previousExpandedRef.current
+ const current = expanded
+
+ // Find newly expanded rows
+ const newlyExpanded = Object.entries(current)
+ .filter(([key, value]) => value === true && prev[key] !== true)
+ .map(([key]) => key)
+
+ if (newlyExpanded.length > 0) {
+ const row = table.getRowModel().rowsById[newlyExpanded[0]]
+ trackCustomEvent({
+ eventCategory: matomoEventCategory,
+ eventAction: "expanded",
+ eventName: (row.original as { name: string }).name,
+ })
+ }
+
+ previousExpandedRef.current = expanded
+ }, [expanded])
+
useEffect(() => {
if (JSON.stringify(data) !== JSON.stringify(previousDataRef.current)) {
setIsVisible(false)
const timer = setTimeout(() => {
setCurrentData(data)
+ table.resetExpanded()
setIsVisible(true)
previousDataRef.current = data
}, 25) // Adjust this value to match your CSS transition duration
@@ -56,67 +114,74 @@ const DataTable = ({
}, [data])
return (
-
-
- {table.getHeaderGroups().map((headerGroup) => (
-
- {headerGroup.headers.map((header) => {
- return (
-
- {header.isPlaceholder
- ? null
- : flexRender(
- header.column.columnDef.header,
- header.getContext()
- )}
-
- )
- })}
-
- ))}
-
-
- {table.getRowModel().rows?.length ? (
- table.getRowModel().rows.map((row, idx) => (
-
- {
- // Prevent expanding the wallet more info section when clicking on the "Visit website" button
- if (!(e.target as Element).matches("a, a svg")) {
- row.getToggleExpandedHandler()()
- }
- }}
- >
- {row.getVisibleCells().map((cell) => (
-
- {flexRender(cell.column.columnDef.cell, cell.getContext())}
-
+
+
+
+
+ {table.getHeaderGroups().map((headerGroup) => (
+
+ {headerGroup.headers.map((header) => (
+
+ {header.isPlaceholder
+ ? null
+ : flexRender(
+ header.column.columnDef.header,
+ header.getContext()
+ )}
+
))}
- {row.getIsExpanded() && (
-
-
- {subComponent && subComponent(row.original, idx)}
-
+ ))}
+
+
+
+
+
+ {table.getRowModel().rows?.length ? (
+ table.getRowModel().rows.map((row, idx) => (
+
+ {
+ // Prevent expanding the wallet more info section when clicking on the "Visit website" button
+ if (!(e.target as Element).matches("a, a svg")) {
+ row.getToggleExpandedHandler()()
+ }
+ }}
+ >
+ {row.getVisibleCells().map((cell) => (
+
+ {flexRender(
+ cell.column.columnDef.cell,
+ cell.getContext()
+ )}
+
+ ))}
- )}
-
- ))
- ) : (
-
-
- {noResultsComponent && noResultsComponent({})}
-
-
- )}
-
-
+ {row.getIsExpanded() && (
+
+
+ {subComponent && subComponent(row.original, idx)}
+
+
+ )}
+
+ ))
+ ) : (
+
+
+ {noResultsComponent && noResultsComponent({})}
+
+
+ )}
+
+
+
)
}
diff --git a/src/components/ExpandableCard.tsx b/src/components/ExpandableCard.tsx
index 3bcddccdf72..acbaf13f212 100644
--- a/src/components/ExpandableCard.tsx
+++ b/src/components/ExpandableCard.tsx
@@ -22,6 +22,7 @@ export type ExpandableCardProps = {
eventAction?: string
eventCategory?: string
eventName?: string
+ visible?: boolean
}
const ExpandableCard = ({
@@ -32,8 +33,9 @@ const ExpandableCard = ({
eventAction = "Clicked",
eventCategory = "",
eventName = "",
+ visible = false,
}: ExpandableCardProps) => {
- const [isVisible, setIsVisible] = useState(false)
+ const [isVisible, setIsVisible] = useState(visible)
const { t } = useTranslation("common")
const matomo = {
eventAction,
@@ -55,7 +57,12 @@ const ExpandableCard = ({
return (
<>
-
+
export const WalletProductTableStory: Story = {
args: {
wallets: walletsData.map((wallet) => {
- return { ...wallet, supportedLanguages: wallet.languages_supported }
+ return {
+ ...wallet,
+ languages_supported: wallet.languages_supported as Lang[],
+ supportedLanguages: wallet.languages_supported as Lang[],
+ supported_chains: [],
+ }
}),
},
render: (args) => {
diff --git a/src/components/FindWalletProductTable/Layer2SelectInput.tsx b/src/components/FindWalletProductTable/Layer2SelectInput.tsx
new file mode 100644
index 00000000000..2eab8baa827
--- /dev/null
+++ b/src/components/FindWalletProductTable/Layer2SelectInput.tsx
@@ -0,0 +1,57 @@
+import { FilterInputState } from "@/lib/types"
+
+import CheckboxFilterInput from "@/components/ProductTable/FilterInputs/CheckboxFilterInput"
+
+import { layer2Data } from "@/data/networks/networks"
+
+interface Layer2SelectInputProps {
+ filterIndex: number
+ itemIndex: number
+ inputState: FilterInputState
+ updateFilterState: (
+ filterIndex: number,
+ itemIndex: number,
+ newInputState: FilterInputState
+ ) => void
+}
+
+const Layer2SelectInput = ({
+ filterIndex,
+ itemIndex,
+ inputState,
+ updateFilterState,
+}: Layer2SelectInputProps) => {
+ return (
+
+ {layer2Data
+ .sort((a, b) => a.name.localeCompare(b.name))
+ .map((network) => (
+ {
+ if (newInputState) {
+ updateFilterState(filterIndex, itemIndex, [
+ ...(inputState as string[]),
+ network.chainName,
+ ])
+ } else {
+ updateFilterState(
+ filterIndex,
+ itemIndex,
+ (inputState as string[]).filter(
+ (name) => name !== network.chainName
+ )
+ )
+ }
+ }}
+ />
+ ))}
+
+ )
+}
+
+export default Layer2SelectInput
diff --git a/src/components/FindWalletProductTable/WalletInfo.tsx b/src/components/FindWalletProductTable/WalletInfo.tsx
index 6791da85ae9..481cb7597a6 100644
--- a/src/components/FindWalletProductTable/WalletInfo.tsx
+++ b/src/components/FindWalletProductTable/WalletInfo.tsx
@@ -7,10 +7,13 @@ import { ButtonLink } from "@/components/Buttons"
import { SupportedLanguagesTooltip } from "@/components/FindWalletProductTable/SupportedLanguagesTooltip"
import { DevicesIcon, LanguagesIcon } from "@/components/icons/wallets"
import { TwImage } from "@/components/Image"
+import Tooltip from "@/components/Tooltip"
import { Badge } from "@/components/ui/badge"
import { formatStringList, getWalletPersonas } from "@/lib/utils/wallets"
+import { ethereumNetworkData, layer2Data } from "@/data/networks/networks"
+
interface WalletInfoProps {
wallet: Wallet
isExpanded: boolean
@@ -51,6 +54,36 @@ const WalletInfo = ({ wallet, isExpanded }: WalletInfoProps) => {
))}
)}
+
+ {wallet.supported_chains.map((chain) => {
+ const chainData = [ethereumNetworkData, ...layer2Data].find(
+ (l2) => l2.chainName === chain
+ )
+ return (
+
+
+
+
+
+ )
+ })}
+
@@ -73,9 +106,39 @@ const WalletInfo = ({ wallet, isExpanded }: WalletInfoProps) => {
)}
+
+ {wallet.supported_chains.map((chain) => {
+ const chainData = [ethereumNetworkData, ...layer2Data].find(
+ (l2) => l2.chainName === chain
+ )
+ return (
+
+
+
+
+
+ )
+ })}
+
-
+
@@ -112,7 +175,7 @@ const WalletInfo = ({ wallet, isExpanded }: WalletInfoProps) => {
-
+
diff --git a/src/components/FindWalletProductTable/hooks/useWalletColumns.tsx b/src/components/FindWalletProductTable/hooks/useWalletColumns.tsx
index 7f5c3655af5..1ba454555ba 100644
--- a/src/components/FindWalletProductTable/hooks/useWalletColumns.tsx
+++ b/src/components/FindWalletProductTable/hooks/useWalletColumns.tsx
@@ -4,23 +4,56 @@ import { ColumnDef } from "@tanstack/react-table"
import { Wallet } from "@/lib/types"
+import type { TableMeta } from "@/components/DataTable"
import WalletInfo from "@/components/FindWalletProductTable/WalletInfo"
-import { TableHead } from "@/components/ui/Table"
+import { Button } from "@/components/ui/buttons/Button"
+import { TableCell } from "@/components/ui/Table"
-// This type is used to define the shape of our data.
-// You can use a Zod schema here if you want.
-export type WalletColumns = {
- id: string
- walletInfo: Wallet
-}
+import { trackCustomEvent } from "@/lib/utils/matomo"
export const useWalletColumns: ColumnDef
[] = [
{
id: "walletInfo",
- header: () => ,
+ header: ({ table }) => {
+ const meta = table.options.meta as TableMeta
+
+ return (
+
+
{
+ trackCustomEvent({
+ eventCategory: "MobileFilterToggle",
+ eventAction: "Tap MobileFilterToggle - sticky",
+ eventName: "show mobile filters true",
+ })
+ meta.setMobileFiltersOpen(true)
+ }}
+ >
+ {`Filters (${meta.activeFiltersCount})`}
+
+ {meta.dataLength === meta.allDataLength ? (
+
+ Showing all wallets ({meta.dataLength})
+
+ ) : (
+
+ Showing{" "}
+
+ {meta.dataLength}/{meta.allDataLength}
+ {" "}
+ wallets
+
+ )}
+
+ )
+ },
cell: ({ row }) => {
return (
-
+
+
+
)
},
},
diff --git a/src/components/FindWalletProductTable/hooks/useWalletFilters.tsx b/src/components/FindWalletProductTable/hooks/useWalletFilters.tsx
index 9fe7dd485e1..dfc292df1fb 100644
--- a/src/components/FindWalletProductTable/hooks/useWalletFilters.tsx
+++ b/src/components/FindWalletProductTable/hooks/useWalletFilters.tsx
@@ -3,6 +3,7 @@ import { useTranslation } from "next-i18next"
import { FilterOption } from "@/lib/types"
import FindWalletLanguageSelectInput from "@/components/FindWalletProductTable/FindWalletLanguageSelectInput"
+import Layer2SelectInput from "@/components/FindWalletProductTable/Layer2SelectInput"
import {
BrowserIcon,
BuyCryptoIcon,
@@ -435,6 +436,30 @@ export const useWalletFilters = (): FilterOption[] => {
},
],
},
+ {
+ title: "Network support",
+ showFilterOption: true,
+ items: [
+ {
+ filterKey: "layer_2_support",
+ filterLabel: "layer_2_support",
+ description: "",
+ inputState: [],
+ ignoreFilterReset: false,
+ input: (filterIndex, itemIndex, inputState, updateFilterState) => {
+ return (
+
+ )
+ },
+ options: [],
+ },
+ ],
+ },
{
title: t("page-find-wallet-languages-supported"),
showFilterOption: true,
diff --git a/src/components/FindWalletProductTable/index.tsx b/src/components/FindWalletProductTable/index.tsx
index b0389658503..97fb0546a43 100644
--- a/src/components/FindWalletProductTable/index.tsx
+++ b/src/components/FindWalletProductTable/index.tsx
@@ -1,7 +1,7 @@
-import { useMemo, useState } from "react"
+import { useEffect, useMemo, useState } from "react"
import { useTranslation } from "next-i18next"
-import { FilterOption } from "@/lib/types"
+import { ChainName, FilterOption, Lang, Wallet } from "@/lib/types"
import { useWalletColumns } from "@/components/FindWalletProductTable/hooks/useWalletColumns"
import { useWalletFilters } from "@/components/FindWalletProductTable/hooks/useWalletFilters"
@@ -13,42 +13,66 @@ import { trackCustomEvent } from "@/lib/utils/matomo"
import FindWalletsNoResults from "./FindWalletsNoResults"
import WalletSubComponent from "./WalletSubComponent"
-const FindWalletProductTable = ({ wallets }) => {
+const FindWalletProductTable = ({ wallets }: { wallets: Wallet[] }) => {
const { t } = useTranslation("page-wallets-find-wallet")
const walletPersonas = useWalletPersonaPresets()
const walletFilterOptions = useWalletFilters()
const [filters, setFilters] = useState(walletFilterOptions)
+ const [isClient, setIsClient] = useState(false)
- const filteredData = useMemo(() => {
- const activeFilterKeys: string[] = []
- let selectedLanguage: string
+ useEffect(() => {
+ setIsClient(true)
+ }, [])
+ const activeFilterKeys = useMemo(() => {
+ const keys: string[] = []
filters.forEach((filter) => {
filter.items.forEach((item) => {
- if (item.filterKey === "languages") {
- selectedLanguage = item.inputState as string
- } else if (item.inputState === true && item.options.length === 0) {
- activeFilterKeys.push(item.filterKey)
+ if (item.inputState === true && item.options.length === 0) {
+ keys.push(item.filterKey)
}
-
- if (item.options && item.options.length > 0) {
+ if (item.options?.length > 0) {
item.options.forEach((option) => {
if (option.inputState === true) {
- activeFilterKeys.push(option.filterKey)
+ keys.push(option.filterKey)
}
})
}
})
})
+ return keys
+ }, [filters])
+
+ const filteredData = useMemo(() => {
+ if (!Array.isArray(wallets)) return []
+
+ let selectedLanguage: string = ""
+ let selectedLayer2: ChainName[] = []
+
+ filters.forEach((filter) => {
+ filter.items.forEach((item) => {
+ if (item.filterKey === "languages") {
+ selectedLanguage = item.inputState as string
+ } else if (item.filterKey === "layer_2_support") {
+ selectedLayer2 = (item.inputState as ChainName[]) || []
+ }
+ })
+ })
return wallets
.filter((item) => {
- return item.languages_supported.includes(selectedLanguage)
+ return item.languages_supported.includes(selectedLanguage as Lang)
+ })
+ .filter((item) => {
+ return (
+ selectedLayer2.length === 0 ||
+ selectedLayer2.every((chain) => item.supported_chains.includes(chain))
+ )
})
.filter((item) => {
return activeFilterKeys.every((key) => item[key])
})
- }, [wallets, filters])
+ }, [wallets, filters, activeFilterKeys])
// Reset filters
const resetFilters = () => {
@@ -60,11 +84,20 @@ const FindWalletProductTable = ({ wallets }) => {
})
}
+ if (!isClient) {
+ return null
+ }
+
+ if (!Array.isArray(wallets)) {
+ return Error loading wallets
+ }
+
return (
-
columns={useWalletColumns}
data={filteredData}
allDataLength={wallets.length}
+ matomoEventCategory="find-wallet"
filters={filters}
presetFilters={walletPersonas}
resetFilters={resetFilters}
diff --git a/src/components/Hero/ContentHero/index.tsx b/src/components/Hero/ContentHero/index.tsx
index 9df5a3ba499..27cdc123617 100644
--- a/src/components/Hero/ContentHero/index.tsx
+++ b/src/components/Hero/ContentHero/index.tsx
@@ -14,7 +14,7 @@ const ContentHero = (props: ContentHeroProps) => {
return (
-
+
(
-
-)
-
-const TwoColumnContent = (props: ChildOnlyProp) => (
-
-)
-
-const SelectedContainer = (props: ChildOnlyProp) => (
-
-)
-
-const H3 = (props: ChildOnlyProp) => (
-
-)
-
-const H4 = (props: ChildOnlyProp) => (
-
-)
-
-interface Exchange {
- name: string
- supports_deposits: Array
- supports_withdrawals: Array
- url: string
-}
-
-interface Layer2 {
- name: string
- bridgeWallets: Array
- bridge: string
-}
-
-interface Option {
- value: string
- label: string
-}
-
-interface Layer2Option extends Option {
- l2: Layer2
-}
-
-interface ExchangeOption extends Option {
- cex: Exchange
-}
-
-interface CexOnboardOption extends Option {
- cexOnboard: CexOnboard
-}
-
-export type Layer2OnboardProps = {
- layer2DataCombined: Array
- ethIcon: StaticImageData
- ethIconAlt: string
-}
-
-const Layer2Onboard = ({
- layer2DataCombined,
- ethIcon,
- ethIconAlt,
-}: Layer2OnboardProps) => {
- const { t } = useTranslation("page-layer-2")
-
- const [selectedCexOnboard, setSelectedCexOnboard] = useState<
- CexOnboard | undefined
- >(undefined)
- const [selectedExchange, setSelectedExchange] = useState<
- Exchange | undefined
- >(undefined)
- const [selectedL2, setSelectedL2] = useState(undefined)
-
- const layer2Options: Array = layer2DataCombined.map((l2) => {
- return {
- label: l2.name,
- value: l2.name,
- l2,
- }
- })
-
- const cexSupportOptions: Array = cexSupport.map(
- (cex: Exchange) => {
- return {
- label: cex.name,
- value: cex.name,
- cex,
- }
- }
- )
-
- const cexOnboardOptions: Array = cexOnboardData.map(
- (cexOnboard: CexOnboard) => {
- return {
- label: cexOnboard.name,
- value: cexOnboard.name,
- cexOnboard: cexOnboard,
- }
- }
- )
-
- const gridContentPlacementStyles = {
- gridContainer: {
- columns: { base: 1, md: 2 },
- templateRows: {
- base: "repeat(3, min-content)",
- md: "repeat(2, min-content)",
- },
- columnGap: "70px",
- rowGap: "10px",
- },
- selectedL2: {
- gridRow: { base: 2, md: "2/-1" },
- gridColumn: { md: "1/2" },
- },
- rightSideSelected: {
- gridRow: { md: "2/-1" },
- gridColumn: { md: "2/-1" },
- },
- logo: {
- gridColumn: { md: "1 / 3" },
- gridRow: { base: selectedL2 ? 3 : 2, md: 2 },
- placeSelf: "center",
- },
- } as const
-
- const handleLayer2SelectChange: SelectOnChange = (
- selectedOption
- ) => {
- if (!selectedOption) return
-
- trackCustomEvent({
- eventCategory: `Selected layer 2 to bridge to`,
- eventAction: `Clicked`,
- eventName: `${selectedOption.l2.name} bridge selected`,
- eventValue: `${selectedOption.l2.name}`,
- })
- setSelectedL2(selectedOption.l2)
- }
-
- const handleExchangeOnboard: SelectOnChange<
- ExchangeOption | CexOnboardOption
- > = (selectedOption) => {
- if (!selectedOption) return
-
- if ("cex" in selectedOption) {
- trackCustomEvent({
- eventCategory: `Selected cex to onboard`,
- eventAction: `Clicked`,
- eventName: `${selectedOption.label} selected`,
- eventValue: `${selectedOption.label}`,
- })
-
- setSelectedExchange(selectedOption.cex)
- setSelectedCexOnboard(undefined)
- } else {
- trackCustomEvent({
- eventCategory: `Selected cexOnboard to onboard`,
- eventAction: `Clicked`,
- eventName: `${selectedOption.label} selected`,
- eventValue: `${selectedOption.label}`,
- })
- setSelectedCexOnboard(selectedOption.cexOnboard)
- setSelectedExchange(undefined)
- }
- }
-
- return (
-
-
-
- {t("layer-2-onboard-title")}
-
- {t("layer-2-onboard-1")}
-
-
-
- {/* LeftDescription */}
-
- {t("layer-2-onboard-wallet-title")}
- {t("layer-2-onboard-wallet-1")}
-
-
- {t("layer-2-more-on-bridges")}
-
-
-
- {/* LeftSelected */}
-
-
-
-
-
- {/* RightDescription */}
-
- {t("layer-2-onboard-exchange-title")}
- {t("layer-2-onboard-exchange-1")}
-
- {t("layer-2-onboard-exchange-2")}{" "}
-
- {t("layer-2-onboard-find-a-wallet")}
-
-
-
- {/* RightSelect */}
-
-
-
-
- {/* LeftSelected extra */}
- {selectedL2 && (
-
-
-
- {`${t("layer-2-onboard-wallet-selected-1")} ${
- selectedL2.name
- } ${t("layer-2-onboard-wallet-selected-2")}`}
-
- {selectedL2.bridgeWallets.join(", ")}
-
- {`${selectedL2.name} ${t("layer-2-bridge")}`}
-
-
-
- )}
- {/* RightSelect exchange */}
- {selectedExchange && (
-
-
-
-
- {t("layer-2-deposits")}
-
- {selectedExchange.supports_deposits.map((l2) => (
- {l2}
- ))}
-
-
-
- {t("layer-2-withdrawals")}
-
- {selectedExchange.supports_withdrawals.map((l2) => (
- {l2}
- ))}
-
-
-
-
- {`${t("layer-2-go-to")} ${selectedExchange.name}`}
-
-
-
- )}
- {/* RightSelect Cex */}
- {selectedCexOnboard && (
-
-
- Supported exchanges
- {selectedCexOnboard.cex_support.join(", ")}
- Supported layer 2s
- {selectedCexOnboard.network_support.join(", ")}
-
- {`${t("layer-2-go-to")} ${selectedCexOnboard.name}`}
-
-
-
- )}
- {/* EthLogo */}
-
-
-
-
-
- )
-}
-
-export default Layer2Onboard
diff --git a/src/components/Layer2NetworksTable/NetworkMaturityTooltip.tsx b/src/components/Layer2NetworksTable/NetworkMaturityTooltip.tsx
new file mode 100644
index 00000000000..d7182641a7c
--- /dev/null
+++ b/src/components/Layer2NetworksTable/NetworkMaturityTooltip.tsx
@@ -0,0 +1,54 @@
+import { MaturityLevel } from "@/lib/types"
+
+import Tooltip from "@/components/Tooltip"
+import { Badge, BadgeProps } from "@/components/ui/badge"
+
+const NetworkMaturityTooltip = ({ maturity }: { maturity: MaturityLevel }) => {
+ const maturityDescription = {
+ "n/a": {
+ label: "N/A",
+ description: "Not applicable to Ethereum mainnet.",
+ },
+ robust: {
+ label: "Robust",
+ description:
+ "Fully decentralized and secure network that cannot be tampered with or stopped by any individual or group, including its creators.\n\nThis is a network that fulfills Ethereum's vision of decentralization.",
+ },
+ maturing: {
+ label: "Maturing",
+ description:
+ "A network transitioning to being decentralized. A group of actors still may be able to halt the network in extreme situations.",
+ },
+ developing: {
+ label: "Developing",
+ description:
+ "A centralized operator runs the network but adds fail-safe features to reduce risks of centralization.",
+ },
+ emerging: {
+ label: "Emerging",
+ description:
+ "A centralized operator runs the network. The data is publicly visible on Ethereum to verify whether the operator is being honest.",
+ },
+ } as const
+
+ return (
+
+
+ Network maturity: {maturityDescription[maturity].label}
+
+
+ {maturityDescription[maturity].description}
+
+
+ }
+ >
+
+ {maturity.toUpperCase()}
+
+
+ )
+}
+
+export default NetworkMaturityTooltip
diff --git a/src/components/Layer2NetworksTable/NetworkUsageChart.tsx b/src/components/Layer2NetworksTable/NetworkUsageChart.tsx
new file mode 100644
index 00000000000..3f7010cbc93
--- /dev/null
+++ b/src/components/Layer2NetworksTable/NetworkUsageChart.tsx
@@ -0,0 +1,157 @@
+import { Bar, BarChart, LabelList, YAxis } from "recharts"
+
+import { ChartConfig, ChartContainer, ChartLegend } from "@/components/ui/chart"
+
+const CustomLegend = ({ payload, chartConfig }) => {
+ return (
+
+ {payload.map((entry, index) => (
+
+
+
{chartConfig[entry.value].label}
+
+ ))}
+
+ )
+}
+
+const NetworkUsageChart = ({ usageData }) => {
+ const chartData = (() => {
+ // Calculate the sum of all values to normalize the data
+ const total = Object.values(usageData).reduce(
+ (sum: number, value: unknown) => sum + (value as number),
+ 0
+ )
+
+ const formatValue = (value: number) => {
+ const percentage = (value / total) * 100
+ return percentage < 1
+ ? Number(percentage.toFixed(1))
+ : Math.round(percentage)
+ }
+
+ return [
+ {
+ nft: formatValue(usageData.nft),
+ defi: formatValue(usageData.defi),
+ social: formatValue(usageData.social),
+ token_transfers: formatValue(usageData.token_transfers),
+ unlabeled: formatValue(usageData.unlabeled),
+ },
+ ]
+ })()
+
+ const chartConfig = {
+ nft: {
+ label: "NFT",
+ color: "#0872FC",
+ },
+ defi: {
+ label: "DeFi",
+ color: "#C32E8A",
+ },
+ social: {
+ label: "Social",
+ color: "#1B9A92",
+ },
+ token_transfers: {
+ label: "Token Transfers",
+ color: "#8E30FF",
+ },
+ unlabeled: {
+ label: "Unlabeled",
+ color: "hsl(var(--body))",
+ },
+ } satisfies ChartConfig
+
+ return (
+
+
+ dataMax + 15]}
+ axisLine={false}
+ tickLine={false}
+ fontSize={12}
+ tickFormatter={(value) => `${value}%`}
+ hide
+ />
+
+ `${value}%`}
+ />
+
+
+ `${value}%`}
+ />
+
+
+ `${value}%`}
+ />
+
+
+ `${value}%`}
+ />
+
+
+ `${value}%`}
+ />
+
+ }
+ />
+
+
+ )
+}
+
+export default NetworkUsageChart
diff --git a/src/components/Layer2NetworksTable/NetworksNoResults.tsx b/src/components/Layer2NetworksTable/NetworksNoResults.tsx
new file mode 100644
index 00000000000..799d8493659
--- /dev/null
+++ b/src/components/Layer2NetworksTable/NetworksNoResults.tsx
@@ -0,0 +1,43 @@
+import { useTranslation } from "next-i18next"
+
+import { trackCustomEvent } from "@/lib/utils/matomo"
+
+import { Button } from "../ui/buttons/Button"
+
+const FindWalletsNoResults = ({ resetFilters }) => {
+ const { t } = useTranslation("page-wallets-find-wallet")
+
+ // Track empty state
+ trackCustomEvent({
+ eventCategory: "Wallet_empty_state",
+ eventAction: "reset",
+ eventName: "triggered",
+ })
+
+ const handleClick = () => {
+ resetFilters()
+ trackCustomEvent({
+ eventCategory: "Wallet_empty_state",
+ eventAction: "reset",
+ eventName: "reset_button_clicked",
+ })
+ }
+
+ return (
+
+
+
+ {t("page-find-wallet-empty-results-title")}
+
+
+ There are no networks matching your criteria, try adding some filters
+
+
+ {t("page-find-wallet-reset-filters")}
+
+
+
+ )
+}
+
+export default FindWalletsNoResults
diff --git a/src/components/Layer2NetworksTable/NetworksSubComponent.tsx b/src/components/Layer2NetworksTable/NetworksSubComponent.tsx
new file mode 100644
index 00000000000..ca8d484c91a
--- /dev/null
+++ b/src/components/Layer2NetworksTable/NetworksSubComponent.tsx
@@ -0,0 +1,286 @@
+import { MdInfoOutline } from "react-icons/md"
+
+import { ButtonLink } from "@/components/Buttons"
+import NetworkUsageChart from "@/components/Layer2NetworksTable/NetworkUsageChart"
+import InlineLink from "@/components/Link"
+import Tooltip from "@/components/Tooltip"
+
+const formatNumber = (num: number): string => {
+ if (num >= 1e9) {
+ return (num / 1e9).toFixed(2) + "B"
+ }
+ if (num >= 1e6) {
+ return (num / 1e6).toFixed(2) + "M"
+ }
+ if (num >= 1e3) {
+ return (num / 1e3).toFixed(0) + "K"
+ }
+ return num.toString()
+}
+
+const NetworkSubComponent = ({ network }) => {
+ return (
+
+
+
+
+
+
+
+ Age{" "}
+
+ Age
+ Shows how long the networks has been operational.
+
+ Data from{" "}
+
+ Growthepie
+
+ .
+
+
+ }
+ customMatomoEvent={{
+ eventCategory: "l2_networks",
+ eventAction: "tooltip",
+ eventName: "age",
+ }}
+ >
+
+
+
+
+ {(() => {
+ const launch = new Date(network.launchDate)
+ const today = new Date()
+ const yearDiff = today.getFullYear() - launch.getFullYear()
+ const monthDiff = today.getMonth() - launch.getMonth()
+
+ const totalMonths = yearDiff * 12 + monthDiff
+ const years = Math.floor(totalMonths / 12)
+ const months = totalMonths % 12
+
+ return `${years ? years + " year" + (years > 1 ? "s" : "") : ""} ${months ? months + " month" + (months > 1 ? "s" : "") : ""}`.trim()
+ })()}
+
+
+
+
+
+
+ Wallet support{" "}
+
+ Wallet support
+
+ Indicates how many wallet apps support using the
+ network.
+
+
+ }
+ customMatomoEvent={{
+ eventCategory: "l2_networks",
+ eventAction: "tooltip",
+ eventName: "wallet_support",
+ }}
+ >
+
+
+
+
+
+
+ {network.walletsSupportedCount}
+
+
+
+
+
+
+
+
+ Active addresses{" "}
+
+
+ Active addresses (weekly)
+
+
+ Number of active addresses on the network in the past
+ 7 days.
+
+
+ Data from{" "}
+
+ Growthepie
+
+ .
+
+
+ }
+ customMatomoEvent={{
+ eventCategory: "l2_networks",
+ eventAction: "tooltip",
+ eventName: "active_addresses",
+ }}
+ >
+
+
+
+
+
{formatNumber(network.activeAddresses)}
+
+
+
+
+ Fee token{" "}
+
+ Fee token
+
+ The token that is used to pay for transactions and
+ using the network.
+
+
+ }
+ customMatomoEvent={{
+ eventCategory: "l2_networks",
+ eventAction: "tooltip",
+ eventName: "fee_token",
+ }}
+ >
+
+
+
+
+
{network.feeToken.join(", ")}
+
+
+
+
+
+
+ Network usage{" "}
+
+ Network usage
+
+ An overview of network usage. Measures transaction count
+ in respective areas within the last 30 days.
+
+
+ Data from{" "}
+
+ Growthepie
+
+ .
+
+
+ }
+ >
+
+
+
+
+ {network.blockspaceData && (
+
+ )}
+ {network.blockspaceData === null && (
+
+ )}
+
+
+
+
+
+
+
Links
+
+
+
+ Official website
+
+
+
+
+
+ Risk analysis
+
+
+
Assessment by L2BEAT
+
+
+
+
+ Detailed analytics
+
+
+
+ Assessment by growthepie
+
+
+
+
+
+
Actions
+
+
+ Bridge to {network.name}
+
+
+ View apps
+
+
+
+
+
+ )
+}
+
+export default NetworkSubComponent
diff --git a/src/components/Layer2NetworksTable/NetworksWalletSelectInput.tsx b/src/components/Layer2NetworksTable/NetworksWalletSelectInput.tsx
new file mode 100644
index 00000000000..c2638146412
--- /dev/null
+++ b/src/components/Layer2NetworksTable/NetworksWalletSelectInput.tsx
@@ -0,0 +1,89 @@
+import { useState } from "react"
+
+import { FilterInputState } from "@/lib/types"
+
+import { Input } from "@/components/ui/input"
+import {
+ Select,
+ SelectContent,
+ SelectItem,
+ SelectTrigger,
+ SelectValue,
+} from "@/components/ui/select"
+
+import { walletsData } from "@/data/wallets/wallet-data"
+
+interface NetworksWalletSelectInputProps {
+ filterIndex: number
+ itemIndex: number
+ inputState: FilterInputState
+ updateFilterState: (
+ filterIndex: number,
+ itemIndex: number,
+ newInputState: FilterInputState
+ ) => void
+}
+
+const NetworksWalletSelectInput = ({
+ filterIndex,
+ itemIndex,
+ inputState,
+ updateFilterState,
+}: NetworksWalletSelectInputProps) => {
+ const [searchQuery, setSearchQuery] = useState("")
+
+ const filteredWallets = walletsData
+ .filter((wallet) =>
+ wallet.name.toLowerCase().includes(searchQuery.toLowerCase())
+ )
+ .sort((a, b) => a.name.localeCompare(b.name))
+
+ return (
+
+
{
+ updateFilterState(filterIndex, itemIndex, e)
+ setSearchQuery("") // Reset search when selection is made
+ }}
+ >
+
+
+
+
+ e.stopPropagation()}
+ onClick={(e) => e.stopPropagation()}
+ >
+ setSearchQuery(e.target.value)}
+ className="w-full"
+ />
+
+ {filteredWallets.length > 0 ? (
+ filteredWallets.map((wallet, idx) => (
+
+ {wallet.name} ({wallet.supported_chains.length})
+
+ ))
+ ) : (
+
+ No wallets found
+
+ )}
+
+
+
+ )
+}
+
+export default NetworksWalletSelectInput
diff --git a/src/components/Layer2NetworksTable/hooks/useNetworkColumns.tsx b/src/components/Layer2NetworksTable/hooks/useNetworkColumns.tsx
new file mode 100644
index 00000000000..d290ad5e935
--- /dev/null
+++ b/src/components/Layer2NetworksTable/hooks/useNetworkColumns.tsx
@@ -0,0 +1,280 @@
+"use client"
+
+import { IoChevronDownSharp, IoChevronUpSharp } from "react-icons/io5"
+import { MdInfoOutline } from "react-icons/md"
+import { ColumnDef } from "@tanstack/react-table"
+
+import { ExtendedRollup, Lang } from "@/lib/types"
+
+import { TableMeta } from "@/components/DataTable"
+import { TwImage } from "@/components/Image"
+import NetworkMaturityTooltip from "@/components/Layer2NetworksTable/NetworkMaturityTooltip"
+import InlineLink from "@/components/Link"
+import Tooltip from "@/components/Tooltip"
+import { Button } from "@/components/ui/buttons/Button"
+import { TableCell, TableHead } from "@/components/ui/Table"
+
+import { cn } from "@/lib/utils/cn"
+import { trackCustomEvent } from "@/lib/utils/matomo"
+
+export const useNetworkColumns: ColumnDef
[] = [
+ {
+ id: "l2Info",
+ header: ({ table }) => {
+ const meta = table.options.meta as TableMeta
+
+ return (
+
+ {
+ trackCustomEvent({
+ eventCategory: "MobileFilterToggle",
+ eventAction: "Tap MobileFilterToggle - sticky",
+ eventName: "show mobile filters true",
+ })
+ meta.setMobileFiltersOpen(true)
+ }}
+ >
+ {`Filters (${meta.activeFiltersCount})`}
+
+
+ Networks showing ({table.options.data.length})
+
+
+ )
+ },
+ cell: ({ table, row }) => {
+ const meta = table.options.meta as TableMeta
+ return (
+
+
+
+
+
+
+
{row.original.name}
+
+
+
+
+
+
Avg. transaction fee
+
+ $
+ {row.original.txCosts.toLocaleString(meta.locale as Lang, {
+ minimumFractionDigits: 2,
+ maximumFractionDigits: 3,
+ })}
+
+
+
+
Market share
+
+ {new Intl.NumberFormat(meta.locale as Lang, {
+ style: "currency",
+ currency: "USD",
+ notation: "compact",
+ minimumSignificantDigits: 3,
+ maximumSignificantDigits: 3,
+ }).format(row.original.tvl)}
+
+
+
+
+
+ {row.original.canExpand === false ? (
+
+ ) : (
+
+ {row.getIsExpanded() ? (
+
+ ) : (
+
+ )}
+
+ )}
+
+
+ )
+ },
+ },
+ {
+ id: "average_transaction_fee",
+ header: () => (
+
+
+ Avg. transaction fee{" "}
+
+
+ Transaction fee
+
+ The average cost of transaction for transfers, swaps,
+ minting and other activities.
+
+
+ Data from{" "}
+
+ GrowThePie
+
+ .
+
+
+ }
+ >
+
+
+
+
+
+ ),
+ cell: ({ table, row }) => {
+ const meta = table.options.meta as TableMeta
+
+ return (
+
+ $
+ {row.original.txCosts.toLocaleString(meta.locale as Lang, {
+ minimumFractionDigits: 2,
+ maximumFractionDigits: 3,
+ })}
+
+ )
+ },
+ },
+ {
+ id: "market_share",
+ header: () => (
+
+
+ Market share{" "}
+
+
+ Market share
+ Total value locked in escrow contracts on Ethereum.
+
+ Data from{" "}
+ L2BEAT .
+
+
+ }
+ >
+
+
+
+
+
+ ),
+ cell: ({ table, row }) => {
+ const meta = table.options.meta as TableMeta
+ return (
+
+
+ {new Intl.NumberFormat(meta.locale as Lang, {
+ style: "currency",
+ currency: "USD",
+ notation: "compact",
+ minimumSignificantDigits: 3,
+ maximumSignificantDigits: 3,
+ }).format(row.original.tvl)}
+
+
+ )
+ },
+ },
+ {
+ id: "network_maturity",
+ header: () => (
+
+
+ Network maturity{" "}
+
+
+ Network maturity
+
+ Looks at the development stage, risks associated with using
+ the network and ecosystem size of the network.
+
+
+ This is a summary metric based on risk analysis done by{" "}
+ L2BEAT .
+
+
+ }
+ >
+
+
+
+
+
+ ),
+ cell: ({ row }) => {
+ return (
+
+
+
+ )
+ },
+ },
+ {
+ id: "dropdown",
+ header: () => ,
+ cell: ({ row }) => {
+ if (row.original.canExpand === false)
+ return (
+
+ )
+
+ return (
+
+
+ {row.getIsExpanded() ? (
+
+ ) : (
+
+ )}
+
+
+ )
+ },
+ },
+]
diff --git a/src/components/Layer2NetworksTable/hooks/useNetworkFilters.tsx b/src/components/Layer2NetworksTable/hooks/useNetworkFilters.tsx
new file mode 100644
index 00000000000..042251f322c
--- /dev/null
+++ b/src/components/Layer2NetworksTable/hooks/useNetworkFilters.tsx
@@ -0,0 +1,193 @@
+import { FilterOption } from "@/lib/types"
+
+import {
+ DevelopingIcon,
+ EmergingIcon,
+ MaturingIcon,
+ RobustIcon,
+} from "@/components/icons/layer-2"
+import NetworksWalletSelectInput from "@/components/Layer2NetworksTable/NetworksWalletSelectInput"
+import SwitchFilterInput from "@/components/ProductTable/FilterInputs/SwitchFilterInput"
+
+import { trackCustomEvent } from "@/lib/utils/matomo"
+
+export const useNetworkFilters = (): FilterOption[] => {
+ return [
+ {
+ title: "Wallet support",
+ showFilterOption: true,
+ items: [
+ {
+ filterKey: "wallets_supported",
+ filterLabel: "wallets_supported",
+ description: "",
+ inputState: "",
+ ignoreFilterReset: false,
+ input: (filterIndex, itemIndex, inputState, updateFilterState) => {
+ return (
+ {
+ trackCustomEvent({
+ eventCategory: "l2_networks",
+ eventAction: "wallet",
+ eventName: newInputState as string,
+ })
+ updateFilterState(filterIndex, itemIndex, newInputState)
+ }}
+ />
+ )
+ },
+ options: [],
+ },
+ ],
+ },
+ {
+ title: "Network maturity",
+ showFilterOption: true,
+ items: [
+ {
+ filterKey: "robust",
+ filterLabel: "Robust",
+ description:
+ "Fully decentralized and secure network that cannot be tampered with or stopped by any individual or group, including its creators.",
+ inputState: true,
+ input: (filterIndex, itemIndex, inputState, updateFilterState) => {
+ return (
+
+ Fully decentralized and secure network that cannot be
+ tampered with or stopped by any individual or group,
+ including its creators.
+
+
+ This is a network that fulfills Ethereum's vision of
+ decentralization.
+ >
+ }
+ filterIndex={filterIndex}
+ itemIndex={itemIndex}
+ inputState={inputState}
+ updateFilterState={(filterIndex, itemIndex, newInputState) => {
+ trackCustomEvent({
+ eventCategory: "l2_networks",
+ eventAction: "filter",
+ eventName: `robust ${newInputState}`,
+ })
+ updateFilterState(filterIndex, itemIndex, newInputState)
+ }}
+ />
+ )
+ },
+ options: [],
+ },
+ {
+ filterKey: "maturing",
+ filterLabel: "Maturing",
+ description:
+ "A network transitions to being decentralized. A group of actors still may be able to halt the network in extreme situations.",
+ inputState: true,
+ input: (filterIndex, itemIndex, inputState, updateFilterState) => {
+ return (
+
+ A network transitioning to being decentralized. A group of
+ actors still may be able to halt the network in extreme
+ situations.
+ >
+ }
+ filterIndex={filterIndex}
+ itemIndex={itemIndex}
+ inputState={inputState}
+ updateFilterState={(filterIndex, itemIndex, newInputState) => {
+ trackCustomEvent({
+ eventCategory: "l2_networks",
+ eventAction: "filter",
+ eventName: `maturing ${newInputState}`,
+ })
+ updateFilterState(filterIndex, itemIndex, newInputState)
+ }}
+ />
+ )
+ },
+ options: [],
+ },
+ {
+ filterKey: "developing",
+ filterLabel: "Developing",
+ description:
+ "Single operator is running the network with public data visibility for transparency. ",
+ inputState: true,
+ input: (filterIndex, itemIndex, inputState, updateFilterState) => {
+ return (
+
+ A centralized operator runs the network but adds fail-safe
+ features to reduce risks of centralization.
+ >
+ }
+ filterIndex={filterIndex}
+ itemIndex={itemIndex}
+ inputState={inputState}
+ updateFilterState={(filterIndex, itemIndex, newInputState) => {
+ trackCustomEvent({
+ eventCategory: "l2_networks",
+ eventAction: "filter",
+ eventName: `developing ${newInputState}`,
+ })
+ updateFilterState(filterIndex, itemIndex, newInputState)
+ }}
+ />
+ )
+ },
+ options: [],
+ },
+ {
+ filterKey: "emerging",
+ filterLabel: "Emerging",
+ description:
+ "Single operator is running the network in private and works towards transparency.",
+ inputState: true,
+ input: (filterIndex, itemIndex, inputState, updateFilterState) => {
+ return (
+
+ A centralized operator runs the network. The data is
+ publicly visible on Ethereum to verify whether the operator
+ is being honest.
+ >
+ }
+ filterIndex={filterIndex}
+ itemIndex={itemIndex}
+ inputState={inputState}
+ updateFilterState={(filterIndex, itemIndex, newInputState) => {
+ trackCustomEvent({
+ eventCategory: "l2_networks",
+ eventAction: "filter",
+ eventName: `emerging ${newInputState}`,
+ })
+ updateFilterState(filterIndex, itemIndex, newInputState)
+ }}
+ />
+ )
+ },
+ options: [],
+ },
+ ],
+ },
+ ]
+}
diff --git a/src/components/Layer2NetworksTable/index.tsx b/src/components/Layer2NetworksTable/index.tsx
new file mode 100644
index 00000000000..70cc3d8e541
--- /dev/null
+++ b/src/components/Layer2NetworksTable/index.tsx
@@ -0,0 +1,80 @@
+import { useMemo, useState } from "react"
+
+import { ExtendedRollup, FilterOption, Lang } from "@/lib/types"
+
+import { useNetworkColumns } from "@/components/Layer2NetworksTable/hooks/useNetworkColumns"
+import { useNetworkFilters } from "@/components/Layer2NetworksTable/hooks/useNetworkFilters"
+import NetworksNoResults from "@/components/Layer2NetworksTable/NetworksNoResults"
+import NetworkSubComponent from "@/components/Layer2NetworksTable/NetworksSubComponent"
+import ProductTable from "@/components/ProductTable"
+
+import { trackCustomEvent } from "@/lib/utils/matomo"
+
+const Layer2NetworksTable = ({
+ layer2Data,
+ locale,
+ mainnetData,
+}: {
+ layer2Data: ExtendedRollup[]
+ locale: Lang
+ mainnetData: ExtendedRollup
+}) => {
+ const networkFilterOptions = useNetworkFilters()
+ const [filters, setFilters] = useState(networkFilterOptions)
+
+ const filteredData = useMemo(() => {
+ const networks = [mainnetData, ...layer2Data]
+
+ const filteredData = networks
+ .filter((network) => {
+ if (network === mainnetData) return true
+
+ const maturityFilter = filters[1].items.find(
+ (item) => item.filterKey === network.networkMaturity
+ )
+ return maturityFilter!.inputState
+ })
+ .filter((network) => {
+ if (filters[0].items[0].inputState === "") return true
+ return network.walletsSupported.includes(
+ filters[0].items[0].inputState as string
+ )
+ })
+
+ return filteredData
+ }, [layer2Data, mainnetData, filters])
+
+ const resetFilters = () => {
+ setFilters(networkFilterOptions)
+ trackCustomEvent({
+ eventCategory: "Layer2NetworksTable",
+ eventAction: "Reset button",
+ eventName: "reset_click",
+ })
+ }
+
+ return (
+ {
+ return
+ }}
+ noResultsComponent={() => (
+
+ )}
+ mobileFiltersLabel={"See networks"}
+ />
+ )
+}
+
+export default Layer2NetworksTable
diff --git a/src/components/Nav/useNav.ts b/src/components/Nav/useNav.ts
index 4d8d4da0f63..45c438c2ab6 100644
--- a/src/components/Nav/useNav.ts
+++ b/src/components/Nav/useNav.ts
@@ -247,10 +247,26 @@ export const useNav = () => {
],
},
{
- label: t("layer-2"),
- description: t("nav-layer-2-description"),
+ label: t("nav-ethereum-networks"),
+ description: t("nav-ethereum-networks-description"),
icon: BsLayers,
- href: "/layer-2/",
+ items: [
+ {
+ label: t("nav-networks-introduction-label"),
+ description: t("nav-networks-introduction-description"),
+ href: "/layer-2/",
+ },
+ {
+ label: t("nav-networks-explore-networks-label"),
+ description: t("nav-networks-explore-networks-description"),
+ href: "/layer-2/networks/",
+ },
+ {
+ label: t("nav-networks-learn-label"),
+ description: t("nav-networks-learn-description"),
+ href: "/layer-2/learn/",
+ },
+ ],
},
],
},
diff --git a/src/components/ProductTable/FilterInputs/CheckboxFilterInput.tsx b/src/components/ProductTable/FilterInputs/CheckboxFilterInput.tsx
index f9f8d244e11..6b26e0da584 100644
--- a/src/components/ProductTable/FilterInputs/CheckboxFilterInput.tsx
+++ b/src/components/ProductTable/FilterInputs/CheckboxFilterInput.tsx
@@ -6,13 +6,13 @@ interface CheckboxFilterInputProps {
label: string
filterIndex: number
itemIndex: number
- optionIndex: number
+ optionIndex?: number
inputState: FilterInputState
updateFilterState: (
filterIndex: number,
itemIndex: number,
newInputState: boolean,
- optionIndex: number
+ optionIndex?: number
) => void
}
@@ -25,15 +25,19 @@ const CheckboxFilterInput = ({
updateFilterState,
}: CheckboxFilterInputProps) => {
return (
-
+
{
- updateFilterState(filterIndex, itemIndex, e as boolean, optionIndex)
+ if (typeof optionIndex !== "undefined") {
+ updateFilterState(filterIndex, itemIndex, e as boolean, optionIndex)
+ } else {
+ updateFilterState(filterIndex, itemIndex, e as boolean)
+ }
}}
/>
- {label}
-
+ {label}
+
)
}
diff --git a/src/components/ProductTable/FilterInputs/SwitchFilterInput.tsx b/src/components/ProductTable/FilterInputs/SwitchFilterInput.tsx
index 8deeba46292..37af6de0bdc 100644
--- a/src/components/ProductTable/FilterInputs/SwitchFilterInput.tsx
+++ b/src/components/ProductTable/FilterInputs/SwitchFilterInput.tsx
@@ -1,3 +1,4 @@
+import { ReactElement } from "react"
import type { IconType } from "react-icons"
import { FilterInputState } from "@/lib/types"
@@ -7,7 +8,7 @@ import Switch from "@/../tailwind/ui/Switch"
interface SwitchFilterInputProps {
Icon?: IconType
label: string
- description?: string
+ description?: string | ReactElement
filterIndex: number
itemIndex: number
inputState: FilterInputState
diff --git a/src/components/ProductTable/index.tsx b/src/components/ProductTable/index.tsx
index 0cac8278137..74e96d858a2 100644
--- a/src/components/ProductTable/index.tsx
+++ b/src/components/ProductTable/index.tsx
@@ -7,38 +7,33 @@ import {
useState,
} from "react"
import { useRouter } from "next/router"
-import { useTranslation } from "next-i18next"
import { ColumnDef } from "@tanstack/react-table"
-import type {
- FilterOption,
- ProductTableColumnDefs,
- ProductTableRow,
- TPresetFilters,
-} from "@/lib/types"
+import type { FilterOption, TPresetFilters } from "@/lib/types"
import Table from "@/components/DataTable"
import Filters from "@/components/ProductTable/Filters"
import MobileFilters from "@/components/ProductTable/MobileFilters"
import PresetFilters from "@/components/ProductTable/PresetFilters"
-import { Button } from "@/components/ui/buttons/Button"
import { trackCustomEvent } from "@/lib/utils/matomo"
-interface ProductTableProps {
- columns: ColumnDef[]
- data: TData[]
+interface ProductTableProps {
+ columns: ColumnDef[]
+ data: T[]
allDataLength: number
filters: FilterOption[]
presetFilters: TPresetFilters
resetFilters: () => void
setFilters: Dispatch>
- subComponent?: FC
+ subComponent?: FC
noResultsComponent?: React.FC
mobileFiltersLabel: string
+ matomoEventCategory: string
+ meta?: Record
}
-const ProductTable = ({
+const ProductTable = ({
columns,
data,
allDataLength,
@@ -49,12 +44,34 @@ const ProductTable = ({
subComponent,
noResultsComponent,
mobileFiltersLabel,
-}: ProductTableProps) => {
+ matomoEventCategory,
+ meta,
+}: ProductTableProps) => {
const router = useRouter()
- const { t } = useTranslation("table")
const [activePresets, setActivePresets] = useState([])
const [mobileFiltersOpen, setMobileFiltersOpen] = useState(false)
+ const parseQueryParams = (queryValue: unknown) => {
+ // Handle boolean values
+ if (queryValue === "true") return true
+ if (queryValue === "false") return false
+
+ // Handle array values
+ if (
+ typeof queryValue === "string" &&
+ queryValue.startsWith("[") &&
+ queryValue.endsWith("]")
+ ) {
+ try {
+ return JSON.parse(decodeURIComponent(queryValue))
+ } catch {
+ return undefined
+ }
+ }
+
+ return undefined
+ }
+
// Update filters based on router query
useEffect(() => {
if (Object.keys(router.query).length > 0) {
@@ -62,14 +79,13 @@ const ProductTable = ({
...filter,
items: filter.items.map((item) => ({
...item,
- inputState: Object.keys(router.query).includes(item.filterKey)
- ? true
- : item.inputState,
+ inputState:
+ parseQueryParams(router.query[item.filterKey]) || item.inputState,
options: item.options.map((option) => ({
...option,
- inputState: Object.keys(router.query).includes(option.filterKey)
- ? true
- : option.inputState,
+ inputState:
+ parseQueryParams(router.query[option.filterKey]) ||
+ option.inputState,
})),
})),
}))
@@ -222,6 +238,17 @@ const ProductTable = ({
).length
)
}
+ if (Array.isArray(item.inputState) && item.inputState.length > 0) {
+ return itemCount + 1
+ }
+
+ if (
+ typeof item.inputState === "string" &&
+ item.filterKey !== "languages"
+ ) {
+ return itemCount + 1
+ }
+
return (
itemCount +
(typeof item.inputState === "boolean" && item.inputState ? 1 : 0)
@@ -268,40 +295,17 @@ const ProductTable = ({
/>
-
-
{
- trackCustomEvent({
- eventCategory: "MobileFilterToggle",
- eventAction: "Tap MobileFilterToggle - sticky",
- eventName: "show mobile filters true",
- })
- setMobileFiltersOpen(true)
- }}
- >
-
- {`${t("table-filters")} (${activeFiltersCount})`}
-
-
-
- {t("table-showing")}{" "}
- {data.length === allDataLength ? (
- {data.length}
- ) : (
-
- {data.length}/{allDataLength}
-
- )}
-
-
diff --git a/src/components/RollupProductDevDoc.tsx b/src/components/RollupProductDevDoc.tsx
deleted file mode 100644
index cc0af5a4922..00000000000
--- a/src/components/RollupProductDevDoc.tsx
+++ /dev/null
@@ -1,58 +0,0 @@
-import { ListItem, UnorderedList } from "@/components/ui/list"
-
-import { layer2Data, Rollups, RollupType } from "@/data/layer-2/layer-2"
-
-import InlineLink from "./Link"
-import Translation from "./Translation"
-
-const rollups = layer2Data as Rollups
-
-export type RollupProductDevDocProps = {
- rollupType: RollupType
-}
-
-const RollupProductDevDoc = ({ rollupType }: RollupProductDevDocProps) => {
- return (
-
- {rollups[rollupType].map(
- ({ name, noteKey, website, developerDocs, l2beat }, idx) => {
- return (
-
-
-
-
- {name}
-
- {noteKey.length > 0 && (
-
- *
-
- )}
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- )
- }
- )}
-
- )
-}
-
-export default RollupProductDevDoc
diff --git a/src/components/Tooltip/index.tsx b/src/components/Tooltip/index.tsx
index 7d965c69949..53910d76485 100644
--- a/src/components/Tooltip/index.tsx
+++ b/src/components/Tooltip/index.tsx
@@ -2,6 +2,7 @@ import React, { ComponentProps, ReactNode, useEffect } from "react"
import { Portal } from "@radix-ui/react-portal"
import { isMobile } from "@/lib/utils/isMobile"
+import { trackCustomEvent } from "@/lib/utils/matomo"
import { Popover, PopoverContent, PopoverTrigger } from "../ui/popover"
import {
@@ -18,6 +19,11 @@ export type TooltipProps = ComponentProps & {
children?: ReactNode
onBeforeOpen?: () => void
container?: HTMLElement | null
+ customMatomoEvent?: {
+ eventCategory: string
+ eventAction: string
+ eventName: string
+ }
}
const Tooltip = ({
@@ -25,6 +31,7 @@ const Tooltip = ({
children,
onBeforeOpen,
container,
+ customMatomoEvent,
...props
}: TooltipProps) => {
const { isOpen, onOpen, onClose } = useDisclosure()
@@ -64,6 +71,10 @@ const Tooltip = ({
const handleOpenChange = (open: boolean) => {
if (open) {
handleOpen()
+ customMatomoEvent &&
+ trackCustomEvent({
+ ...customMatomoEvent,
+ })
} else {
onClose()
}
diff --git a/src/components/Translatathon/ApplyNow.tsx b/src/components/Translatathon/ApplyNow.tsx
index ea683a07de6..adb5959f380 100644
--- a/src/components/Translatathon/ApplyNow.tsx
+++ b/src/components/Translatathon/ApplyNow.tsx
@@ -17,12 +17,10 @@ export const ApplyNow = () => {
return (
Apply now
diff --git a/src/components/icons/layer-2/DevelopingIcon.tsx b/src/components/icons/layer-2/DevelopingIcon.tsx
new file mode 100644
index 00000000000..422d6c9bbc5
--- /dev/null
+++ b/src/components/icons/layer-2/DevelopingIcon.tsx
@@ -0,0 +1,37 @@
+import { createIconBase } from "@/components/icons/icon-base"
+import { commonIconDefaultAttrs } from "@/components/icons/utils"
+
+export const DevelopingIcon = createIconBase({
+ displayName: "DevelopingIcon",
+ viewBox: "0 0 16 24",
+ className: "w-4 h-auto",
+ ...commonIconDefaultAttrs,
+ children: (
+ <>
+
+
+
+
+ >
+ ),
+})
diff --git a/src/components/icons/layer-2/EmergingIcon.tsx b/src/components/icons/layer-2/EmergingIcon.tsx
new file mode 100644
index 00000000000..876f08844d7
--- /dev/null
+++ b/src/components/icons/layer-2/EmergingIcon.tsx
@@ -0,0 +1,32 @@
+import { createIconBase } from "@/components/icons/icon-base"
+import { commonIconDefaultAttrs } from "@/components/icons/utils"
+
+export const EmergingIcon = createIconBase({
+ displayName: "EmergingIcon",
+ viewBox: "0 0 16 24",
+ className: "w-4 h-auto",
+ ...commonIconDefaultAttrs,
+ children: (
+ <>
+
+
+
+
+ >
+ ),
+})
diff --git a/src/components/icons/layer-2/MaturingIcon.tsx b/src/components/icons/layer-2/MaturingIcon.tsx
new file mode 100644
index 00000000000..f0585e6a8c6
--- /dev/null
+++ b/src/components/icons/layer-2/MaturingIcon.tsx
@@ -0,0 +1,50 @@
+import { createIconBase } from "@/components/icons/icon-base"
+import { commonIconDefaultAttrs } from "@/components/icons/utils"
+
+export const MaturingIcon = createIconBase({
+ displayName: "MaturingIcon",
+ viewBox: "0 0 16 24",
+ className: "w-4 h-auto",
+ ...commonIconDefaultAttrs,
+ children: (
+ <>
+
+
+
+
+
+
+
+ >
+ ),
+})
diff --git a/src/components/icons/layer-2/RobustIcon.tsx b/src/components/icons/layer-2/RobustIcon.tsx
new file mode 100644
index 00000000000..b900e1d2678
--- /dev/null
+++ b/src/components/icons/layer-2/RobustIcon.tsx
@@ -0,0 +1,67 @@
+import { createIconBase } from "@/components/icons/icon-base"
+import { commonIconDefaultAttrs } from "@/components/icons/utils"
+
+export const RobustIcon = createIconBase({
+ displayName: "RobustIcon",
+ viewBox: "0 0 16 24",
+ className: "w-4 h-auto",
+ ...commonIconDefaultAttrs,
+ children: (
+ <>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ >
+ ),
+})
diff --git a/src/components/icons/layer-2/index.ts b/src/components/icons/layer-2/index.ts
new file mode 100644
index 00000000000..1325723a21b
--- /dev/null
+++ b/src/components/icons/layer-2/index.ts
@@ -0,0 +1,6 @@
+import { DevelopingIcon } from "./DevelopingIcon"
+import { EmergingIcon } from "./EmergingIcon"
+import { MaturingIcon } from "./MaturingIcon"
+import { RobustIcon } from "./RobustIcon"
+
+export { DevelopingIcon, EmergingIcon, MaturingIcon, RobustIcon }
diff --git a/src/components/ui/badge.tsx b/src/components/ui/badge.tsx
index 3b14f44a3d7..951807e9808 100644
--- a/src/components/ui/badge.tsx
+++ b/src/components/ui/badge.tsx
@@ -21,6 +21,11 @@ const badgeVariants = cva(
outline: "text-foreground",
// TODO: remove variant once we finish the badge and tag components with DS styles
productTable: "bg-body-light text-body font-medium uppercase",
+ emerging: "bg-blue-600 text-white border-none",
+ developing: "bg-blue-400 text-white border-none",
+ maturing: "bg-blue-200 text-black border-none",
+ robust: "bg-blue-100 text-black border-none",
+ "n/a": "hidden lg:block",
},
},
defaultVariants: {
diff --git a/src/components/ui/chart.tsx b/src/components/ui/chart.tsx
new file mode 100644
index 00000000000..18b60a29a11
--- /dev/null
+++ b/src/components/ui/chart.tsx
@@ -0,0 +1,363 @@
+import * as React from "react"
+import * as RechartsPrimitive from "recharts"
+
+import { cn } from "@/lib/utils/cn"
+
+// Format: { THEME_NAME: CSS_SELECTOR }
+const THEMES = { light: "", dark: ".dark" } as const
+
+export type ChartConfig = {
+ [k in string]: {
+ label?: React.ReactNode
+ icon?: React.ComponentType
+ } & (
+ | { color?: string; theme?: never }
+ | { color?: never; theme: Record }
+ )
+}
+
+type ChartContextProps = {
+ config: ChartConfig
+}
+
+const ChartContext = React.createContext(null)
+
+function useChart() {
+ const context = React.useContext(ChartContext)
+
+ if (!context) {
+ throw new Error("useChart must be used within a ")
+ }
+
+ return context
+}
+
+const ChartContainer = React.forwardRef<
+ HTMLDivElement,
+ React.ComponentProps<"div"> & {
+ config: ChartConfig
+ children: React.ComponentProps<
+ typeof RechartsPrimitive.ResponsiveContainer
+ >["children"]
+ }
+>(({ id, className, children, config, ...props }, ref) => {
+ const uniqueId = React.useId()
+ const chartId = `chart-${id || uniqueId.replace(/:/g, "")}`
+
+ return (
+
+
+
+
+ {children}
+
+
+
+ )
+})
+ChartContainer.displayName = "Chart"
+
+const ChartStyle = ({ id, config }: { id: string; config: ChartConfig }) => {
+ const colorConfig = Object.entries(config).filter(
+ ([_, config]) => config.theme || config.color
+ )
+
+ if (!colorConfig.length) {
+ return null
+ }
+
+ return (
+