Skip to content

Commit dd24356

Browse files
Merge pull request #605 from shapehq/refresh-project-navigation
Adds project refresh functionality to context and UI
2 parents 3570836 + c6a4e3f commit dd24356

File tree

3 files changed

+96
-91
lines changed

3 files changed

+96
-91
lines changed

src/common/context/ProjectsContext.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,11 @@ export const SidebarTogglableContext = createContext<boolean>(true)
88
type ProjectsContextValue = {
99
refreshing: boolean,
1010
projects: Project[],
11+
refreshProjects: () => void,
1112
}
1213

1314
export const ProjectsContext = createContext<ProjectsContextValue>({
1415
refreshing: false,
1516
projects: [],
17+
refreshProjects: () => {},
1618
})

src/features/projects/view/ProjectsContextProvider.tsx

Lines changed: 30 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
"use client";
22

3-
import { useState, useEffect, useRef } from "react";
3+
import { useState, useEffect, useRef, useCallback } from "react";
44
import { ProjectsContext } from "@/common";
55
import { Project } from "@/features/projects/domain";
66

@@ -15,47 +15,43 @@ const ProjectsContextProvider = ({
1515
const [refreshing, setRefreshing] = useState(false);
1616
const isLoadingRef = useRef(false);
1717

18+
1819
const setProjectsAndRefreshed = (value: Project[]) => {
1920
setProjects(value);
2021
};
2122

23+
const refreshProjects = useCallback(() => {
24+
if (isLoadingRef.current) return;
25+
isLoadingRef.current = true;
26+
setRefreshing(true);
27+
fetch("/api/refresh-projects", { method: "POST" })
28+
.then((res) => res.json())
29+
.then(({ projects }) => {
30+
if (projects) setProjectsAndRefreshed(projects);
31+
})
32+
.catch((error) => console.error("Failed to refresh projects", error))
33+
.finally(() => {
34+
isLoadingRef.current = false;
35+
setRefreshing(false);
36+
});
37+
}, []);
38+
2239
// Trigger background refresh after initial mount
23-
useEffect(() => {
24-
const refreshProjects = () => {
25-
if (isLoadingRef.current) {
26-
return;
27-
}
28-
isLoadingRef.current = true;
29-
setRefreshing(true);
30-
fetch("/api/refresh-projects", { method: "POST" })
31-
.then((res) => res.json())
32-
.then(({ projects }) => {
33-
if (projects) setProjectsAndRefreshed(projects);
34-
})
35-
.catch((error) => console.error("Failed to refresh projects", error))
36-
.finally(() => {
37-
isLoadingRef.current = false;
38-
setRefreshing(false);
39-
});
40-
};
41-
// Initial refresh
42-
refreshProjects();
43-
const handleVisibilityChange = () => {
44-
if (!document.hidden) refreshProjects();
45-
};
4640

47-
document.addEventListener("visibilitychange", handleVisibilityChange);
48-
return () =>
49-
document.removeEventListener("visibilitychange", handleVisibilityChange);
50-
}, []);
41+
useEffect(() => {
42+
// Initial refresh
43+
refreshProjects();
44+
const handleVisibilityChange = () => {
45+
if (!document.hidden) refreshProjects();
46+
};
47+
document.addEventListener("visibilitychange", handleVisibilityChange);
48+
return () => {
49+
document.removeEventListener("visibilitychange", handleVisibilityChange);
50+
};
51+
}, [refreshProjects]);
5152

5253
return (
53-
<ProjectsContext.Provider
54-
value={{
55-
projects,
56-
refreshing,
57-
}}
58-
>
54+
<ProjectsContext.Provider value={{ projects, refreshing, refreshProjects }}>
5955
{children}
6056
</ProjectsContext.Provider>
6157
);
Lines changed: 64 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
"use client"
1+
"use client";
22

33
import {
44
Box,
@@ -7,26 +7,33 @@ import {
77
ListItemButton,
88
Skeleton as MuiSkeleton,
99
Stack,
10-
Typography
11-
} from "@mui/material"
12-
import MenuItemHover from "@/common/ui/MenuItemHover"
13-
import { Project } from "@/features/projects/domain"
14-
import { useProjectSelection } from "@/features/projects/data"
15-
import ProjectAvatar, { Squircle as ProjectAvatarSquircle } from "./ProjectAvatar"
16-
import { useCloseSidebarOnSelection } from "@/features/sidebar/data"
10+
Typography,
11+
} from "@mui/material";
12+
import MenuItemHover from "@/common/ui/MenuItemHover";
13+
import { Project } from "@/features/projects/domain";
14+
import { useProjectSelection } from "@/features/projects/data";
15+
import { useContext } from "react";
16+
import { ProjectsContext } from "@/common";
17+
import ProjectAvatar, {
18+
Squircle as ProjectAvatarSquircle,
19+
} from "./ProjectAvatar";
20+
import { useCloseSidebarOnSelection } from "@/features/sidebar/data";
1721

18-
const AVATAR_SIZE = { width: 40, height: 40 }
22+
const AVATAR_SIZE = { width: 40, height: 40 };
1923

2024
const ProjectListItem = ({ project }: { project: Project }) => {
21-
const { project: selectedProject, selectProject } = useProjectSelection()
22-
const selected = project.id === selectedProject?.id
23-
const { closeSidebarIfNeeded } = useCloseSidebarOnSelection()
25+
const { project: selectedProject, selectProject } = useProjectSelection();
26+
const { refreshProjects } = useContext(ProjectsContext);
27+
const selected = project.id === selectedProject?.id;
28+
const { closeSidebarIfNeeded } = useCloseSidebarOnSelection();
29+
2430
return (
2531
<Template
2632
selected={selected}
2733
onSelect={() => {
28-
closeSidebarIfNeeded()
29-
selectProject(project)
34+
closeSidebarIfNeeded();
35+
selectProject(project);
36+
refreshProjects();
3037
}}
3138
avatar={
3239
<ProjectAvatar
@@ -37,55 +44,55 @@ const ProjectListItem = ({ project }: { project: Project }) => {
3744
}
3845
title={project.displayName}
3946
/>
40-
)
41-
}
47+
);
48+
};
4249

43-
export default ProjectListItem
50+
export default ProjectListItem;
4451

4552
export const Skeleton = () => {
4653
return (
47-
<Template disabled avatar={
48-
<ProjectAvatarSquircle width={AVATAR_SIZE.width} height={AVATAR_SIZE.height}>
49-
<MuiSkeleton
50-
variant="rectangular"
51-
animation="wave"
52-
sx={{ width: "100%", height: "100%" }}
53-
/>
54-
</ProjectAvatarSquircle>
55-
}>
54+
<Template
55+
disabled
56+
avatar={
57+
<ProjectAvatarSquircle
58+
width={AVATAR_SIZE.width}
59+
height={AVATAR_SIZE.height}
60+
>
61+
<MuiSkeleton
62+
variant="rectangular"
63+
animation="wave"
64+
sx={{ width: "100%", height: "100%" }}
65+
/>
66+
</ProjectAvatarSquircle>
67+
}
68+
>
5669
<MuiSkeleton variant="text" animation="wave" width={100} />
5770
</Template>
58-
)
59-
}
71+
);
72+
};
6073

