From 49a32c454267ce7f90888c0cc18a89c9a76f432c Mon Sep 17 00:00:00 2001 From: VasuS609 Date: Sat, 31 Jan 2026 19:52:10 +0530 Subject: [PATCH 1/5] feat: Add collapsible sidebar with persistent state and responsive grid MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Added chevron toggle button in sidebar footer with smooth 180° rotation - Implemented persistent sidebar state using localStorage across app restarts - Added smooth transitions (300ms ease-in-out) for all sidebar animations - Footer text (version & copyright) displayed on single line, hidden when collapsed - Tooltip shows 'Collapse/Expand sidebar' on hover for better UX - Grid layout now responsive to sidebar state - shows more images per row when collapsed - Grid min-size adjusts from 224px to 200px when sidebar collapses for optimal space usage - All transitions synchronized and smooth without visual glitches --- .../components/Media/ChronologicalGallery.tsx | 13 +++- .../Navigation/Sidebar/AppSidebar.tsx | 67 +++++++++++++++++-- 2 files changed, 74 insertions(+), 6 deletions(-) diff --git a/frontend/src/components/Media/ChronologicalGallery.tsx b/frontend/src/components/Media/ChronologicalGallery.tsx index f033e35a0..9d7fd3200 100644 --- a/frontend/src/components/Media/ChronologicalGallery.tsx +++ b/frontend/src/components/Media/ChronologicalGallery.tsx @@ -6,6 +6,7 @@ import { groupImagesByYearMonthFromMetadata } from '@/utils/dateUtils'; import { setCurrentViewIndex } from '@/features/imageSlice'; import { MediaView } from './MediaView'; import { selectIsImageViewOpen } from '@/features/imageSelectors'; +import { useSidebar } from '@/components/ui/sidebar'; export type MonthMarker = { offset: number; @@ -34,6 +35,11 @@ export const ChronologicalGallery = ({ const monthHeaderRefs = useRef>(new Map()); const galleryRef = useRef(null); const isImageViewOpen = useSelector(selectIsImageViewOpen); + const { open: sidebarOpen } = useSidebar(); + + // Adjust grid minmax based on sidebar state + // When sidebar is collapsed, reduce min size to fit more images per row + const gridMinSize = sidebarOpen ? '224px' : '200px'; // Optimized grouping with proper date handling const grouped = useMemo( @@ -157,7 +163,12 @@ export const ChronologicalGallery = ({ {/* Images Grid */} -
+
{imgs.map((img) => { const chronologicalIndex = imageIndexMap.get(img.id) ?? -1; diff --git a/frontend/src/components/Navigation/Sidebar/AppSidebar.tsx b/frontend/src/components/Navigation/Sidebar/AppSidebar.tsx index ec018ec12..d3adeae78 100644 --- a/frontend/src/components/Navigation/Sidebar/AppSidebar.tsx +++ b/frontend/src/components/Navigation/Sidebar/AppSidebar.tsx @@ -7,6 +7,7 @@ import { SidebarMenuButton, SidebarMenuItem, SidebarSeparator, + useSidebar, } from '@/components/ui/sidebar'; import { Bolt, @@ -16,15 +17,40 @@ import { Video, BookImage, ClockFading, + ChevronLeft, } from 'lucide-react'; import { useLocation, Link } from 'react-router'; import { ROUTES } from '@/constants/routes'; import { getVersion } from '@tauri-apps/api/app'; import { useEffect, useState } from 'react'; +import { Button } from '@/components/ui/button'; +import { + Tooltip, + TooltipContent, + TooltipProvider, + TooltipTrigger, +} from '@/components/ui/tooltip'; export function AppSidebar() { const location = useLocation(); const [version, setVersion] = useState('1.0.0'); + const { open, setOpen, toggleSidebar } = useSidebar(); + + // Load collapsed state from localStorage on mount + useEffect(() => { + const savedState = localStorage.getItem('sidebar-open'); + if (savedState !== null) { + const shouldBeOpen = savedState === 'true'; + if (open !== shouldBeOpen) { + toggleSidebar(); + } + } + }, []); + + // Save collapsed state to localStorage whenever it changes + useEffect(() => { + localStorage.setItem('sidebar-open', String(open)); + }, [open]); useEffect(() => { getVersion().then((version) => { @@ -59,7 +85,7 @@ export function AppSidebar() {
.
@@ -73,7 +99,7 @@ export function AppSidebar() { asChild isActive={isActive(item.path)} tooltip={item.name} - className="rounded-sm" + className="rounded-sm transition-all duration-200" > @@ -85,9 +111,40 @@ export function AppSidebar() { -
-
PictoPy v{version}
-
© 2025 PictoPy
+
+
+
PictoPy v{version}
+
© 2025 PictoPy
+
+
+ + + + + + +

{open ? 'Collapse sidebar' : 'Expand sidebar'}

+
+
+
+
From 88bde832f5716d2089589d458f215dc32447567b Mon Sep 17 00:00:00 2001 From: VasuS609 Date: Sat, 31 Jan 2026 20:51:27 +0530 Subject: [PATCH 2/5] feat: add collapsible sidebar with persistent state and responsive grid - Implement collapsible sidebar with localStorage persistence - Add responsive gallery grid that adjusts to sidebar state (224px when open, 200px when closed) - Fix race condition in sidebar state management by using setOpen directly - Make copyright year dynamic using new Date().getFullYear() - Remove redundant boolean comparison in favorites filter - Update HTML title to 'PictoPy - AI-Powered Photo Manager' - Fix strict equality operator in landing page Fixes #942 --- frontend/index.html | 2 +- .../components/Media/ChronologicalGallery.tsx | 6 ++--- .../Navigation/Sidebar/AppSidebar.tsx | 26 ++++++++++--------- frontend/src/pages/Home/MyFav.tsx | 2 +- landing-page/src/Pages/Landing page/Home1.tsx | 2 +- 5 files changed, 20 insertions(+), 18 deletions(-) diff --git a/frontend/index.html b/frontend/index.html index ff93803bb..ac397ffdb 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -4,7 +4,7 @@ - Tauri + React + Typescript + PictoPy - AI-Powered Photo Manager diff --git a/frontend/src/components/Media/ChronologicalGallery.tsx b/frontend/src/components/Media/ChronologicalGallery.tsx index 9d7fd3200..a5a4584ae 100644 --- a/frontend/src/components/Media/ChronologicalGallery.tsx +++ b/frontend/src/components/Media/ChronologicalGallery.tsx @@ -163,10 +163,10 @@ export const ChronologicalGallery = ({
{/* Images Grid */} -
{imgs.map((img) => { diff --git a/frontend/src/components/Navigation/Sidebar/AppSidebar.tsx b/frontend/src/components/Navigation/Sidebar/AppSidebar.tsx index d3adeae78..76d7ca5b6 100644 --- a/frontend/src/components/Navigation/Sidebar/AppSidebar.tsx +++ b/frontend/src/components/Navigation/Sidebar/AppSidebar.tsx @@ -36,18 +36,14 @@ export function AppSidebar() { const [version, setVersion] = useState('1.0.0'); const { open, setOpen, toggleSidebar } = useSidebar(); - // Load collapsed state from localStorage on mount useEffect(() => { const savedState = localStorage.getItem('sidebar-open'); if (savedState !== null) { const shouldBeOpen = savedState === 'true'; - if (open !== shouldBeOpen) { - toggleSidebar(); - } + setOpen(shouldBeOpen); } - }, []); + }, [setOpen]); - // Save collapsed state to localStorage whenever it changes useEffect(() => { localStorage.setItem('sidebar-open', String(open)); }, [open]); @@ -115,14 +111,20 @@ export function AppSidebar() {
-
PictoPy v{version}
-
© 2025 PictoPy
+
+ PictoPy v{version} +
+
+ © {new Date().getFullYear()} PictoPy +
-
+
@@ -130,7 +132,7 @@ export function AppSidebar() { variant="ghost" size="icon" onClick={toggleSidebar} - className="h-8 w-8 transition-transform duration-300 ease-in-out hover:bg-accent" + className="hover:bg-accent h-8 w-8 transition-transform duration-300 ease-in-out" > { }, [data, isSuccess, dispatch, isSearchActive]); const favouriteImages = useMemo( - () => images.filter((image) => image.isFavourite === true), + () => images.filter((image) => image.isFavourite), [images], ); diff --git a/landing-page/src/Pages/Landing page/Home1.tsx b/landing-page/src/Pages/Landing page/Home1.tsx index 7c07409a7..89dc88bee 100644 --- a/landing-page/src/Pages/Landing page/Home1.tsx +++ b/landing-page/src/Pages/Landing page/Home1.tsx @@ -82,7 +82,7 @@ const shuffle = (array: (typeof squareData)[0][]) => { let currentIndex = array.length, randomIndex; - while (currentIndex != 0) { + while (currentIndex !== 0) { randomIndex = Math.floor(Math.random() * currentIndex); currentIndex--; From d124d3e41f8caa4631f72d36a4ca601fea3e5321 Mon Sep 17 00:00:00 2001 From: VasuS609 Date: Thu, 5 Feb 2026 01:16:26 +0530 Subject: [PATCH 3/5] fix: resolve sidebar collapse white screen and state persistence --- .../Navigation/Sidebar/AppSidebar.tsx | 55 +++++++------------ frontend/src/components/ui/sidebar.tsx | 32 ++++++++--- 2 files changed, 44 insertions(+), 43 deletions(-) diff --git a/frontend/src/components/Navigation/Sidebar/AppSidebar.tsx b/frontend/src/components/Navigation/Sidebar/AppSidebar.tsx index 76d7ca5b6..0b12cfe74 100644 --- a/frontend/src/components/Navigation/Sidebar/AppSidebar.tsx +++ b/frontend/src/components/Navigation/Sidebar/AppSidebar.tsx @@ -27,26 +27,13 @@ import { Button } from '@/components/ui/button'; import { Tooltip, TooltipContent, - TooltipProvider, TooltipTrigger, } from '@/components/ui/tooltip'; export function AppSidebar() { const location = useLocation(); const [version, setVersion] = useState('1.0.0'); - const { open, setOpen, toggleSidebar } = useSidebar(); - - useEffect(() => { - const savedState = localStorage.getItem('sidebar-open'); - if (savedState !== null) { - const shouldBeOpen = savedState === 'true'; - setOpen(shouldBeOpen); - } - }, [setOpen]); - - useEffect(() => { - localStorage.setItem('sidebar-open', String(open)); - }, [open]); + const { open, toggleSidebar } = useSidebar(); useEffect(() => { getVersion().then((version) => { @@ -125,27 +112,25 @@ export function AppSidebar() {
- - - - - - -

{open ? 'Collapse sidebar' : 'Expand sidebar'}

-
-
-
+ + + + + +

{open ? 'Collapse sidebar' : 'Expand sidebar'}

+
+
diff --git a/frontend/src/components/ui/sidebar.tsx b/frontend/src/components/ui/sidebar.tsx index 0a3c6ab02..4415b1d79 100644 --- a/frontend/src/components/ui/sidebar.tsx +++ b/frontend/src/components/ui/sidebar.tsx @@ -68,9 +68,22 @@ function SidebarProvider({ const [openMobile, setOpenMobile] = React.useState(false); // This is the internal state of the sidebar. - // We use openProp and setOpenProp for control from outside the component. const [_open, _setOpen] = React.useState(defaultOpen); const open = openProp ?? _open; + + // Restore sidebar state from cookie on mount + React.useEffect(() => { + try { + const cookies = document.cookie.split('; ').find(row => row.startsWith(SIDEBAR_COOKIE_NAME + '=')); + if (cookies) { + const savedState = cookies.split('=')[1] === 'true'; + _setOpen(savedState); + } + } catch { + // Ignore cookie errors + } + }, []); + const setOpen = React.useCallback( (value: boolean | ((value: boolean) => boolean)) => { const openState = typeof value === 'function' ? value(open) : value; @@ -81,7 +94,11 @@ function SidebarProvider({ } // This sets the cookie to keep the sidebar state. - document.cookie = `${SIDEBAR_COOKIE_NAME}=${openState}; path=/; max-age=${SIDEBAR_COOKIE_MAX_AGE}`; + try { + document.cookie = `${SIDEBAR_COOKIE_NAME}=${openState}; path=/; max-age=${SIDEBAR_COOKIE_MAX_AGE}`; + } catch { + + } }, [setOpenProp, open], ); @@ -107,8 +124,7 @@ function SidebarProvider({ return () => window.removeEventListener('keydown', handleKeyDown); }, [toggleSidebar]); - // We add a state so that we can do data-state="expanded" or "collapsed". - // This makes it easier to style the sidebar with Tailwind classes. + const state = open ? 'expanded' : 'collapsed'; const contextValue = React.useMemo( @@ -231,7 +247,7 @@ function Sidebar({ side === 'left' ? 'left-0 group-data-[collapsible=offcanvas]:left-[calc(var(--sidebar-width)*-1)]' : 'right-0 group-data-[collapsible=offcanvas]:right-[calc(var(--sidebar-width)*-1)]', - // Adjust the padding for floating and inset variants. + variant === 'floating' || variant === 'inset' ? 'p-2 group-data-[collapsible=icon]:w-[calc(var(--sidebar-width-icon)+(--spacing(4))+2px)]' : 'group-data-[collapsible=icon]:w-(--sidebar-width-icon) group-data-[side=left]:border-r group-data-[side=right]:border-l', @@ -425,7 +441,7 @@ function SidebarGroupAction({ data-sidebar="group-action" className={cn( 'text-sidebar-foreground ring-sidebar-ring hover:bg-sidebar-accent hover:text-sidebar-accent-foreground absolute top-3.5 right-3 flex aspect-square w-5 items-center justify-center rounded-md p-0 outline-hidden transition-transform focus-visible:ring-2 [&>svg]:size-4 [&>svg]:shrink-0', - // Increases the hit area of the button on mobile. + 'after:absolute after:-inset-2 md:after:hidden', 'group-data-[collapsible=icon]:hidden', className, @@ -560,7 +576,7 @@ function SidebarMenuAction({ data-sidebar="menu-action" className={cn( 'text-sidebar-foreground ring-sidebar-ring hover:bg-sidebar-accent hover:text-sidebar-accent-foreground peer-hover/menu-button:text-sidebar-accent-foreground absolute top-1.5 right-1 flex aspect-square w-5 items-center justify-center rounded-md p-0 outline-hidden transition-transform focus-visible:ring-2 [&>svg]:size-4 [&>svg]:shrink-0', - // Increases the hit area of the button on mobile. + 'after:absolute after:-inset-2 md:after:hidden', 'peer-data-[size=sm]/menu-button:top-1', 'peer-data-[size=default]/menu-button:top-1.5', @@ -604,7 +620,7 @@ function SidebarMenuSkeleton({ }: React.ComponentProps<'div'> & { showIcon?: boolean; }) { - // Random width between 50 to 90%. + const width = React.useMemo(() => { return `${Math.floor(Math.random() * 40) + 50}%`; }, []); From c73cbe559b29e69d4539d709f52b5bf607080ddc Mon Sep 17 00:00:00 2001 From: VasuS609 Date: Thu, 5 Feb 2026 01:54:21 +0530 Subject: [PATCH 4/5] fix: format code with prettier and ensure no linting errors --- frontend/src/components/ui/sidebar.tsx | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/frontend/src/components/ui/sidebar.tsx b/frontend/src/components/ui/sidebar.tsx index 4415b1d79..b24456fef 100644 --- a/frontend/src/components/ui/sidebar.tsx +++ b/frontend/src/components/ui/sidebar.tsx @@ -74,7 +74,9 @@ function SidebarProvider({ // Restore sidebar state from cookie on mount React.useEffect(() => { try { - const cookies = document.cookie.split('; ').find(row => row.startsWith(SIDEBAR_COOKIE_NAME + '=')); + const cookies = document.cookie + .split('; ') + .find((row) => row.startsWith(SIDEBAR_COOKIE_NAME + '=')); if (cookies) { const savedState = cookies.split('=')[1] === 'true'; _setOpen(savedState); @@ -96,9 +98,7 @@ function SidebarProvider({ // This sets the cookie to keep the sidebar state. try { document.cookie = `${SIDEBAR_COOKIE_NAME}=${openState}; path=/; max-age=${SIDEBAR_COOKIE_MAX_AGE}`; - } catch { - - } + } catch {} }, [setOpenProp, open], ); @@ -124,7 +124,6 @@ function SidebarProvider({ return () => window.removeEventListener('keydown', handleKeyDown); }, [toggleSidebar]); - const state = open ? 'expanded' : 'collapsed'; const contextValue = React.useMemo( @@ -247,7 +246,7 @@ function Sidebar({ side === 'left' ? 'left-0 group-data-[collapsible=offcanvas]:left-[calc(var(--sidebar-width)*-1)]' : 'right-0 group-data-[collapsible=offcanvas]:right-[calc(var(--sidebar-width)*-1)]', - + variant === 'floating' || variant === 'inset' ? 'p-2 group-data-[collapsible=icon]:w-[calc(var(--sidebar-width-icon)+(--spacing(4))+2px)]' : 'group-data-[collapsible=icon]:w-(--sidebar-width-icon) group-data-[side=left]:border-r group-data-[side=right]:border-l', @@ -441,7 +440,7 @@ function SidebarGroupAction({ data-sidebar="group-action" className={cn( 'text-sidebar-foreground ring-sidebar-ring hover:bg-sidebar-accent hover:text-sidebar-accent-foreground absolute top-3.5 right-3 flex aspect-square w-5 items-center justify-center rounded-md p-0 outline-hidden transition-transform focus-visible:ring-2 [&>svg]:size-4 [&>svg]:shrink-0', - + 'after:absolute after:-inset-2 md:after:hidden', 'group-data-[collapsible=icon]:hidden', className, @@ -576,7 +575,7 @@ function SidebarMenuAction({ data-sidebar="menu-action" className={cn( 'text-sidebar-foreground ring-sidebar-ring hover:bg-sidebar-accent hover:text-sidebar-accent-foreground peer-hover/menu-button:text-sidebar-accent-foreground absolute top-1.5 right-1 flex aspect-square w-5 items-center justify-center rounded-md p-0 outline-hidden transition-transform focus-visible:ring-2 [&>svg]:size-4 [&>svg]:shrink-0', - + 'after:absolute after:-inset-2 md:after:hidden', 'peer-data-[size=sm]/menu-button:top-1', 'peer-data-[size=default]/menu-button:top-1.5', @@ -620,7 +619,6 @@ function SidebarMenuSkeleton({ }: React.ComponentProps<'div'> & { showIcon?: boolean; }) { - const width = React.useMemo(() => { return `${Math.floor(Math.random() * 40) + 50}%`; }, []); From 655a936c00e87ac2307309b5970b7c1cf141605d Mon Sep 17 00:00:00 2001 From: VasuS609 Date: Thu, 5 Feb 2026 02:00:36 +0530 Subject: [PATCH 5/5] fix: resolve merge conflict in AppSidebar.tsx --- frontend/src/components/Navigation/Sidebar/AppSidebar.tsx | 6 ------ 1 file changed, 6 deletions(-) diff --git a/frontend/src/components/Navigation/Sidebar/AppSidebar.tsx b/frontend/src/components/Navigation/Sidebar/AppSidebar.tsx index 9e9c91985..0b12cfe74 100644 --- a/frontend/src/components/Navigation/Sidebar/AppSidebar.tsx +++ b/frontend/src/components/Navigation/Sidebar/AppSidebar.tsx @@ -94,7 +94,6 @@ export function AppSidebar() { -<<<<<<< sidebar-toggle
-======= -
-
PictoPy v{version}
-
© {new Date().getFullYear()} PictoPy
->>>>>>> main