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:
ZhenYi 2026-05-14 23:14:36 +08:00
parent 4322f36a76
commit f4653f2399
2 changed files with 73 additions and 7 deletions

View File

@ -15,3 +15,5 @@ export { AccessSettings } from "./settings/AccessSettings";
export { LabelsSettings } from "./settings/LabelsSettings"; export { LabelsSettings } from "./settings/LabelsSettings";
export { BillingSettings } from "./settings/BillingSettings"; export { BillingSettings } from "./settings/BillingSettings";
export { ProjectCreateMenuModal } from "./components/ProjectCreateMenuModal"; export { ProjectCreateMenuModal } from "./components/ProjectCreateMenuModal";
export { ProjectJoinPage } from "./ProjectJoinPage";
export { ProjectInvitationPage } from "./ProjectInvitationPage";

View File

@ -1,19 +1,24 @@
import { Outlet, useMatch, useParams } from "react-router-dom"; import { Link, Outlet, useMatch, useParams } from "react-router-dom";
import { useState } from "react"; import { createContext, useContext, useState } from "react";
import { PanelLeftOpen } from "lucide-react"; import { Lock, PanelLeftOpen } from "lucide-react";
import { ServerIconRail } from "@/components/layout/ServerIconRail"; import { ServerIconRail } from "@/components/layout/ServerIconRail";
import { ChannelSidebar } from "@/components/layout/ChannelSidebar"; import { ChannelSidebar } from "@/components/layout/ChannelSidebar";
import { Header } from "@/components/layout/Header"; import { Header } from "@/components/layout/Header";
import { MemberList } from "@/components/layout/MemberList"; import { MemberList } from "@/components/layout/MemberList";
import { RoomProvider } from "@/contexts/room"; import { RoomProvider } from "@/contexts/room";
import { createContext, useContext } from "react";
import { useIsMobile, useIsTablet } from "@/hooks/use-mobile"; 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 { interface ProjectContextType {
showMembers: boolean; showMembers: boolean;
setShowMembers: (v: boolean) => void; setShowMembers: (v: boolean) => void;
currentRoomName: string | null; currentRoomName: string | null;
setCurrentRoomName: (name: string | null) => void; setCurrentRoomName: (name: string | null) => void;
projectInfo: ProjectInfoRelational | null;
isProjectMember: boolean;
isProjectPreview: boolean;
} }
const ProjectContext = createContext<ProjectContextType>({ const ProjectContext = createContext<ProjectContextType>({
@ -21,29 +26,78 @@ const ProjectContext = createContext<ProjectContextType>({
setShowMembers: () => {}, setShowMembers: () => {},
currentRoomName: null, currentRoomName: null,
setCurrentRoomName: () => {}, setCurrentRoomName: () => {},
projectInfo: null,
isProjectMember: false,
isProjectPreview: false,
}); });
// eslint-disable-next-line react-refresh/only-export-components // eslint-disable-next-line react-refresh/only-export-components
export const useProjectLayout = () => useContext(ProjectContext); 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() { export function ProjectLayout() {
const [showMembers, setShowMembers] = useState(false); const [showMembers, setShowMembers] = useState(false);
const [currentRoomName, setCurrentRoomName] = useState<string | null>(null); const [currentRoomName, setCurrentRoomName] = useState<string | null>(null);
const [isSidebarCollapsed, setIsSidebarCollapsed] = useState(false); const [isSidebarCollapsed, setIsSidebarCollapsed] = useState(false);
const { projectName } = useParams<{ projectName: string }>(); const { projectName } = useParams<{ projectName: string }>();
const { data: projectInfo = null } = useProjectInfo(projectName);
const channelMatch = useMatch("/:projectName/channel/:roomId"); const channelMatch = useMatch("/:projectName/channel/:roomId");
const chatMatch = useMatch("/:projectName/chat/*"); const chatMatch = useMatch("/:projectName/chat/*");
const roomId = channelMatch?.params.roomId ?? null; const roomId = channelMatch?.params.roomId ?? null;
const isMobile = useIsMobile(); const isMobile = useIsMobile();
const isTablet = useIsTablet(); 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; const mainShouldOwnScroll = !channelMatch && !chatMatch;
return ( return (
<ProjectContext.Provider value={{ showMembers, setShowMembers, currentRoomName, setCurrentRoomName }}> <ProjectContext.Provider
<RoomProvider roomId={roomId} projectName={projectName}> 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)" }}> <div className="flex h-screen overflow-hidden" style={{ backgroundColor: "var(--surface-ground)" }}>
{!isMobile && <ServerIconRail />} {!isMobile && <ServerIconRail />}
@ -89,6 +143,16 @@ export function ProjectLayout() {
style={{ backgroundColor: "var(--surface-ground)" }} style={{ backgroundColor: "var(--surface-ground)" }}
> >
<Header /> <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 <main
className={mainShouldOwnScroll ? "flex-1 overflow-y-auto" : "flex-1 overflow-hidden min-h-0"} className={mainShouldOwnScroll ? "flex-1 overflow-y-auto" : "flex-1 overflow-hidden min-h-0"}
style={{ backgroundColor: "var(--surface-ground)" }} style={{ backgroundColor: "var(--surface-ground)" }}