6174
export const Template = ({
6275
disabled,
6376
selected,
6477
onSelect,
6578
avatar,
6679
title,
67-
children
80+
children,
6881
}: {
69-
disabled?: boolean
70-
selected?: boolean
71-
onSelect?: () => void
72-
avatar: React.ReactNode
73-
title?: string
74-
children?: React.ReactNode
82+
disabled?: boolean;
83+
selected?: boolean;
84+
onSelect?: () => void;
85+
avatar: React.ReactNode;
86+
title?: string;
87+
children?: React.ReactNode;
7588
}) => {
7689
return (
7790
<ListItem disablePadding>
78-
<Button
79-
disabled={disabled}
80-
selected={selected}
81-
onSelect={onSelect}
82-
>
91+
<Button disabled={disabled} selected={selected} onSelect={onSelect}>
8392
<MenuItemHover disabled={disabled}>
8493
<Stack direction="row" alignItems="center" spacing={1.5}>
85-
<Box sx={{ width: 40, height: 40 }}>
86-
{avatar}
87-
</Box>
88-
{title &&
94+
<Box sx={{ width: 40, height: 40 }}>{avatar}</Box>
95+
{title && (
8996
<ListItemText
9097
primary={
9198
<Typography
@@ -95,37 +102,37 @@ export const Template = ({
95102
letterSpacing: 0.1,
96103
whiteSpace: "nowrap",
97104
overflow: "hidden",
98-
textOverflow: "ellipsis"
105+
textOverflow: "ellipsis",
99106
}}
100107
>
101108
{title}
102109
</Typography>
103110
}
104111
/>
105-
}
112+
)}
106113
{children}
107114
</Stack>
108115
</MenuItemHover>
109116
</Button>
110117
</ListItem>
111-
)
112-
}
118+
);
119+
};
113120

114121
const Button = ({
115122
disabled,
116123
selected,
117124
onSelect,
118-
children
125+
children,
119126
}: {
120-
disabled?: boolean
121-
selected?: boolean
122-
onSelect?: () => void
123-
children?: React.ReactNode
127+
disabled?: boolean;
128+
selected?: boolean;
129+
onSelect?: () => void;
130+
children?: React.ReactNode;
124131
}) => {
125132
return (
126133
<>
127134
{disabled && children}
128-
{!disabled &&
135+
{!disabled && (
129136
<ListItemButton
130137
disabled={disabled}
131138
onClick={onSelect}
@@ -135,7 +142,7 @@ const Button = ({
135142
>
136143
{children}
137144
</ListItemButton>
138-
}
145+
)}
139146
</>
140-
)
141-
}
147+
);
148+
};

0 commit comments

Comments
 (0)