diff --git a/.agents/skills/intlayer-content/references/concept_content.md b/.agents/skills/intlayer-content/references/concept_content.md index 6cafa2e214..f75fbd3ff9 100644 --- a/.agents/skills/intlayer-content/references/concept_content.md +++ b/.agents/skills/intlayer-content/references/concept_content.md @@ -344,15 +344,15 @@ Transforms the dictionary into a per-locale dictionary where each field declared **Example:** -```json +```jsonc // Per-locale dictionary { "key": "about-page", "locale": "en", "content": { "title": "About Us", // This becomes a translation node for 'en' - "description": "Learn more about our company" - } + "description": "Learn more about our company", + }, } ``` @@ -422,7 +422,7 @@ Instructions for automatically filling dictionary content from external sources. **Examples:** -```json +```jsonc // Disable filling { "fill": false diff --git a/apps/app/.env.template b/apps/app/.env.template index 1a61a02932..3407681ba9 100644 --- a/apps/app/.env.template +++ b/apps/app/.env.template @@ -71,4 +71,4 @@ VITE_STRIPE_ONE_TIME_PAYMENT_PRICE_ID= # Election # ####################################################################################### -GH_TOKEN= \ No newline at end of file +GH_TOKEN= diff --git a/apps/app/package.json b/apps/app/package.json index 13a45e711c..764e902ba7 100644 --- a/apps/app/package.json +++ b/apps/app/package.json @@ -68,6 +68,7 @@ "@tanstack/react-table": "9.2.4", "@tanstack/react-virtual": "3.14.10", "@tanstack/router-plugin": "1.168.35", + "@tanstack/start-static-server-functions": "^1.167.32", "defu": "6.1.7", "framer-motion": "13.1.1", "fuse.js": "7.5.0", diff --git a/apps/app/src/components/Dashboard/DashboardNavbar/OrganizationDropdown.tsx b/apps/app/src/components/Dashboard/DashboardNavbar/OrganizationDropdown.tsx index 7e3d4cbf75..16d00fe337 100644 --- a/apps/app/src/components/Dashboard/DashboardNavbar/OrganizationDropdown.tsx +++ b/apps/app/src/components/Dashboard/DashboardNavbar/OrganizationDropdown.tsx @@ -8,6 +8,7 @@ import { import { Button } from '@intlayer/design-system/button'; import { Container } from '@intlayer/design-system/container'; import { DropDown } from '@intlayer/design-system/drop-down'; +import { useIsMounted } from '@intlayer/design-system/hooks'; import { Modal } from '@intlayer/design-system/modal'; import { ChevronsUpDown } from 'lucide-react'; import { type FC, useState } from 'react'; @@ -16,6 +17,7 @@ import { OrganizationCreationForm } from '../OrganizationForm/OrganizationCreati export const OrganizationDropdown: FC = () => { const { session } = useSession(); + const isMounted = useIsMounted(); const { data: organizations } = useGetOrganizations(); const [isCreationModalOpen, setIsCreationModalOpen] = useState(false); const { mutate: selectOrganization, isPending: isSelectOrganizationLoading } = @@ -43,12 +45,21 @@ export const OrganizationDropdown: FC = () => { selectOrganization(organizationId); }; - const otherOrganizations = (organizations?.data ?? []) - .filter( - (organizationEl: any) => - String(organizationEl.id) !== String(organization?.id) - ) - .slice(0, 10); + // The organizations list is client-only: the API client authenticates with + // `credentials: 'include'`, which is a browser mechanism, so SSR always sees + // an empty list and renders the empty state. Holding the list back until + // after mount keeps the first client render identical to the server markup — + // otherwise the fetch resolving around hydration swaps the empty state for + // buttons mid-hydration and the tree is thrown away. The panel is closed at + // that point, so nothing is visibly deferred. + const otherOrganizations = isMounted + ? (organizations?.data ?? []) + .filter( + (organizationEl: any) => + String(organizationEl.id) !== String(organization?.id) + ) + .slice(0, 10) + : []; return organization ? ( <> diff --git a/apps/app/src/components/Dashboard/DashboardNavbar/ProjectDropdown.tsx b/apps/app/src/components/Dashboard/DashboardNavbar/ProjectDropdown.tsx index 3251c48249..caee5dc52a 100644 --- a/apps/app/src/components/Dashboard/DashboardNavbar/ProjectDropdown.tsx +++ b/apps/app/src/components/Dashboard/DashboardNavbar/ProjectDropdown.tsx @@ -7,6 +7,7 @@ import { import { Button } from '@intlayer/design-system/button'; import { Container } from '@intlayer/design-system/container'; import { DropDown } from '@intlayer/design-system/drop-down'; +import { useIsMounted } from '@intlayer/design-system/hooks'; import { Modal } from '@intlayer/design-system/modal'; import { ChevronsUpDown } from 'lucide-react'; import { type ComponentProps, type FC, useState } from 'react'; @@ -19,6 +20,7 @@ type ProjectDropdownProps = Partial> & { export const ProjectDropdown: FC = (props) => { const { session } = useSession(); + const isMounted = useIsMounted(); const { data: projects } = useGetProjects(); const { mutate: selectProject, isPending: isSelectProjectLoading } = @@ -43,9 +45,15 @@ export const ProjectDropdown: FC = (props) => { selectProject(projectId); }; - const otherProjects = (projects?.data ?? []) - .filter((projectEl: any) => String(projectEl.id) !== String(project?.id)) - .slice(0, 10); + // Client-only list held back until after mount — see `OrganizationDropdown` + // for why SSR can never populate it. + const otherProjects = isMounted + ? (projects?.data ?? []) + .filter( + (projectEl: any) => String(projectEl.id) !== String(project?.id) + ) + .slice(0, 10) + : []; return project ? ( <> diff --git a/apps/app/src/components/Dashboard/DashboardSidebar/DashboardSidebar.tsx b/apps/app/src/components/Dashboard/DashboardSidebar/DashboardSidebar.tsx index b7ff460790..df5e3ccc0c 100644 --- a/apps/app/src/components/Dashboard/DashboardSidebar/DashboardSidebar.tsx +++ b/apps/app/src/components/Dashboard/DashboardSidebar/DashboardSidebar.tsx @@ -628,11 +628,9 @@ export const DashboardSidebar: FC = ({ /> - {!isCollapsed && - !IS_SELF_HOSTED && - process.env.NODE_ENV === 'development' && ( - - )} + {!isCollapsed && !IS_SELF_HOSTED && import.meta.env.DEV && ( + + )} {/* Environment switcher — shown when project has >1 environments */} {environments.length > 1 && diff --git a/apps/app/src/components/Dashboard/DashboardSidebar/ReviewerMarketplaceBanner.tsx b/apps/app/src/components/Dashboard/DashboardSidebar/ReviewerMarketplaceBanner.tsx index f76ca7dc5f..0002b3f977 100644 --- a/apps/app/src/components/Dashboard/DashboardSidebar/ReviewerMarketplaceBanner.tsx +++ b/apps/app/src/components/Dashboard/DashboardSidebar/ReviewerMarketplaceBanner.tsx @@ -1,10 +1,9 @@ -import {} from '@intlayer/design-system/api'; import { Button } from '@intlayer/design-system/button'; import { Container } from '@intlayer/design-system/container'; -import { usePersistedStore } from '@intlayer/design-system/hooks'; +import { useIsMounted, usePersistedStore } from '@intlayer/design-system/hooks'; import { App_ReviewerMarketplace_Path } from '@intlayer/design-system/routes'; import { X } from 'lucide-react'; -import { type FC, useEffect } from 'react'; +import type { FC } from 'react'; import { useIntlayer } from 'react-intlayer'; import { Link } from '#components/Link/Link'; @@ -12,16 +11,17 @@ const STORAGE_KEY = 'isReviewerMarketplaceBannerClosed'; export const ReviewerMarketplaceBanner: FC = () => { const { reviewerMarketplace } = useIntlayer('dashboard-sidebar'); - const [isVisible, setIsVisible] = usePersistedStore(STORAGE_KEY, false); + // The key stores the dismissal, as its name says. It previously held + // `isVisible`, so closing the banner persisted `false`, which the mount + // effect then read as "not closed" and re-opened — the banner could never be + // dismissed for good. + const [isClosed, setIsClosed] = usePersistedStore(STORAGE_KEY, false); + const isMounted = useIsMounted(); - useEffect(() => { - const isClosed = localStorage.getItem(STORAGE_KEY) === 'true'; - if (!isClosed) { - setIsVisible(true); - } - }, []); - - if (!isVisible) return <>; + // Stays hidden until mount: the server cannot know about a dismissal, and + // rendering before `usePersistedStore` has read it back would flash a banner + // the user already closed. + if (!isMounted || isClosed) return <>; return ( { variant="hoverable" size="icon-sm" Icon={X} - onClick={() => setIsVisible(false)} + onClick={() => setIsClosed(true)} /> diff --git a/apps/app/src/components/Dashboard/DashboardSkeleton.tsx b/apps/app/src/components/Dashboard/DashboardSkeleton.tsx index 798c4ce9dd..9c226ca35f 100644 --- a/apps/app/src/components/Dashboard/DashboardSkeleton.tsx +++ b/apps/app/src/components/Dashboard/DashboardSkeleton.tsx @@ -3,7 +3,7 @@ import type { FC } from 'react'; import { Skeleton } from '#components/Skeleton'; export const DashboardSkeleton: FC = () => ( -
+
{/* Navbar Skeleton */} = ({ reviewer }) => { transparency="full" className="flex flex-1 flex-col overflow-hidden" > -
+

{content.contact} {reviewer.name ?? 'reviewer'}

diff --git a/apps/app/src/components/SwitchThemeSwitcher.tsx b/apps/app/src/components/SwitchThemeSwitcher.tsx index 290804e508..e354aea270 100644 --- a/apps/app/src/components/SwitchThemeSwitcher.tsx +++ b/apps/app/src/components/SwitchThemeSwitcher.tsx @@ -1,3 +1,4 @@ +import { useIsMounted } from '@intlayer/design-system/hooks'; import { SwitchSelector, type SwitchSelectorChoices, @@ -9,6 +10,11 @@ import { useTheme } from '#/providers/ThemeProvider'; export const SwitchThemeSwitcher: FC = () => { const { resolvedTheme, setTheme } = useTheme(); + const isMounted = useIsMounted(); + + if (!isMounted) { + return null; + } const themeSwitcher = [ { diff --git a/apps/app/src/hooks/useDashboardRightPanel.ts b/apps/app/src/hooks/useDashboardRightPanel.ts index eb8658a73a..0e7a2cb910 100644 --- a/apps/app/src/hooks/useDashboardRightPanel.ts +++ b/apps/app/src/hooks/useDashboardRightPanel.ts @@ -2,6 +2,13 @@ import { useSyncExternalStore } from 'react'; type PanelState = { activePanel: string | null }; +/** + * Snapshot handed to the server render and to hydration. It has to be the same + * reference on every call, otherwise `useSyncExternalStore` sees a new value + * each render and loops. + */ +const SERVER_PANEL_STATE: PanelState = { activePanel: null }; + class PanelObservable { private listeners = new Set<() => void>(); private state: PanelState = { activePanel: null }; @@ -15,6 +22,8 @@ class PanelObservable { getSnapshot = () => this.state; + getServerSnapshot = () => SERVER_PANEL_STATE; + open = (id: string) => { const next = this.state.activePanel === id ? null : id; if (this.state.activePanel === next) return; @@ -29,7 +38,9 @@ class PanelObservable { }; private emit = () => { - this.listeners.forEach((l) => l()); + this.listeners.forEach((listener) => { + listener(); + }); }; } @@ -39,7 +50,7 @@ export const useDashboardRightPanel = () => { const state = useSyncExternalStore( dashboardRightPanelManager.subscribe, dashboardRightPanelManager.getSnapshot, - () => ({ activePanel: null }) as PanelState + dashboardRightPanelManager.getServerSnapshot ); return { diff --git a/apps/app/src/hooks/useVisualEditorKeys.ts b/apps/app/src/hooks/useVisualEditorKeys.ts index 1df65df039..4a6dee5232 100644 --- a/apps/app/src/hooks/useVisualEditorKeys.ts +++ b/apps/app/src/hooks/useVisualEditorKeys.ts @@ -1,5 +1,12 @@ import { useSyncExternalStore } from 'react'; +/** + * Snapshot handed to the server render and to hydration. It has to be the same + * reference on every call, otherwise `useSyncExternalStore` sees a new value + * each render and loops. + */ +const SERVER_KEYS: string[] = []; + class DisplayedKeysObservable { private listeners = new Set<() => void>(); private state: string[] = []; @@ -13,6 +20,8 @@ class DisplayedKeysObservable { getSnapshot = (): string[] => this.state; + getServerSnapshot = (): string[] => SERVER_KEYS; + setKeys = (keys: string[]) => { if ( keys.length === this.state.length && @@ -32,5 +41,5 @@ export const useVisualEditorKeys = (): string[] => useSyncExternalStore( visualEditorKeysManager.subscribe, visualEditorKeysManager.getSnapshot, - () => [] + visualEditorKeysManager.getServerSnapshot ); diff --git a/apps/app/src/router.tsx b/apps/app/src/router.tsx index 5ff9e9e51d..e9edfe27e1 100644 --- a/apps/app/src/router.tsx +++ b/apps/app/src/router.tsx @@ -1,6 +1,7 @@ import { Loader } from '@intlayer/design-system/loader'; import { getQueryClient } from '@intlayer/design-system/providers'; import { createRouter as createTanStackRouter } from '@tanstack/react-router'; +import { setupRouterSsrQueryIntegration } from '@tanstack/react-router-ssr-query'; import { NotFoundComponent } from '#components/NotFoundComponent'; import { routeTree } from './routeTree.gen'; @@ -29,6 +30,22 @@ export function getRouter() { defaultPendingComponent: Loader, }); + /** + * Dehydrates the query cache into the SSR payload and rehydrates it before + * the first client render. Without it, loaders such as the root's + * `sessionQueryOptions` prime `['session']` on the server only, so the server + * markup renders session-dependent UI (organization/project dropdowns) that + * the first client render — reading an empty cache — omits, and hydration + * fails. + */ + setupRouterSsrQueryIntegration({ + router, + queryClient, + // `ReactQueryProvider` in `__root.tsx` already provides this same client, + // along with the toast and invalidation wiring the integration lacks. + wrapQueryClient: false, + }); + return router; } diff --git a/apps/app/src/routes/{-$locale}/_dashboard/route.tsx b/apps/app/src/routes/{-$locale}/_dashboard/route.tsx index 102cbfb91b..59bb1e0c4e 100644 --- a/apps/app/src/routes/{-$locale}/_dashboard/route.tsx +++ b/apps/app/src/routes/{-$locale}/_dashboard/route.tsx @@ -300,7 +300,7 @@ function DashboardLayout() { return (
diff --git a/apps/app/src/styles.css b/apps/app/src/styles.css index 265939dae2..7d04879ef6 100644 --- a/apps/app/src/styles.css +++ b/apps/app/src/styles.css @@ -1,5 +1,5 @@ -@import "@intlayer/design-system/css"; @import "tailwindcss"; +@import "@intlayer/design-system/css"; @import "./shiki.css"; @import "./monaco.css"; /* MarkdownEditor (Novel/Tiptap) styles — only the CMS dashboard uses that @@ -8,297 +8,134 @@ @import "@intlayer/design-system/markdown-editor.css"; @layer base { - html { - font-size: 12px; /* Reduced base font size to match previous Next.js app scaling */ - } - - /* 1. Standard Scale (Landing Page) */ - :root { - --text-3xl: 1.875rem; /* 30px */ - --text-3xl-leading: 2.25rem; /* 36px */ - - /* Add text-base for context */ - --text-base: 1rem; /* 16px */ - --text-base-leading: 1.5rem; /* 24px */ - } -} - -@theme { - --font-sans: "Geist", "Inter", sans-serif; - - --color-background: rgba(255, 255, 255); - --color-background-opposite: rgba(18, 18, 18); - - --color-text: rgba(18, 18, 18); - --color-text-opposite: rgba(255, 255, 255); - --color-text-dark: rgba(255, 255, 255); - --color-text-light: rgba(18, 18, 18); - - --color-card: rgba(231, 231, 231); - --color-card-hover: rgba(231, 231, 231); - - --color-hypertext: rgba(203, 235, 64); - --color-hypertext-hover: rgba(197, 101, 212); - --color-hypertext-active: rgba(203, 235, 64); - - --color-shadow: rgba(18, 18, 18); - - --color-primary: rgba(203, 235, 64); - --color-primary-50: rgba(249, 253, 230); - --color-primary-100: rgba(238, 248, 186); - --color-primary-200: rgba(226, 243, 143); - --color-primary-300: rgba(215, 239, 105); - --color-primary-400: rgba(203, 235, 64); - --color-primary-500: rgba(182, 211, 57); - --color-primary-600: rgba(153, 177, 48); - --color-primary-700: rgba(116, 134, 37); - --color-primary-800: rgba(92, 106, 29); - --color-primary-900: rgba(67, 77, 21); - --color-primary-950: rgba(40, 46, 12); + html { + font-size: 12px; + /* Reduced base font size to match previous Next.js app scaling */ + } - --color-secondary: rgba(255, 230, 109); - --color-secondary-50: rgba(254, 251, 232); - --color-secondary-100: rgba(255, 246, 194); - --color-secondary-200: rgba(255, 230, 109); - --color-secondary-300: rgba(255, 216, 69); - --color-secondary-400: rgba(252, 193, 19); - --color-secondary-500: rgba(236, 168, 6); - --color-secondary-600: rgba(204, 129, 2); - --color-secondary-700: rgba(162, 90, 6); - --color-secondary-800: rgba(134, 71, 13); - --color-secondary-900: rgba(114, 58, 17); - --color-secondary-950: rgba(67, 29, 5); + /* 1. Standard Scale (Landing Page) */ + :root { + --text-3xl: 1.875rem; + /* 30px */ + --text-3xl-leading: 2.25rem; + /* 36px */ - --color-neutral: rgba(93, 93, 93); - --color-neutral-50: rgba(249, 249, 249); - --color-neutral-100: rgba(231, 231, 231); - --color-neutral-200: rgba(209, 209, 209); - --color-neutral-300: rgba(176, 176, 176); - --color-neutral-400: rgba(136, 136, 136); - --color-neutral-500: rgba(109, 109, 109); - --color-neutral-600: rgba(93, 93, 93); - --color-neutral-700: rgba(79, 79, 79); - --color-neutral-800: rgba(69, 69, 69); - --color-neutral-900: rgba(61, 61, 61); - --color-neutral-950: rgba(48, 48, 48); - - --color-success: rgba(0, 204, 102); - --color-success-50: rgba(254, 244, 255); - --color-success-100: rgba(252, 232, 255); - --color-success-200: rgba(248, 208, 254); - --color-success-300: rgba(241, 171, 252); - --color-success-400: rgba(203, 235, 64); - --color-success-500: rgba(197, 101, 212); - --color-success-600: rgba(163, 81, 175); - --color-success-700: rgba(128, 61, 139); - --color-success-800: rgba(93, 41, 102); - --color-success-900: rgba(59, 21, 65); - - --color-warning: rgba(214, 153, 66); - --color-warning-50: rgba(254, 251, 232); - --color-warning-100: rgba(255, 246, 194); - --color-warning-200: rgba(255, 230, 109); - --color-warning-300: rgba(255, 216, 69); - --color-warning-400: rgba(252, 193, 19); - --color-warning-500: rgba(236, 168, 6); - --color-warning-600: rgba(204, 129, 2); - --color-warning-700: rgba(162, 90, 6); - --color-warning-800: rgba(134, 71, 13); - --color-warning-900: rgba(114, 58, 17); - - --color-error: rgba(181, 24, 13); - --color-error-50: rgba(255, 245, 237); - --color-error-100: rgba(255, 232, 213); - --color-error-200: rgba(254, 206, 170); - --color-error-300: rgba(253, 171, 116); - --color-error-400: rgba(251, 125, 60); - --color-error-500: rgba(249, 90, 22); - --color-error-600: rgba(234, 63, 12); - --color-error-700: rgba(194, 45, 12); - --color-error-800: rgba(154, 37, 18); - --color-error-900: rgba(124, 33, 18); - - --color-white: rgba(255, 255, 255); - - --color-black: rgba(0, 0, 0); + /* Add text-base for context */ + --text-base: 1rem; + /* 16px */ + --text-base-leading: 1.5rem; + /* 24px */ + } } -@variant dark -( -:where([data-theme="dark"], [data-theme="dark"] *) &); -@variant light (:where([data-theme="light"], [data-theme="light"] *) &); - -:root, -[data-theme="light"] { - --font-sans: "Geist", "Inter", sans-serif; - - --color-background: rgba(255, 255, 255); - --color-background-opposite: rgba(18, 18, 18); - - --color-text: rgba(18, 18, 18); - --color-text-opposite: rgba(255, 255, 255); - --color-text-dark: rgba(255, 255, 255); - --color-text-light: rgba(18, 18, 18); - - --color-card: rgba(231, 231, 231); - --color-card-hover: rgba(231, 231, 231); - - --color-hypertext: rgba(203, 235, 64); - --color-hypertext-hover: rgba(197, 101, 212); - --color-hypertext-active: rgba(203, 235, 64); - - --color-shadow: rgba(18, 18, 18); - - --color-primary: rgba(203, 235, 64); - --color-secondary: rgba(255, 230, 109); - --color-neutral: rgba(93, 93, 93); - - --color-success: rgba(0, 204, 102); - --color-warning: rgba(214, 153, 66); - --color-error: rgba(181, 24, 13); - - --color-white: rgba(255, 255, 255); - --color-black: rgba(0, 0, 0); - - --navbar-height: 64px; -} - -[data-theme="dark"] { - --font-sans: "Geist", "Inter", sans-serif; - - --color-background: rgba(23, 23, 23); - --color-background-opposite: rgba(255, 255, 255); - - --color-text: rgba(255, 245, 237); - --color-text-opposite: rgba(18, 18, 18); - - --color-card: rgba(39, 39, 39); - --color-card-hover: rgba(79, 79, 79); - - --color-hypertext: rgba(203, 235, 64); - --color-hypertext-hover: rgba(197, 101, 212); - --color-hypertext-active: rgba(203, 235, 64); - - --color-shadow: rgba(18, 18, 18); - - --color-primary: rgba(203, 235, 64); - --color-secondary: rgba(255, 230, 109); - --color-neutral: rgba(93, 93, 93); - - --color-success: rgba(0, 204, 102); - --color-warning: rgba(214, 153, 66); - --color-error: rgba(181, 24, 13); - - --color-white: rgba(255, 255, 255); - --color-black: rgba(0, 0, 0); - - --navbar-height: 64px; -} /* Price range slider thumb styles */ .price-range-thumb { - -webkit-appearance: none; - appearance: none; - pointer-events: none; - background: transparent; - outline: none; + -webkit-appearance: none; + appearance: none; + pointer-events: none; + background: transparent; + outline: none; } + .price-range-thumb::-webkit-slider-thumb { - -webkit-appearance: none; - appearance: none; - pointer-events: all; - width: 18px; - height: 18px; - border-radius: 50%; - background: var(--color-text); - border: 2px solid var(--color-card); - cursor: pointer; - box-shadow: 0 1px 3px rgba(0, 0, 0, 0.2); + -webkit-appearance: none; + appearance: none; + pointer-events: all; + width: 18px; + height: 18px; + border-radius: 50%; + background: var(--color-text); + border: 2px solid var(--color-card); + cursor: pointer; + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.2); } + .price-range-thumb::-moz-range-thumb { - pointer-events: all; - width: 18px; - height: 18px; - border-radius: 50%; - background: var(--color-text); - border: 2px solid var(--color-card); - cursor: pointer; - box-shadow: 0 1px 3px rgba(0, 0, 0, 0.2); + pointer-events: all; + width: 18px; + height: 18px; + border-radius: 50%; + background: var(--color-text); + border: 2px solid var(--color-card); + cursor: pointer; + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.2); } + .price-range-thumb::-webkit-slider-runnable-track { - background: transparent; + background: transparent; } + .price-range-thumb::-moz-range-track { - background: transparent; + background: transparent; } -@plugin '@tailwindcss/aspect-ratio'; - -@source "./src/**/*.{ts,tsx,svg}"; -@source "../node_modules/@intlayer/design-system/dist/**/*.{js,jsx,mjs,ts,tsx,svg}"; - @keyframes shrink-title { - to { - font-size: 1.2rem; - padding-top: 0.8rem; - padding-bottom: 0.6rem; - } + to { + font-size: 1.2rem; + padding-top: 0.8rem; + padding-bottom: 0.6rem; + } } @keyframes infiniteScroll { - from { - transform: translateX(0); - } - to { - transform: translateX(-50%); - } + from { + transform: translateX(0); + } + + to { + transform: translateX(-50%); + } } @keyframes infiniteScrollInverse { - from { - transform: translateX(-50%); - } - to { - transform: translateX(0); - } + from { + transform: translateX(-50%); + } + + to { + transform: translateX(0); + } } @keyframes float { - 0%, - 100% { - transform: translateY(0); - } - 50% { - transform: translateY(var(--float-distance, 10px)); - } + + 0%, + 100% { + transform: translateY(0); + } + + 50% { + transform: translateY(var(--float-distance, 10px)); + } } .horizontal-loop-1 { - animation-name: infiniteScroll; - animation-duration: 85s; - animation-iteration-count: infinite; - animation-timing-function: linear; + animation-name: infiniteScroll; + animation-duration: 85s; + animation-iteration-count: infinite; + animation-timing-function: linear; } .horizontal-loop-2 { - animation-name: infiniteScrollInverse; - animation-duration: 45s; - animation-iteration-count: infinite; - animation-timing-function: linear; + animation-name: infiniteScrollInverse; + animation-duration: 45s; + animation-iteration-count: infinite; + animation-timing-function: linear; } .horizontal-loop-4 { - animation-name: infiniteScroll; - animation-duration: 50s; - animation-iteration-count: infinite; - animation-timing-function: linear; + animation-name: infiniteScroll; + animation-duration: 50s; + animation-iteration-count: infinite; + animation-timing-function: linear; } .animate-float { - animation-name: float; - animation-duration: 3s; - animation-iteration-count: infinite; - animation-timing-function: ease-in-out; + animation-name: float; + animation-duration: 3s; + animation-iteration-count: infinite; + animation-timing-function: ease-in-out; } diff --git a/apps/website/.env.template b/apps/website/.env.template index e296946a08..197e71af7f 100644 --- a/apps/website/.env.template +++ b/apps/website/.env.template @@ -15,6 +15,8 @@ VITE_BACKEND_URL=https://back.intlayer.org VITE_ENABLE_SERVICE_WORKER=false +DISABLE_OPTIMIZATION=false + ####################################################################################### # PostHog # ####################################################################################### diff --git a/apps/website/Dockerfile b/apps/website/Dockerfile index 2bdfd4bb1e..89b93134ae 100644 --- a/apps/website/Dockerfile +++ b/apps/website/Dockerfile @@ -19,6 +19,7 @@ ARG VITE_BACKEND_DOMAIN ARG VITE_BACKEND_URL ARG VITE_ENABLE_SERVICE_WORKER ARG INTLAYER_BACKEND_URL +ARG DISABLE_OPTIMIZATION ENV INTLAYER_CLIENT_ID=${INTLAYER_CLIENT_ID} ENV INTLAYER_CLIENT_SECRET=${INTLAYER_CLIENT_SECRET} @@ -29,6 +30,8 @@ ENV VITE_CMS_URL=${VITE_CMS_URL} ENV VITE_BACKEND_DOMAIN=${VITE_BACKEND_DOMAIN} ENV VITE_BACKEND_URL=${VITE_BACKEND_URL} ENV VITE_ENABLE_SERVICE_WORKER=${VITE_ENABLE_SERVICE_WORKER} +ENV INTLAYER_BACKEND_URL=${INTLAYER_BACKEND_URL} +ENV DISABLE_OPTIMIZATION=${DISABLE_OPTIMIZATION} ENV CI=true # Create app directory diff --git a/apps/website/scripts/compress-static.ts b/apps/website/scripts/compress-static.ts index 43cd714292..48ef01d5d5 100644 --- a/apps/website/scripts/compress-static.ts +++ b/apps/website/scripts/compress-static.ts @@ -347,5 +347,7 @@ export const compressDirectory = async ( * `compressDirectory`. */ if (import.meta.main) { - await compressDirectory(PUBLIC_DIRECTORY, 'prerendered output'); + if (process.env.DISABLE_OPTIMIZATION !== 'true') { + await compressDirectory(PUBLIC_DIRECTORY, 'prerendered output'); + } } diff --git a/apps/website/scripts/inline-critical-css.ts b/apps/website/scripts/inline-critical-css.ts index 5d7568fd0e..0ff7b0266c 100644 --- a/apps/website/scripts/inline-critical-css.ts +++ b/apps/website/scripts/inline-critical-css.ts @@ -244,17 +244,15 @@ const inlineCriticalCss = async (): Promise => { try { allFiles = await listFilesRecursively(PUBLIC_DIRECTORY); } catch { - console.error( - ` ✗ ${relative(process.cwd(), PUBLIC_DIRECTORY)} not found — nothing inlined.` + console.log( + ` ℹ ${relative(process.cwd(), PUBLIC_DIRECTORY)} not found — skipping inlining.` ); - process.exitCode = 1; return; } const pages = allFiles.filter((file) => file.endsWith('.html')); if (pages.length === 0) { - console.error(' ✗ No prerendered pages found — nothing inlined.'); - process.exitCode = 1; + console.log(' ℹ No prerendered pages found — skipping inlining.'); return; } @@ -318,4 +316,8 @@ const inlineCriticalCss = async (): Promise => { * Importing this file (from the test) only pulls in `inlineStylesheetLinks`; * running it directly is the `postbuild` pass over the whole output. */ -if (import.meta.main) await inlineCriticalCss(); +if (import.meta.main) { + if (process.env.DISABLE_OPTIMIZATION !== 'true') { + await inlineCriticalCss(); + } +} diff --git a/apps/website/src/components/BlogPage/BlogCommentSection.tsx b/apps/website/src/components/BlogPage/BlogCommentSection.tsx index ccd810a527..e198896a9d 100644 --- a/apps/website/src/components/BlogPage/BlogCommentSection.tsx +++ b/apps/website/src/components/BlogPage/BlogCommentSection.tsx @@ -157,7 +157,7 @@ const CommentForm: FC = ({
-

+

{content.commentsAreModeratedAndWill}

@@ -196,11 +196,13 @@ export const BlogCommentSection: FC = ({ return (
-

{content.comments}

+

+ {content.comments} +

{/* Approved comments list */} {comments.length === 0 && submitState !== 'success' ? ( -

+

{content.noCommentsYetBeThe}

) : ( @@ -216,17 +218,17 @@ export const BlogCommentSection: FC = ({ gap="sm" >
- + {comment.authorName}
-

+

{comment.content}

diff --git a/apps/website/src/components/BlogPage/BlogNavList.tsx b/apps/website/src/components/BlogPage/BlogNavList.tsx index a405b933c8..a9bfe290f1 100644 --- a/apps/website/src/components/BlogPage/BlogNavList.tsx +++ b/apps/website/src/components/BlogPage/BlogNavList.tsx @@ -123,7 +123,7 @@ export const BlogNavListContent: FC = ({ label={key2} to={sectionDefault?.relativeUrl ?? ''} isActive={isSelfActive && !isSubSectionActive} - className="block w-full flex-row items-center text-nowrap p-2 text-left text-sm transition-colors hover:text-text" + className="block w-full flex-row items-center text-nowrap p-2 text-left text-sm transition-colors hover:text-foreground" > {section2Data?.title} @@ -136,7 +136,7 @@ export const BlogNavListContent: FC = ({
{subSections2 && Object.keys(subSections2).length > 0 && ( -
+
{Object.keys(subSections2).map((key3) => { const section3Data = subSections2[key3]; const slugs = @@ -157,7 +157,7 @@ export const BlogNavListContent: FC = ({ '' } isActive={isActive} - className="block w-full flex-row items-center text-nowrap p-2 text-left text-xs transition-colors hover:text-text" + className="block w-full flex-row items-center text-nowrap p-2 text-left text-xs transition-colors hover:text-foreground" > {section3Data.title} @@ -170,7 +170,7 @@ export const BlogNavListContent: FC = ({ ) : ( diff --git a/apps/website/src/components/BlogPage/RelatedPosts.tsx b/apps/website/src/components/BlogPage/RelatedPosts.tsx index 1363f38f2a..244a24dddb 100644 --- a/apps/website/src/components/BlogPage/RelatedPosts.tsx +++ b/apps/website/src/components/BlogPage/RelatedPosts.tsx @@ -81,7 +81,7 @@ export const RelatedPosts: FC = ({ return (
-

+

{content.relatedPosts}

@@ -93,16 +93,16 @@ export const RelatedPosts: FC = ({ label={content.visitBlogTitle({ title: post.title })} className="group flex flex-col gap-2.5 py-5 no-underline sm:px-5 last:sm:pr-0 first:sm:pl-0" > -

+

{post.title}

{post.description && ( -

+

{post.description}

)} {post.author && ( -

+

{post.author.name}

)} @@ -149,7 +149,7 @@ export const LastPosts: FC = ({ return (
-

+

{content.lastPosts}

@@ -161,16 +161,16 @@ export const LastPosts: FC = ({ label={content.visitBlogTitle({ title: post.title })} className="group flex flex-col gap-2.5 py-5 no-underline sm:px-5 last:sm:pr-0 first:sm:pl-0" > -

+

{post.title}

{post.description && ( -

+

{post.description}

)} {post.author && ( -

+

{post.author.name}

)} diff --git a/apps/website/src/components/BlogPage/template.html b/apps/website/src/components/BlogPage/template.html index 2fca8e836c..c124a3e453 100644 --- a/apps/website/src/components/BlogPage/template.html +++ b/apps/website/src/components/BlogPage/template.html @@ -4659,7 +4659,7 @@ href="/en/for-developers/" class="group/card flex items-start gap-3 p-3 rounded-lg hover:bg-mist-50 transition-colors group-data-[state=open]/panel:animate-in group-data-[state=open]/panel:fade-in-0 group-data-[state=open]/panel:slide-in-from-top-1 group-data-[state=open]/panel:duration-300 group-data-[state=open]/panel:fill-mode-both [animation-delay:40ms]" >

astro.config.mjs Setup

The entry point is your Astro config file:

File Structure under src/pages/: