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 && ( -
- {alt -
+
+ +
)} - +
{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 ( +
+ + {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 */} - - - -
- {/* 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 */} - - {ethIconAlt} - -
-
- ) -} - -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 +

+ +
+
+ ) +} + +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 && ( +
+

No data available

+
+ )} +
+
+
+
+
+
+

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 ( +
+ 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 ( + + +

+ 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 ? ( +
+ ) : ( + + )} +
+ + ) + }, + }, + { + 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 ( + + + + ) + }, + }, +] 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 ( -
+
+ {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 = ({ />
-
- -

- {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 ( +