Skip to content

Commit

Permalink
Merge branch 'main' into dependabot/npm_and_yarn/npm_and_yarn-e1825f7a49
Browse files Browse the repository at this point in the history
  • Loading branch information
dcts authored Nov 8, 2024
2 parents e26fba4 + 0697468 commit e4d03cb
Show file tree
Hide file tree
Showing 15 changed files with 147 additions and 186 deletions.
34 changes: 0 additions & 34 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -179,37 +179,3 @@ To add a new banner, follow these steps:
3. If you are the designer creating the banner, the content needs to be delivered as an SVG with a transparent background (see examples for [desktop](https://github.com/DeXter-on-Radix/website/blob/main/public/promo-banners/validator-node-staking/desktop-600x80.svg) or [mobile](https://github.com/DeXter-on-Radix/website/blob/main/public/promo-banners/validator-node-staking/mobile-600x200.svg)). Furthermore, ensure there is only a single call to action (CTA). Avoid having multiple competing actions like "STAKE NOW" and "learn more". Decide which one is more important and design the banner accordingly :D
4. Upload both files to `/public/promo-banners/`.
5. Fill out `imageUrl`, `imageUrlMobile` and optionally `redirecturl` inside [`src/app/layout.tsx`](https://github.com/DeXter-on-Radix/website/blob/main/src/app/layout.tsx#L15-L20).

## Hydration Error Handling

**Problem**

State variables get cached, leading to unpredictable initial renders of components (e.g., initialization based on cookies). This violates React/NextJS patterns and triggers hydration errors.

**Handling Hydration Errors**

1. **No Error**: If no hydration error occurs, no action is needed.
2. **Error Detected**: Identify the component causing the error.
3. **Fixing the Component**: Add the following code to the problematic component:

```tsx
import { useHydrationErrorFix } from "hooks";

function ComponentWithHydrationError() {
const isClient = useHydrationErrorFix();

// Additional code like useEffect or other hooks go here!
// ...

if (!isClient) return <></>;

return (
/* Your component JSX */
);
}
```

**Common Causes of Hydration Errors**

1. **Radix Connect Button**: Caches logged-in users. Components relying on login status will render differently based on the user's login state.
2. **Language Detection**: Cached in cookies. Components will render differently for users from various regions based on their browser-detected language.
Binary file added public/promo-banners/astrl-desktop3x.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added public/promo-banners/astrl-mobile3x.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
5 changes: 0 additions & 5 deletions src/app/components/DexterButton.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
"use client";

import { useHydrationErrorFix } from "hooks";
import { twMerge } from "tailwind-merge";

interface DexterButtonProps {
Expand All @@ -24,10 +23,6 @@ export function DexterButton({
buttonClassName = "",
labelClassName = "",
}: DexterButtonProps) {
const isClient = useHydrationErrorFix();

if (!isClient) return null;

const wrapperDefaultClassName = `z-100 min-w-[220px] max-${maxWidth}`;
const buttonDefaultClassName = `min-${minHeight} ${maxWidth} px-4 mb-6 mt-8 rounded bg-dexter-green-OG text-black uppercase opacity-100`;
const labelDefaultClassName = "font-bold text-sm tracking-[.1px]";
Expand Down
10 changes: 4 additions & 6 deletions src/app/components/NavBar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { useSelector } from "react-redux";
import {
useAppDispatch,
useAppSelector,
useHydrationErrorFix,
useIsMobile,
useTranslations,
} from "hooks";
import { getSupportedLanguagesAsString } from "../state/i18nSlice";
Expand All @@ -16,7 +16,7 @@ import { radixSlice } from "../state/radixSlice";

import Cookies from "js-cookie";
import { usePathname } from "next/navigation";
import { isMobile, shortenWalletAddress } from "../utils";
import { shortenWalletAddress } from "../utils";
import {
fetchAccountHistory,
accountHistorySlice,
Expand Down Expand Up @@ -272,9 +272,7 @@ function MobileMenu({
menuOpen: boolean;
setMenuOpen: (newMenuOpen: boolean) => void;
}) {
const isClient = useHydrationErrorFix();
if (!isClient) return null;

const isMobile = useIsMobile();
return (
<div
className={`flex flex-col
Expand All @@ -285,7 +283,7 @@ function MobileMenu({
fixed top-0 left-0
py-5
bg-[rgba(0,0,0,0.8)] backdrop-blur-lg
${isMobile() ? "px-6" : "px-10"}
${isMobile ? "px-6" : "px-10"}
${
menuOpen
? "opacity-100 scale-100"
Expand Down
11 changes: 1 addition & 10 deletions src/app/components/OrderInput.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,7 @@ import {
truncateWithPrecision,
} from "../utils";

import {
useAppDispatch,
useAppSelector,
useTranslations,
useHydrationErrorFix,
} from "hooks";
import { useAppDispatch, useAppSelector, useTranslations } from "hooks";
import { fetchBalances } from "state/pairSelectorSlice";
import {
OrderSide,
Expand Down Expand Up @@ -360,7 +355,6 @@ function PostOnlyCheckbox() {
}

function SubmitButton() {
const isClient = useHydrationErrorFix(); // to fix HydrationError
const t = useTranslations();
const dispatch = useAppDispatch();
const {
Expand Down Expand Up @@ -391,9 +385,6 @@ function SubmitButton() {
.replaceAll("<$SIDE>", t(side))
.replaceAll("<$TOKEN_SYMBOL>", token1.symbol);

// Fix HydrationError
if (!isClient) return <></>;

return (
<button
className={`w-full h-[40px] p-3 my-6 rounded ${
Expand Down
2 changes: 2 additions & 0 deletions src/app/components/PairSelector.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import React from "react";
import { searchPairs } from "utils";

import { BLACKLISTED_PAIRS } from "../data/BLACKLISTED_PAIRS";
import { accountHistorySlice } from "state/accountHistorySlice";

interface PairInfo {
name: string;
Expand Down Expand Up @@ -95,6 +96,7 @@ export function PairSelector() {
pairName: option["name"],
})
);
dispatch(accountHistorySlice.actions.resetSelectedOrdersToCancel());
setHighlightedIndex(-1);
setIsOpen(!isOpen);
},
Expand Down
96 changes: 90 additions & 6 deletions src/app/hooks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,14 @@ import {
setLocalStoragePaginationValue,
} from "utils";

// Define an enum for the operating system types
export enum OperatingSystem {
MAC = "MAC",
WINDOWS = "WINDOWS",
LINUX = "LINUX",
UNKNOWN = "UNKNOWN",
}

// https://redux-toolkit.js.org/tutorials/typescript#define-typed-hooks
export const useAppDispatch: () => AppDispatch = useDispatch;
export const useAppSelector: TypedUseSelectorHook<RootState> = useSelector;
Expand All @@ -24,17 +32,93 @@ export const useTranslations = () => {
return t;
};

// Hook to fix hydration errors by delaying rendering until client-side mount
export const useHydrationErrorFix = () => {
const [isClient, setIsClient] = useState(false);

export const useIsMobile = () => {
const [isMobile, setIsMobile] = useState(false);
useEffect(() => {
setIsClient(true);
const checkIfMobile = () => {
try {
const userAgent = navigator.userAgent.toLowerCase();
if (
userAgent.match(/Android/i) ||
userAgent.match(/webOS/i) ||
userAgent.match(/avantgo/i) ||
userAgent.match(/iPhone/i) ||
userAgent.match(/iPad/i) ||
userAgent.match(/iPod/i) ||
userAgent.match(/BlackBerry/i) ||
userAgent.match(/bolt/i) ||
userAgent.match(/Windows Phone/i) ||
userAgent.match(/Phone/i)
) {
return true;
}
return false;
} catch (err) {
console.error(err);
return window.innerWidth < 768;
}
};
setIsMobile(checkIfMobile());
}, []);
return isMobile;
};

return isClient;
export const useOperatingSystem = () => {
const [operatingSystem, setOperatingSystem] = useState<OperatingSystem>(
OperatingSystem.UNKNOWN
);
useEffect(() => {
const detectOperatingSystem = (): OperatingSystem => {
if (typeof window === "undefined" || typeof navigator === "undefined") {
return OperatingSystem.UNKNOWN;
}
const userAgent = navigator.userAgent.toLowerCase();
if (userAgent.includes("mac os")) {
return OperatingSystem.MAC;
} else if (userAgent.includes("windows")) {
return OperatingSystem.WINDOWS;
} else if (userAgent.includes("linux")) {
return OperatingSystem.LINUX;
} else {
return OperatingSystem.UNKNOWN;
}
};
const os = detectOperatingSystem();
setOperatingSystem(os);
}, []);
return operatingSystem;
};

export function useBrowserLanguage(defaultLanguage: string = "en") {
const [language, setLanguage] = useState<string>(defaultLanguage);
const detectBrowserLanguage = (defaultLanguage: string = "en"): string => {
const toLngCode = (str: string) => str.substring(0, 2).toLowerCase();
if (typeof window !== "undefined" && typeof navigator !== "undefined") {
if (Array.isArray(navigator.languages) && navigator.languages.length) {
return toLngCode(navigator.languages[0]);
}
if (navigator.language) {
return toLngCode(navigator.language);
}
if ((navigator as any).userLanguage) {
return toLngCode((navigator as any).userLanguage);
}
if ((navigator as any).browserLanguage) {
return toLngCode((navigator as any).browserLanguage);
}
if ((navigator as any).systemLanguage) {
return toLngCode((navigator as any).systemLanguage);
}
}
return defaultLanguage;
};
useEffect(() => {
const browserLanguage = detectBrowserLanguage(defaultLanguage);
setLanguage(browserLanguage);
}, [defaultLanguage]);
return language;
}

export function usePagination<T>(data: T[], paginationId?: string) {
const [currentPage, setCurrentPage] = useState(0);

Expand Down
37 changes: 22 additions & 15 deletions src/app/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,10 @@ import { DexterToaster } from "./components/DexterToaster";
import { useEffect, Suspense } from "react";
import { initializeSubscriptions, unsubscribeAll } from "./subscriptions";
import { store } from "./state/store";

import { detectBrowserLanguage } from "./utils";
import { i18nSlice } from "./state/i18nSlice";

import { useAppDispatch, useAppSelector, useBrowserLanguage } from "hooks";
import Cookies from "js-cookie";
import { useAppDispatch, useAppSelector } from "hooks";
import { i18nSlice } from "state/i18nSlice";
import { radixSlice } from "state/radixSlice";

export default function RootLayout({
children,
Expand All @@ -28,7 +26,7 @@ export default function RootLayout({
return () => {
unsubscribeAll();
};
}, []);
});

// TODO: after MVP remove "use client", fix all as many Components as possible
// to be server components for better SSG and SEO
Expand All @@ -50,21 +48,30 @@ export default function RootLayout({
function AppBody({ children }: { children: React.ReactNode }) {
const dispatch = useAppDispatch();
const path = usePathname();
const { isHydrated } = useAppSelector((state) => state.radix);

// set hydration globally once
useEffect(() => {
dispatch(radixSlice.actions.setIsHydrated(true));
}, [dispatch]);

// Detect browser langauge
const { textContent } = useAppSelector((state) => state.i18n);
const supportedLanguages = Object.keys(textContent);
const browserLanguage = useBrowserLanguage();

useEffect(() => {
const userLanguageCookieValue = Cookies.get("userLanguage");
if (userLanguageCookieValue) {
dispatch(i18nSlice.actions.changeLanguage(userLanguageCookieValue));
} else {
const browserLang = detectBrowserLanguage();
if (supportedLanguages.includes(browserLang)) {
dispatch(i18nSlice.actions.changeLanguage(browserLang));
if (isHydrated) {
// Detect browser language or retrieve from cookie only after hydration
const userLanguageCookieValue = Cookies.get("userLanguage");
if (userLanguageCookieValue) {
dispatch(i18nSlice.actions.changeLanguage(userLanguageCookieValue));
} else {
if (supportedLanguages.includes(browserLanguage)) {
dispatch(i18nSlice.actions.changeLanguage(browserLanguage));
}
}
}
}, [dispatch, supportedLanguages]);
}, [dispatch, isHydrated, supportedLanguages, browserLanguage]);

return (
<body>
Expand Down
8 changes: 2 additions & 6 deletions src/app/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

import { DexterButton } from "components/DexterButton";
import StatsWidget from "components/StatsWidget";
import { useTranslations, useHydrationErrorFix } from "hooks";
import { useTranslations } from "hooks";

enum Device {
MOBILE = "MOBILE",
Expand Down Expand Up @@ -51,6 +51,7 @@ export default function Landing() {
const tradeProps = getTopicsSectionProps(TopicSectionEnum.TRADE, t);
const stakeProps = getTopicsSectionProps(TopicSectionEnum.STAKE, t);
const contributeProps = getTopicsSectionProps(TopicSectionEnum.CONTRIBUTE, t);

return (
<div className="bg-dexter-grey-light">
<HeroSection />
Expand All @@ -65,9 +66,6 @@ export default function Landing() {

function HeroSection() {
const t = useTranslations();
const isClient = useHydrationErrorFix();

if (!isClient) return <></>;
return (
<div
className={
Expand Down Expand Up @@ -223,8 +221,6 @@ function TopicSection({
buttonText,
reversed,
}: TopicSectionProps): JSX.Element {
const isClient = useHydrationErrorFix();
if (!isClient) return <></>;
return (
<div
className={`${backgroundColor} py-20 max-[820px]:py-10 z-[100] relative`}
Expand Down
Loading

0 comments on commit e4d03cb

Please sign in to comment.