feat(project): update project layout and routing
Enhance project layout with header navigation, channel sidebar integration, and improved routing structure.
This commit is contained in:
parent
4322f36a76
commit
f4653f2399
@ -15,3 +15,5 @@ export { AccessSettings } from "./settings/AccessSettings";
|
||||
export { LabelsSettings } from "./settings/LabelsSettings";
|
||||
export { BillingSettings } from "./settings/BillingSettings";
|
||||
export { ProjectCreateMenuModal } from "./components/ProjectCreateMenuModal";
|
||||
export { ProjectJoinPage } from "./ProjectJoinPage";
|
||||
export { ProjectInvitationPage } from "./ProjectInvitationPage";
|
||||
|
||||
@ -1,19 +1,24 @@
|
||||
import { Outlet, useMatch, useParams } from "react-router-dom";
|
||||
import { useState } from "react";
|
||||
import { PanelLeftOpen } from "lucide-react";
|
||||
import { Link, Outlet, useMatch, useParams } from "react-router-dom";
|
||||
import { createContext, useContext, useState } from "react";
|
||||
import { Lock, PanelLeftOpen } from "lucide-react";
|
||||
import { ServerIconRail } from "@/components/layout/ServerIconRail";
|
||||
import { ChannelSidebar } from "@/components/layout/ChannelSidebar";
|
||||
import { Header } from "@/components/layout/Header";
|
||||
import { MemberList } from "@/components/layout/MemberList";
|
||||
import { RoomProvider } from "@/contexts/room";
|
||||
import { createContext, useContext } from "react";
|
||||
import { useIsMobile, useIsTablet } from "@/hooks/use-mobile";
|
||||
import { useProjectInfo } from "@/hooks/useProjectInfo";
|
||||
import type { ProjectInfoRelational } from "@/client/model";
|
||||
import { Button } from "@/components/ui/button";
|
||||
|
||||
interface ProjectContextType {
|
||||
showMembers: boolean;
|
||||
setShowMembers: (v: boolean) => void;
|
||||
currentRoomName: string | null;
|
||||
setCurrentRoomName: (name: string | null) => void;
|
||||
projectInfo: ProjectInfoRelational | null;
|
||||
isProjectMember: boolean;
|
||||
isProjectPreview: boolean;
|
||||
}
|
||||
|
||||
const ProjectContext = createContext<ProjectContextType>({
|
||||
@ -21,29 +26,78 @@ const ProjectContext = createContext<ProjectContextType>({
|
||||
setShowMembers: () => {},
|
||||
currentRoomName: null,
|
||||
setCurrentRoomName: () => {},
|
||||
projectInfo: null,
|
||||
isProjectMember: false,
|
||||
isProjectPreview: false,
|
||||
});
|
||||
|
||||
// eslint-disable-next-line react-refresh/only-export-components
|
||||
export const useProjectLayout = () => useContext(ProjectContext);
|
||||
|
||||
export function ProjectJoinBanner({
|
||||
compact = false,
|
||||
message = "Join this project to participate and use project tools.",
|
||||
}: {
|
||||
compact?: boolean;
|
||||
message?: string;
|
||||
}) {
|
||||
const { projectInfo } = useProjectLayout();
|
||||
const projectName = projectInfo?.name;
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`flex ${compact ? "items-center justify-between gap-3 rounded-lg px-4 py-3" : "items-start justify-between gap-4 px-6 py-4"} border bg-muted/30`}
|
||||
style={{ borderColor: "var(--border-subtle)" }}
|
||||
>
|
||||
<div className="flex min-w-0 items-start gap-3">
|
||||
<div className="mt-0.5 flex size-8 shrink-0 items-center justify-center rounded-lg bg-muted text-muted-foreground">
|
||||
<Lock className="size-4" />
|
||||
</div>
|
||||
<div className="min-w-0">
|
||||
<p className="text-sm font-medium text-foreground">Preview mode</p>
|
||||
<p className="text-sm text-muted-foreground">{message}</p>
|
||||
</div>
|
||||
</div>
|
||||
{projectName && (
|
||||
<Button asChild size={compact ? "sm" : "default"} className="shrink-0">
|
||||
<Link to={`/${projectName}/join`}>Apply to join</Link>
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function ProjectLayout() {
|
||||
const [showMembers, setShowMembers] = useState(false);
|
||||
const [currentRoomName, setCurrentRoomName] = useState<string | null>(null);
|
||||
const [isSidebarCollapsed, setIsSidebarCollapsed] = useState(false);
|
||||
const { projectName } = useParams<{ projectName: string }>();
|
||||
const { data: projectInfo = null } = useProjectInfo(projectName);
|
||||
const channelMatch = useMatch("/:projectName/channel/:roomId");
|
||||
const chatMatch = useMatch("/:projectName/chat/*");
|
||||
const roomId = channelMatch?.params.roomId ?? null;
|
||||
const isMobile = useIsMobile();
|
||||
const isTablet = useIsTablet();
|
||||
|
||||
const canShowMembers = !isMobile && !isTablet;
|
||||
const isProjectMember = !!projectInfo?.role;
|
||||
const isProjectPreview = !!projectInfo && !projectInfo.role;
|
||||
const canShowMembers = !isMobile && !isTablet && isProjectMember;
|
||||
|
||||
const mainShouldOwnScroll = !channelMatch && !chatMatch;
|
||||
|
||||
return (
|
||||
<ProjectContext.Provider value={{ showMembers, setShowMembers, currentRoomName, setCurrentRoomName }}>
|
||||
<RoomProvider roomId={roomId} projectName={projectName}>
|
||||
<ProjectContext.Provider
|
||||
value={{
|
||||
showMembers,
|
||||
setShowMembers,
|
||||
currentRoomName,
|
||||
setCurrentRoomName,
|
||||
projectInfo,
|
||||
isProjectMember,
|
||||
isProjectPreview,
|
||||
}}
|
||||
>
|
||||
<RoomProvider roomId={isProjectMember ? roomId : null} projectName={projectName}>
|
||||
<div className="flex h-screen overflow-hidden" style={{ backgroundColor: "var(--surface-ground)" }}>
|
||||
{!isMobile && <ServerIconRail />}
|
||||
|
||||
@ -89,6 +143,16 @@ export function ProjectLayout() {
|
||||
style={{ backgroundColor: "var(--surface-ground)" }}
|
||||
>
|
||||
<Header />
|
||||
{isProjectPreview && (
|
||||
<ProjectJoinBanner
|
||||
compact
|
||||
message={
|
||||
projectInfo?.is_public
|
||||
? "This public project is read-only until you join."
|
||||
: "You need to join before using project actions."
|
||||
}
|
||||
/>
|
||||
)}
|
||||
<main
|
||||
className={mainShouldOwnScroll ? "flex-1 overflow-y-auto" : "flex-1 overflow-hidden min-h-0"}
|
||||
style={{ backgroundColor: "var(--surface-ground)" }}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user