Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions src/common/context/ProjectsContext.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,11 @@ export const SidebarTogglableContext = createContext<boolean>(true)
type ProjectsContextValue = {
refreshing: boolean,
projects: Project[],
refreshProjects: () => void,
}

export const ProjectsContext = createContext<ProjectsContextValue>({
refreshing: false,
projects: [],
refreshProjects: () => {},
})
64 changes: 30 additions & 34 deletions src/features/projects/view/ProjectsContextProvider.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
"use client";

import { useState, useEffect, useRef } from "react";
import { useState, useEffect, useRef, useCallback } from "react";
import { ProjectsContext } from "@/common";
import { Project } from "@/features/projects/domain";

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


const setProjectsAndRefreshed = (value: Project[]) => {
setProjects(value);
};

const refreshProjects = useCallback(() => {
if (isLoadingRef.current) return;
isLoadingRef.current = true;
setRefreshing(true);
fetch("/api/refresh-projects", { method: "POST" })
.then((res) => res.json())
.then(({ projects }) => {
if (projects) setProjectsAndRefreshed(projects);
})
.catch((error) => console.error("Failed to refresh projects", error))
.finally(() => {
isLoadingRef.current = false;
setRefreshing(false);
});
}, []);

// Trigger background refresh after initial mount
useEffect(() => {
const refreshProjects = () => {
if (isLoadingRef.current) {
return;
}
isLoadingRef.current = true;
setRefreshing(true);
fetch("/api/refresh-projects", { method: "POST" })
.then((res) => res.json())
.then(({ projects }) => {
if (projects) setProjectsAndRefreshed(projects);
})
.catch((error) => console.error("Failed to refresh projects", error))
.finally(() => {
isLoadingRef.current = false;
setRefreshing(false);
});
};
// Initial refresh
refreshProjects();
const handleVisibilityChange = () => {
if (!document.hidden) refreshProjects();
};

document.addEventListener("visibilitychange", handleVisibilityChange);
return () =>
document.removeEventListener("visibilitychange", handleVisibilityChange);
}, []);
useEffect(() => {
// Initial refresh
refreshProjects();
const handleVisibilityChange = () => {
if (!document.hidden) refreshProjects();
};
document.addEventListener("visibilitychange", handleVisibilityChange);
return () => {
document.removeEventListener("visibilitychange", handleVisibilityChange);
};
}, [refreshProjects]);

return (
<ProjectsContext.Provider
value={{
projects,
refreshing,
}}
>
<ProjectsContext.Provider value={{ projects, refreshing, refreshProjects }}>
{children}
</ProjectsContext.Provider>
);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
"use client"
"use client";

import {
Box,
Expand All @@ -7,26 +7,33 @@ import {
ListItemButton,
Skeleton as MuiSkeleton,
Stack,
Typography
} from "@mui/material"
import MenuItemHover from "@/common/ui/MenuItemHover"
import { Project } from "@/features/projects/domain"
import { useProjectSelection } from "@/features/projects/data"
import ProjectAvatar, { Squircle as ProjectAvatarSquircle } from "./ProjectAvatar"
import { useCloseSidebarOnSelection } from "@/features/sidebar/data"
Typography,
} from "@mui/material";
import MenuItemHover from "@/common/ui/MenuItemHover";
import { Project } from "@/features/projects/domain";
import { useProjectSelection } from "@/features/projects/data";
import { useContext } from "react";
import { ProjectsContext } from "@/common";
import ProjectAvatar, {
Squircle as ProjectAvatarSquircle,
} from "./ProjectAvatar";
import { useCloseSidebarOnSelection } from "@/features/sidebar/data";

const AVATAR_SIZE = { width: 40, height: 40 }
const AVATAR_SIZE = { width: 40, height: 40 };

const ProjectListItem = ({ project }: { project: Project }) => {
const { project: selectedProject, selectProject } = useProjectSelection()
const selected = project.id === selectedProject?.id
const { closeSidebarIfNeeded } = useCloseSidebarOnSelection()
const { project: selectedProject, selectProject } = useProjectSelection();
const { refreshProjects } = useContext(ProjectsContext);
const selected = project.id === selectedProject?.id;
const { closeSidebarIfNeeded } = useCloseSidebarOnSelection();

return (
<Template
selected={selected}
onSelect={() => {
closeSidebarIfNeeded()
selectProject(project)
closeSidebarIfNeeded();
selectProject(project);
refreshProjects();
}}
avatar={
<ProjectAvatar
Expand All @@ -37,55 +44,55 @@ const ProjectListItem = ({ project }: { project: Project }) => {
}
title={project.displayName}
/>
)
}
);
};

export default ProjectListItem
export default ProjectListItem;

export const Skeleton = () => {
return (
<Template disabled avatar={
<ProjectAvatarSquircle width={AVATAR_SIZE.width} height={AVATAR_SIZE.height}>
<MuiSkeleton
variant="rectangular"
animation="wave"
sx={{ width: "100%", height: "100%" }}
/>
</ProjectAvatarSquircle>
}>
<Template
disabled
avatar={
<ProjectAvatarSquircle
width={AVATAR_SIZE.width}
height={AVATAR_SIZE.height}
>
<MuiSkeleton
variant="rectangular"
animation="wave"
sx={{ width: "100%", height: "100%" }}
/>
</ProjectAvatarSquircle>
}
>
<MuiSkeleton variant="text" animation="wave" width={100} />
</Template>
)
}
);
};

export const Template = ({
disabled,
selected,
onSelect,
avatar,
title,
children
children,
}: {
disabled?: boolean
selected?: boolean
onSelect?: () => void
avatar: React.ReactNode
title?: string
children?: React.ReactNode
disabled?: boolean;
selected?: boolean;
onSelect?: () => void;
avatar: React.ReactNode;
title?: string;
children?: React.ReactNode;
}) => {
return (
<ListItem disablePadding>
<Button
disabled={disabled}
selected={selected}
onSelect={onSelect}
>
<Button disabled={disabled} selected={selected} onSelect={onSelect}>
<MenuItemHover disabled={disabled}>
<Stack direction="row" alignItems="center" spacing={1.5}>
<Box sx={{ width: 40, height: 40 }}>
{avatar}
</Box>
{title &&
<Box sx={{ width: 40, height: 40 }}>{avatar}</Box>
{title && (
<ListItemText
primary={
<Typography
Expand All @@ -95,37 +102,37 @@ export const Template = ({
letterSpacing: 0.1,
whiteSpace: "nowrap",
overflow: "hidden",
textOverflow: "ellipsis"
textOverflow: "ellipsis",
}}
>
{title}
</Typography>
}
/>
}
)}
{children}
</Stack>
</MenuItemHover>
</Button>
</ListItem>
)
}
);
};

const Button = ({
disabled,
selected,
onSelect,
children
children,
}: {
disabled?: boolean
selected?: boolean
onSelect?: () => void
children?: React.ReactNode
disabled?: boolean;
selected?: boolean;
onSelect?: () => void;
children?: React.ReactNode;
}) => {
return (
<>
{disabled && children}
{!disabled &&
{!disabled && (
<ListItemButton
disabled={disabled}
onClick={onSelect}
Expand All @@ -135,7 +142,7 @@ const Button = ({
>
{children}
</ListItemButton>
}
)}
</>
)
}
);
};