Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 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>
}
)}
</>
)
}
);
};