️ Faster tab switches (#1701)

This commit is contained in:
Luke Vella 2025-04-29 22:34:52 +01:00 committed by GitHub
parent 486bd50139
commit 590e19ae16
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 59 additions and 29 deletions

View file

@ -1,10 +1,14 @@
{ {
"editor.codeActionsOnSave": { "editor.codeActionsOnSave": {
"source.fixAll": "explicit" "source.fixAll.biome": "explicit",
"source.organizeImports.biome": "explicit"
}, },
"[javascript]": { "[javascript]": {
"editor.defaultFormatter": "biomejs.biome" "editor.defaultFormatter": "biomejs.biome"
}, },
"[typescriptreact]": {
"editor.defaultFormatter": "biomejs.biome"
},
"typescript.tsdk": "node_modules/typescript/lib", "typescript.tsdk": "node_modules/typescript/lib",
"typescript.preferences.importModuleSpecifier": "shortest", "typescript.preferences.importModuleSpecifier": "shortest",
"cSpell.words": ["Rallly", "Vella"], "cSpell.words": ["Rallly", "Vella"],
@ -14,5 +18,6 @@
"typescript.preferences.preferTypeOnlyAutoImports": true, "typescript.preferences.preferTypeOnlyAutoImports": true,
"typescript.tsserver.log": "verbose", "typescript.tsserver.log": "verbose",
"typescript.tsserver.trace": "messages", "typescript.tsserver.trace": "messages",
"references.preferredLocation": "view" "references.preferredLocation": "view",
"editor.defaultFormatter": "biomejs.biome"
} }

View file

@ -1,4 +1,5 @@
"use client"; "use client";
import { cn } from "@rallly/ui";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@rallly/ui/page-tabs"; import { Tabs, TabsContent, TabsList, TabsTrigger } from "@rallly/ui/page-tabs";
import { useRouter, useSearchParams } from "next/navigation"; import { useRouter, useSearchParams } from "next/navigation";
import React from "react"; import React from "react";
@ -9,6 +10,8 @@ export function EventsTabbedView({ children }: { children: React.ReactNode }) {
const searchParams = useSearchParams(); const searchParams = useSearchParams();
const name = "status"; const name = "status";
const router = useRouter(); const router = useRouter();
const [isPending, startTransition] = React.useTransition();
const [tab, setTab] = React.useState(searchParams.get(name) ?? "upcoming");
const handleTabChange = React.useCallback( const handleTabChange = React.useCallback(
(value: string) => { (value: string) => {
const params = new URLSearchParams(searchParams); const params = new URLSearchParams(searchParams);
@ -17,15 +20,16 @@ export function EventsTabbedView({ children }: { children: React.ReactNode }) {
params.delete("page"); params.delete("page");
const newUrl = `?${params.toString()}`; const newUrl = `?${params.toString()}`;
router.replace(newUrl, { scroll: false }); startTransition(() => {
setTab(value);
router.replace(newUrl, { scroll: false });
});
}, },
[router, searchParams], [router, searchParams],
); );
const value = searchParams.get(name) ?? "upcoming";
return ( return (
<Tabs value={value} onValueChange={handleTabChange}> <Tabs value={tab} onValueChange={handleTabChange}>
<TabsList> <TabsList>
<TabsTrigger value="upcoming"> <TabsTrigger value="upcoming">
<Trans i18nKey="upcoming" defaults="Upcoming" /> <Trans i18nKey="upcoming" defaults="Upcoming" />
@ -34,7 +38,15 @@ export function EventsTabbedView({ children }: { children: React.ReactNode }) {
<Trans i18nKey="past" defaults="Past" /> <Trans i18nKey="past" defaults="Past" />
</TabsTrigger> </TabsTrigger>
</TabsList> </TabsList>
<TabsContent tabIndex={-1} value={value} key={value}> <TabsContent
tabIndex={-1}
value={tab}
key={tab}
className={cn(
"transition-opacity",
isPending ? "opacity-50 delay-200 pointer-events-none" : "",
)}
>
{children} {children}
</TabsContent> </TabsContent>
</Tabs> </Tabs>

View file

@ -138,13 +138,13 @@ export default async function Page({
</PageDescription> </PageDescription>
</PageHeader> </PageHeader>
<PageContent> <PageContent>
<EventsTabbedView> <div className="space-y-4">
<div className="space-y-4"> <SearchInput
<SearchInput placeholder={t("searchEventsPlaceholder", {
placeholder={t("searchEventsPlaceholder", { defaultValue: "Search events by title...",
defaultValue: "Search events by title...", })}
})} />
/> <EventsTabbedView>
<div className="space-y-6"> <div className="space-y-6">
{paginatedEvents.length === 0 && ( {paginatedEvents.length === 0 && (
<ScheduledEventEmptyState status={status} /> <ScheduledEventEmptyState status={status} />
@ -177,8 +177,8 @@ export default async function Page({
/> />
)} )}
</div> </div>
</div> </EventsTabbedView>
</EventsTabbedView> </div>
</PageContent> </PageContent>
</PageContainer> </PageContainer>
); );

View file

@ -168,13 +168,13 @@ export default async function Page({
</div> </div>
</div> </div>
<PageContent className="space-y-4"> <PageContent className="space-y-4">
<SearchInput
placeholder={t("searchPollsPlaceholder", {
defaultValue: "Search polls by title...",
})}
/>
<PollsTabbedView> <PollsTabbedView>
<div className="space-y-4"> <div className="space-y-4">
<SearchInput
placeholder={t("searchPollsPlaceholder", {
defaultValue: "Search polls by title...",
})}
/>
{polls.length === 0 ? ( {polls.length === 0 ? (
<PollsEmptyState /> <PollsEmptyState />
) : ( ) : (

View file

@ -1,14 +1,18 @@
"use client"; "use client";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@rallly/ui/page-tabs"; import { Tabs, TabsContent, TabsList, TabsTrigger } from "@rallly/ui/page-tabs";
import { useRouter, useSearchParams } from "next/navigation"; import { useRouter, useSearchParams } from "next/navigation";
import React from "react";
import { Trans } from "@/components/trans"; import { Trans } from "@/components/trans";
import { cn } from "@rallly/ui";
import React from "react";
export function PollsTabbedView({ children }: { children: React.ReactNode }) { export function PollsTabbedView({ children }: { children: React.ReactNode }) {
const searchParams = useSearchParams(); const searchParams = useSearchParams();
const name = "status"; const name = "status";
const router = useRouter(); const router = useRouter();
const [isPending, startTransition] = React.useTransition();
const [tab, setTab] = React.useState(searchParams.get(name) ?? "live");
const handleTabChange = React.useCallback( const handleTabChange = React.useCallback(
(value: string) => { (value: string) => {
const params = new URLSearchParams(searchParams); const params = new URLSearchParams(searchParams);
@ -16,16 +20,17 @@ export function PollsTabbedView({ children }: { children: React.ReactNode }) {
params.delete("page"); params.delete("page");
const newUrl = `?${params.toString()}`; startTransition(() => {
router.replace(newUrl, { scroll: false }); setTab(value);
const newUrl = `?${params.toString()}`;
router.replace(newUrl, { scroll: false });
});
}, },
[router, searchParams], [router, searchParams],
); );
const value = searchParams.get(name) ?? "live";
return ( return (
<Tabs value={value} onValueChange={handleTabChange}> <Tabs value={tab} onValueChange={handleTabChange}>
<TabsList> <TabsList>
<TabsTrigger value="live"> <TabsTrigger value="live">
<Trans i18nKey="pollStatusOpen" defaults="Live" /> <Trans i18nKey="pollStatusOpen" defaults="Live" />
@ -37,7 +42,15 @@ export function PollsTabbedView({ children }: { children: React.ReactNode }) {
<Trans i18nKey="pollStatusFinalized" defaults="Finalized" /> <Trans i18nKey="pollStatusFinalized" defaults="Finalized" />
</TabsTrigger> </TabsTrigger>
</TabsList> </TabsList>
<TabsContent tabIndex={-1} value={value} key={value}> <TabsContent
tabIndex={-1}
value={tab}
key={tab}
className={cn(
"transition-opacity",
isPending ? "opacity-50 delay-200 pointer-events-none" : "",
)}
>
{children} {children}
</TabsContent> </TabsContent>
</Tabs> </Tabs>

View file

@ -18,7 +18,7 @@ const Progress = React.forwardRef<
{...props} {...props}
> >
<ProgressPrimitive.Indicator <ProgressPrimitive.Indicator
className="bg-primary h-full w-full flex-1 transition-all" className="bg-gradient-to-r from-purple-500 to-indigo-500 h-full w-full flex-1 transition-all"
style={{ transform: `translateX(-${100 - (value || 0)}%)` }} style={{ transform: `translateX(-${100 - (value || 0)}%)` }}
/> />
</ProgressPrimitive.Root> </ProgressPrimitive.Root>