fix: board not found flicker on reloading public boards (#149)

* fix: board not found flicker on reloading public boards

* fix: using netx-theme's ThemeProvider

* chore: installing next-theme

* fix: theme issue on landing page

* fix: image theme hydration issue
This commit is contained in:
Ridham Khandar
2025-08-22 20:58:59 +05:30
committed by GitHub
parent ba2fd7f695
commit 3233c35836
11 changed files with 53 additions and 40 deletions

View File

@@ -49,6 +49,7 @@
"next": "^15.3.4", "next": "^15.3.4",
"next-logger": "^5.0.1", "next-logger": "^5.0.1",
"next-runtime-env": "^1.7.2", "next-runtime-env": "^1.7.2",
"next-themes": "^0.4.6",
"nextjs-cors": "^2.2.0", "nextjs-cors": "^2.2.0",
"pino": "^9.6.0", "pino": "^9.6.0",
"posthog-js": "^1.254.0", "posthog-js": "^1.254.0",

View File

@@ -10,7 +10,7 @@ import { authClient } from "@kan/auth/client";
import { useClickOutside } from "~/hooks/useClickOutside"; import { useClickOutside } from "~/hooks/useClickOutside";
import { useModal } from "~/providers/modal"; import { useModal } from "~/providers/modal";
import { useTheme } from "~/providers/theme"; import { useTheme } from "next-themes";
import { WorkspaceProvider } from "~/providers/workspace"; import { WorkspaceProvider } from "~/providers/workspace";
import FeedbackModal from "./FeedbackModal"; import FeedbackModal from "./FeedbackModal";
import Modal from "./modal"; import Modal from "./modal";
@@ -41,7 +41,7 @@ export default function Dashboard({
rightPanel, rightPanel,
hasRightPanel = false, hasRightPanel = false,
}: DashboardProps) { }: DashboardProps) {
const theme = useTheme(); const { theme } = useTheme();
const { modalContentType } = useModal(); const { modalContentType } = useModal();
const { data: session, isPending: sessionLoading } = authClient.useSession(); const { data: session, isPending: sessionLoading } = authClient.useSession();
@@ -90,7 +90,7 @@ export default function Dashboard({
} }
}); });
const isDarkMode = theme.activeTheme === "dark"; const isDarkMode = theme === "dark";
return ( return (
<> <>

View File

@@ -18,7 +18,7 @@ import settingsIconLight from "~/assets/settings-light.json";
import ReactiveButton from "~/components/ReactiveButton"; import ReactiveButton from "~/components/ReactiveButton";
import UserMenu from "~/components/UserMenu"; import UserMenu from "~/components/UserMenu";
import WorkspaceMenu from "~/components/WorkspaceMenu"; import WorkspaceMenu from "~/components/WorkspaceMenu";
import { useTheme } from "~/providers/theme"; import { useTheme } from "next-themes";
interface SideNavigationProps { interface SideNavigationProps {
user: UserType; user: UserType;
@@ -59,9 +59,9 @@ export default function SideNavigation({
const { pathname } = router; const { pathname } = router;
const { activeTheme } = useTheme(); const { theme, resolvedTheme } = useTheme();
const isDarkMode = activeTheme === "dark"; const isDarkMode = resolvedTheme === "dark";
const navigation = [ const navigation = [
{ {

View File

@@ -1,23 +1,22 @@
import { CgDarkMode } from "react-icons/cg"; import { CgDarkMode } from "react-icons/cg";
import { useTheme } from "next-themes";
import { useTheme } from "~/providers/theme";
const ThemeToggle = () => { const ThemeToggle = () => {
const { themePreference, switchTheme } = useTheme(); const { theme, setTheme } = useTheme();
const toggleTheme = () => { const toggleTheme = () => {
switchTheme(themePreference === "light" ? "dark" : "light"); setTheme(theme === "light" ? "dark" : "light");
}; };
return ( return (
<button <button
onClick={toggleTheme} onClick={toggleTheme}
className="rounded p-1.5 transition-all hover:bg-light-200 dark:hover:bg-dark-100" className="rounded p-1.5 transition-all hover:bg-light-200 dark:hover:bg-dark-100"
aria-label={`Switch to ${themePreference === "light" ? "dark" : "light"} theme`} aria-label={`Switch to ${theme === "light" ? "dark" : "light"} theme`}
> >
<CgDarkMode <CgDarkMode
className={`h-4 w-4 text-light-900 transition-transform duration-200 dark:text-dark-900 ${ className={`h-4 w-4 text-light-900 transition-transform duration-200 dark:text-dark-900 ${
themePreference === "dark" ? "rotate-180" : "rotate-0" theme === "dark" ? "rotate-180" : "rotate-0"
}`} }`}
/> />
</button> </button>

View File

@@ -10,7 +10,7 @@ import { authClient } from "@kan/auth/client";
import { useIsMobile } from "~/hooks/useMediaQuery"; import { useIsMobile } from "~/hooks/useMediaQuery";
import { useModal } from "~/providers/modal"; import { useModal } from "~/providers/modal";
import { useTheme } from "~/providers/theme"; import { useTheme } from "next-themes";
import { getAvatarUrl } from "~/utils/helpers"; import { getAvatarUrl } from "~/utils/helpers";
interface UserMenuProps { interface UserMenuProps {
@@ -29,7 +29,7 @@ export default function UserMenu({
onCloseSideNav, onCloseSideNav,
}: UserMenuProps) { }: UserMenuProps) {
const router = useRouter(); const router = useRouter();
const { themePreference, switchTheme } = useTheme(); const { theme, setTheme } = useTheme();
const { openModal } = useModal(); const { openModal } = useModal();
const isMobile = useIsMobile(); const isMobile = useIsMobile();
@@ -127,12 +127,12 @@ export default function UserMenu({
</div> </div>
<Menu.Item> <Menu.Item>
<button <button
onClick={() => switchTheme("system")} onClick={() => setTheme("system")}
className="flex w-full items-center rounded-[5px] px-3 py-2 text-left text-xs hover:bg-light-200 dark:hover:bg-dark-400" className="flex w-full items-center rounded-[5px] px-3 py-2 text-left text-xs hover:bg-light-200 dark:hover:bg-dark-400"
> >
<span <span
className={twMerge( className={twMerge(
themePreference === "system" ? "visible" : "invisible", theme === "system" ? "visible" : "invisible",
"mr-4 h-[6px] w-[6px] rounded-full bg-light-900 dark:bg-dark-900", "mr-4 h-[6px] w-[6px] rounded-full bg-light-900 dark:bg-dark-900",
)} )}
/> />
@@ -141,12 +141,12 @@ export default function UserMenu({
</Menu.Item> </Menu.Item>
<Menu.Item> <Menu.Item>
<button <button
onClick={() => switchTheme("dark")} onClick={() => setTheme("dark")}
className="flex w-full items-center rounded-[5px] px-3 py-2 text-left text-xs hover:bg-light-200 dark:hover:bg-dark-400" className="flex w-full items-center rounded-[5px] px-3 py-2 text-left text-xs hover:bg-light-200 dark:hover:bg-dark-400"
> >
<span <span
className={twMerge( className={twMerge(
themePreference === "dark" ? "visible" : "invisible", theme === "dark" ? "visible" : "invisible",
"mr-4 h-[6px] w-[6px] rounded-full bg-light-900 dark:bg-dark-900", "mr-4 h-[6px] w-[6px] rounded-full bg-light-900 dark:bg-dark-900",
)} )}
/> />
@@ -155,12 +155,12 @@ export default function UserMenu({
</Menu.Item> </Menu.Item>
<Menu.Item> <Menu.Item>
<button <button
onClick={() => switchTheme("light")} onClick={() => setTheme("light")}
className="flex w-full items-center rounded-[5px] px-3 py-2 text-left text-xs hover:bg-light-200 dark:hover:bg-dark-400" className="flex w-full items-center rounded-[5px] px-3 py-2 text-left text-xs hover:bg-light-200 dark:hover:bg-dark-400"
> >
<span <span
className={twMerge( className={twMerge(
themePreference === "light" ? "visible" : "invisible", theme === "light" ? "visible" : "invisible",
"mr-4 h-[6px] w-[6px] rounded-full bg-light-900 dark:bg-dark-900", "mr-4 h-[6px] w-[6px] rounded-full bg-light-900 dark:bg-dark-900",
)} )}
/> />

View File

@@ -14,7 +14,7 @@ import { useEffect } from "react";
import { LinguiProviderWrapper } from "~/providers/lingui"; import { LinguiProviderWrapper } from "~/providers/lingui";
import { ModalProvider } from "~/providers/modal"; import { ModalProvider } from "~/providers/modal";
import { PopupProvider } from "~/providers/popup"; import { PopupProvider } from "~/providers/popup";
import { ThemeProvider } from "~/providers/theme"; import { ThemeProvider } from "next-themes";
import { api } from "~/utils/api"; import { api } from "~/utils/api";
const jakarta = Plus_Jakarta_Sans({ const jakarta = Plus_Jakarta_Sans({
@@ -81,9 +81,9 @@ const MyApp: AppType = ({ Component, pageProps }: AppPropsWithLayout) => {
<script src="/__ENV.js" /> <script src="/__ENV.js" />
<main className="font-sans"> <main className="font-sans">
<LinguiProviderWrapper> <LinguiProviderWrapper>
<ThemeProvider> <ThemeProvider attribute="class" defaultTheme="system" enableSystem>
<ModalProvider> <ModalProvider>
<PopupProvider> <PopupProvider>
{posthogKey ? ( {posthogKey ? (
<PostHogProvider client={posthog}> <PostHogProvider client={posthog}>
{getLayout(<Component {...pageProps} />)} {getLayout(<Component {...pageProps} />)}
@@ -91,8 +91,8 @@ const MyApp: AppType = ({ Component, pageProps }: AppPropsWithLayout) => {
) : ( ) : (
getLayout(<Component {...pageProps} />) getLayout(<Component {...pageProps} />)
)} )}
</PopupProvider> </PopupProvider>
</ModalProvider> </ModalProvider>
</ThemeProvider> </ThemeProvider>
</LinguiProviderWrapper> </LinguiProviderWrapper>
</main> </main>

View File

@@ -1,17 +1,17 @@
import PatternedBackground from "~/components/PatternedBackground"; import PatternedBackground from "~/components/PatternedBackground";
import { useTheme } from "~/providers/theme"; import { useTheme } from "next-themes";
import Footer from "./Footer"; import Footer from "./Footer";
import Header from "./Header"; import Header from "./Header";
import { authClient } from "@kan/auth/client"; import { authClient } from "@kan/auth/client";
export default function Layout({ children }: { children: React.ReactNode }) { export default function Layout({ children }: { children: React.ReactNode }) {
const theme = useTheme(); const { theme } = useTheme();
const { data: session } = authClient.useSession(); const { data: session } = authClient.useSession();
const isLoggedIn = !!session?.user; const isLoggedIn = !!session?.user;
const isDarkMode = theme.activeTheme === "dark"; const isDarkMode = theme === "dark";
return ( return (
<> <>

View File

@@ -6,7 +6,7 @@ import { IoLogoGithub, IoLogoHackernews } from "react-icons/io";
import Button from "~/components/Button"; import Button from "~/components/Button";
import { PageHead } from "~/components/PageHead"; import { PageHead } from "~/components/PageHead";
import { useTheme } from "~/providers/theme"; import { useTheme } from "next-themes";
import Cta from "./components/Cta"; import Cta from "./components/Cta";
import FAQs from "./components/Faqs"; import FAQs from "./components/Faqs";
import Features from "./components/Features"; import Features from "./components/Features";
@@ -14,9 +14,10 @@ import Layout from "./components/Layout";
import Pricing from "./components/Pricing"; import Pricing from "./components/Pricing";
export default function HomeView() { export default function HomeView() {
const theme = useTheme(); const { resolvedTheme } = useTheme();
const isDarkMode = resolvedTheme === "dark";
const isDarkMode = theme.activeTheme === "dark";
return ( return (
<Layout> <Layout>
<PageHead title="Kan.bn | The open source alternative to Trello" /> <PageHead title="Kan.bn | The open source alternative to Trello" />
@@ -88,19 +89,27 @@ export default function HomeView() {
</div> </div>
<div className="px-4"> <div className="px-4">
<div className="mb-24 rounded-[16px] border border-light-300 bg-light-50 p-1 shadow-md dark:border-dark-300 dark:bg-dark-100 lg:rounded-[24px] lg:p-2"> <div className="mb-24 rounded-[16px] border border-light-300 bg-light-50 p-1 shadow-md dark:border-dark-300 dark:bg-dark-100 lg:rounded-[24px] lg:p-2">
<div className="overflow-hidden rounded-[12px] border border-light-300 shadow-sm dark:border-dark-300 lg:rounded-[16px]"> <div className="relative overflow-hidden rounded-[12px] border border-light-300 shadow-sm dark:border-dark-300 lg:rounded-[16px]">
<Image <Image
src={`/hero-${isDarkMode ? "dark" : "light"}.png`} src={`/hero-light.png`}
alt="kanban" alt="kanban"
width={1100} width={1100}
height={1000} height={1000}
className="block dark:hidden"
/>
<Image
src={`/hero-dark.png`}
alt="kanban"
width={1100}
height={1000}
className="hidden dark:block"
/> />
</div> </div>
</div> </div>
</div> </div>
<div className="relative pt-10"> <div className="relative pt-10">
<div id="features" className="absolute -top-20" /> <div id="features" className="absolute -top-20" />
<Features theme={theme.activeTheme} /> <Features theme={resolvedTheme === "dark" ? "dark" : "light"} />
</div> </div>
<div className="relative pt-10"> <div className="relative pt-10">
<div id="pricing" className="absolute -top-20" /> <div id="pricing" className="absolute -top-20" />
@@ -111,7 +120,7 @@ export default function HomeView() {
<FAQs /> <FAQs />
</div> </div>
<div className="relative"> <div className="relative">
<Cta theme={theme.activeTheme} /> <Cta theme={resolvedTheme ?? "light"} />
</div> </div>
</div> </div>
</Layout> </Layout>

View File

@@ -45,7 +45,7 @@ export default function PublicBoardView() {
members: formatToArray(router.query.members), members: formatToArray(router.query.members),
labels: formatToArray(router.query.labels), labels: formatToArray(router.query.labels),
}, },
{ enabled: !!boardSlug, placeholderData: keepPreviousData }, { enabled: router.isReady && !!boardSlug, placeholderData: keepPreviousData },
); );
const CopyBoardLink = () => { const CopyBoardLink = () => {
@@ -107,7 +107,7 @@ export default function PublicBoardView() {
<div className="relative h-full overflow-hidden rounded-md border pb-8 dark:border-dark-200"> <div className="relative h-full overflow-hidden rounded-md border pb-8 dark:border-dark-200">
<PatternedBackground /> <PatternedBackground />
<div className="z-10 flex w-full justify-between p-8"> <div className="z-10 flex w-full justify-between p-8">
{isLoading ? ( {isLoading || !router.isReady ? (
<div className="flex space-x-2"> <div className="flex space-x-2">
<div className="h-[2.3rem] w-[150px] animate-pulse rounded-[5px] bg-light-200 dark:bg-dark-100" /> <div className="h-[2.3rem] w-[150px] animate-pulse rounded-[5px] bg-light-200 dark:bg-dark-100" />
</div> </div>
@@ -134,13 +134,13 @@ export default function PublicBoardView() {
</div> </div>
<div className="scrollbar-w-none scrollbar-track-rounded-[4px] scrollbar-thumb-rounded-[4px] scrollbar-h-[8px] relative h-full flex-1 overflow-y-hidden overflow-x-scroll overscroll-contain scrollbar scrollbar-track-light-200 scrollbar-thumb-light-400 dark:scrollbar-track-dark-100 dark:scrollbar-thumb-dark-300"> <div className="scrollbar-w-none scrollbar-track-rounded-[4px] scrollbar-thumb-rounded-[4px] scrollbar-h-[8px] relative h-full flex-1 overflow-y-hidden overflow-x-scroll overscroll-contain scrollbar scrollbar-track-light-200 scrollbar-thumb-light-400 dark:scrollbar-track-dark-100 dark:scrollbar-thumb-dark-300">
{isLoading ? ( {isLoading || !router.isReady ? (
<div className="ml-[2rem] flex"> <div className="ml-[2rem] flex">
<div className="0 mr-5 h-[500px] w-[18rem] animate-pulse rounded-md bg-light-200 dark:bg-dark-100" /> <div className="0 mr-5 h-[500px] w-[18rem] animate-pulse rounded-md bg-light-200 dark:bg-dark-100" />
<div className="0 mr-5 h-[275px] w-[18rem] animate-pulse rounded-md bg-light-200 dark:bg-dark-100" /> <div className="0 mr-5 h-[275px] w-[18rem] animate-pulse rounded-md bg-light-200 dark:bg-dark-100" />
<div className="0 mr-5 h-[375px] w-[18rem] animate-pulse rounded-md bg-light-200 dark:bg-dark-100" /> <div className="0 mr-5 h-[375px] w-[18rem] animate-pulse rounded-md bg-light-200 dark:bg-dark-100" />
</div> </div>
) : !data ? ( ) : !data && !isLoading && router.isReady && !!boardSlug ? (
<div className="z-10 flex h-full w-full flex-col items-center justify-center space-y-8 pb-[150px]"> <div className="z-10 flex h-full w-full flex-col items-center justify-center space-y-8 pb-[150px]">
<div className="flex flex-col items-center"> <div className="flex flex-col items-center">
<HiOutlineLockClosed className="h-10 w-10 text-light-800 dark:text-dark-800" /> <HiOutlineLockClosed className="h-10 w-10 text-light-800 dark:text-dark-800" />

View File

@@ -4,6 +4,7 @@ import { fontFamily } from "tailwindcss/defaultTheme";
import baseConfig from "@kan/tailwind-config/web"; import baseConfig from "@kan/tailwind-config/web";
export default { export default {
darkMode: "class",
content: [...baseConfig.content], content: [...baseConfig.content],
plugins: [require("@tailwindcss/typography")], plugins: [require("@tailwindcss/typography")],
presets: [baseConfig], presets: [baseConfig],

3
pnpm-lock.yaml generated
View File

@@ -171,6 +171,9 @@ importers:
next-runtime-env: next-runtime-env:
specifier: ^1.7.2 specifier: ^1.7.2
version: 1.8.0 version: 1.8.0
next-themes:
specifier: ^0.4.6
version: 0.4.6(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
nextjs-cors: nextjs-cors:
specifier: ^2.2.0 specifier: ^2.2.0
version: 2.2.0(next@15.3.4(@babel/core@7.24.5)(babel-plugin-macros@3.1.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)) version: 2.2.0(next@15.3.4(@babel/core@7.24.5)(babel-plugin-macros@3.1.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))