diff --git a/src/app/channel/layout.tsx b/src/app/channel/layout.tsx index 385aae1..d81692e 100644 --- a/src/app/channel/layout.tsx +++ b/src/app/channel/layout.tsx @@ -1,49 +1,64 @@ -import { createContext, useContext, useState, useMemo, useCallback, type ReactNode } from "react"; -import { Outlet } from "react-router-dom"; -import { useMatch } from "react-router-dom"; -import { ChevronRight } 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 { useIsMobile, useIsTablet } from "@/hooks/use-mobile"; +import { + createContext, + useContext, + useState, + useMemo, + useCallback, + type ReactNode, +} from "react" +import { Outlet } from "react-router-dom" +import { useMatch } from "react-router-dom" +import { ChevronRight } 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 { useIsMobile, useIsTablet } from "@/hooks/use-mobile" interface ChannelContextType { - showMembers: boolean; - setShowMembers: (v: boolean) => void; + showMembers: boolean + setShowMembers: (v: boolean) => void } const ChannelContext = createContext({ showMembers: false, setShowMembers: () => {}, -}); +}) // eslint-disable-next-line react-refresh/only-export-components -export const useChannel = () => useContext(ChannelContext); +export const useChannel = () => useContext(ChannelContext) export function ChannelLayout({ children }: { children?: ReactNode }) { - const [showMembers, setShowMembers] = useState(false); - const [isSidebarCollapsed, setIsSidebarCollapsed] = useState(false); - const isMobile = useIsMobile(); - const isTablet = useIsTablet(); - const canShowMembers = !isMobile && !isTablet; + const [showMembers, setShowMembers] = useState(false) + const [isSidebarCollapsed, setIsSidebarCollapsed] = useState(false) + const isMobile = useIsMobile() + const isTablet = useIsTablet() + const canShowMembers = !isMobile && !isTablet const contextValue = useMemo( () => ({ showMembers, setShowMembers }), - [showMembers], - ); + [showMembers] + ) - const toggleSidebar = useCallback( - () => setIsSidebarCollapsed((v) => !v), - [], - ); + const toggleSidebar = useCallback(() => setIsSidebarCollapsed((v) => !v), []) - const roomMatch = useMatch("/channel/:roomId"); - const mainShouldOwnScroll = !roomMatch; + const roomMatch = useMatch("/channel/:roomId") + const mainShouldOwnScroll = !roomMatch return ( -
+
+ -
+
{children ?? } @@ -91,11 +115,13 @@ export function ChannelLayout({ children }: { children?: ReactNode }) {
{canShowMembers && ( -
+
{showMembers && }
)}
- ); + ) } diff --git a/src/app/project/board/BoardHeader.tsx b/src/app/project/board/BoardHeader.tsx index 11a27a3..23e734c 100644 --- a/src/app/project/board/BoardHeader.tsx +++ b/src/app/project/board/BoardHeader.tsx @@ -1,13 +1,13 @@ -import { useNavigate } from "react-router-dom"; -import { Plus, Trash2, ArrowLeft } from "lucide-react"; -import { BOARD_PAGE } from "@/css/app/board-styles"; +import { useNavigate } from "react-router-dom" +import { Plus, Trash2, ArrowLeft } from "lucide-react" +import { Button } from "@/components/ui/button" interface BoardHeaderProps { - projectName: string; - boardName: string; - boardDescription?: string; - onDeleteBoard: () => void; - onAddColumn: () => void; + projectName: string + boardName: string + boardDescription?: string + onDeleteBoard: () => void + onAddColumn: () => void } export function BoardHeader({ @@ -17,40 +17,60 @@ export function BoardHeader({ onDeleteBoard, onAddColumn, }: BoardHeaderProps) { - const navigate = useNavigate(); + const navigate = useNavigate() return ( -
-
-
- -

{boardName}

+
+
+
+
+ +
+
+

+ {boardName} +

+ + /{projectName} + +
+ {boardDescription && ( +

+ {boardDescription} +

+ )} +
+
+ +
-

- {boardDescription} -

-
-
- -
- ); + ) } diff --git a/src/app/project/issue-detail/IssueSidebar.tsx b/src/app/project/issue-detail/IssueSidebar.tsx index 2e42de3..f01e67d 100644 --- a/src/app/project/issue-detail/IssueSidebar.tsx +++ b/src/app/project/issue-detail/IssueSidebar.tsx @@ -15,9 +15,10 @@ import { useUnlinkPRMutation, useLinkRepoMutation, useUnlinkRepoMutation, -} from "@/hooks/useIssueExtraQuery"; -import { Button } from "@/components/ui/button"; -import { Badge } from "@/components/ui/badge"; +} from "@/hooks/useIssueExtraQuery" +import { Button } from "@/components/ui/button" +import { Badge } from "@/components/ui/badge" +import { Separator } from "@/components/ui/separator" import { Tag, Bell, @@ -27,97 +28,135 @@ import { Plus, X, Users, - Settings -} from "lucide-react"; -import { Avatar, AvatarFallback } from "@/components/ui/avatar"; + Settings, +} from "lucide-react" +import { Avatar, AvatarFallback } from "@/components/ui/avatar" import { - DropdownMenu, - DropdownMenuContent, - DropdownMenuItem, - DropdownMenuTrigger, - DropdownMenuSeparator -} from "@/components/ui/dropdown-menu"; -import { useState } from "react"; + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuTrigger, + DropdownMenuSeparator, +} from "@/components/ui/dropdown-menu" +import { useState } from "react" import { - Dialog, - DialogContent, - DialogHeader, - DialogTitle, -} from "@/components/ui/dialog"; -import { Input } from "@/components/ui/input"; -import { t } from "@/i18n/T"; + Dialog, + DialogContent, + DialogHeader, + DialogTitle, +} from "@/components/ui/dialog" +import { Input } from "@/components/ui/input" +import { t } from "@/i18n/T" interface IssueSidebarProps { - projectName: string; - issueNumber: number; + projectName: string + issueNumber: number } export function IssueSidebar({ projectName, issueNumber }: IssueSidebarProps) { - const { data: assignees = [] } = useIssueAssigneesQuery({ projectName, issueNumber }); - const { data: issueLabels = [] } = useIssueLabelsQuery({ projectName, issueNumber }); - const { data: projectLabels = [] } = useProjectLabelsQuery(projectName); - const { data: subscribers = [] } = useIssueSubscribersQuery({ projectName, issueNumber }); - const { data: prs = [] } = useIssuePRsQuery({ projectName, issueNumber }); - const { data: repos = [] } = useIssueReposQuery({ projectName, issueNumber }); + const { data: assignees = [] } = useIssueAssigneesQuery({ + projectName, + issueNumber, + }) + const { data: issueLabels = [] } = useIssueLabelsQuery({ + projectName, + issueNumber, + }) + const { data: projectLabels = [] } = useProjectLabelsQuery(projectName) + const { data: subscribers = [] } = useIssueSubscribersQuery({ + projectName, + issueNumber, + }) + const { data: prs = [] } = useIssuePRsQuery({ projectName, issueNumber }) + const { data: repos = [] } = useIssueReposQuery({ projectName, issueNumber }) - const addAssignee = useAddAssigneeMutation(); - const removeAssignee = useRemoveAssigneeMutation(); - const addLabel = useAddIssueLabelMutation(); - const removeLabel = useRemoveIssueLabelMutation(); - const subscribe = useIssueSubscribeMutation(); - const unsubscribe = useIssueUnsubscribeMutation(); - const linkPR = useLinkPRMutation(); - const unlinkPR = useUnlinkPRMutation(); - const linkRepo = useLinkRepoMutation(); - const unlinkRepo = useUnlinkRepoMutation(); + const addAssignee = useAddAssigneeMutation() + const removeAssignee = useRemoveAssigneeMutation() + const addLabel = useAddIssueLabelMutation() + const removeLabel = useRemoveIssueLabelMutation() + const subscribe = useIssueSubscribeMutation() + const unsubscribe = useIssueUnsubscribeMutation() + const linkPR = useLinkPRMutation() + const unlinkPR = useUnlinkPRMutation() + const linkRepo = useLinkRepoMutation() + const unlinkRepo = useUnlinkRepoMutation() - const isSubscribed = subscribers.some(s => s.user_id === "me"); + const isSubscribed = subscribers.some((s) => s.user_id === "me") - const [showLinkPR, setShowLinkPR] = useState(false); - const [prRepo, setPrRepo] = useState(""); - const [prNum, setPrNum] = useState(""); - const [showLinkRepo, setShowLinkRepo] = useState(false); - const [repoName, setRepoName] = useState(""); + const [showLinkPR, setShowLinkPR] = useState(false) + const [prRepo, setPrRepo] = useState("") + const [prNum, setPrNum] = useState("") + const [showLinkRepo, setShowLinkRepo] = useState(false) + const [repoName, setRepoName] = useState("") return ( -
+
{/* Assignees */}
-
-

- {t("project.issue_detail.assignees")} +
+

+ {t("project.issue_detail.assignees")} + + {assignees.length} +

- + - - {t("project.issue_detail.assign_to_me")} - -

{t("project.issue_detail.suggestions")}

- addAssignee.mutate({ projectName, issueNumber, userId: "me" })}> - admin - + + + {t("project.issue_detail.assign_to_me")} + + +

+ {t("project.issue_detail.suggestions")} +

+ + addAssignee.mutate({ projectName, issueNumber, userId: "me" }) + } + > + admin +
-
+
{assignees.length === 0 ? ( -

{t("project.issue_detail.no_assigned")}

+

+ {t("project.issue_detail.no_assigned")} +

) : ( - assignees.map(a => ( -
- - {a.username[0].toUpperCase()} + assignees.map((a) => ( +
+ + + {a.username[0].toUpperCase()} + - {a.username} + + {a.username} +
)) @@ -125,57 +164,82 @@ export function IssueSidebar({ projectName, issueNumber }: IssueSidebarProps) {
-
+ {/* Labels */}
-
-

- {t("project.issue_detail.labels")} +
+

+ {t("project.issue_detail.labels")} + + {issueLabels.length} +

- + - -

{t("project.issue_detail.apply_labels")}

- {projectLabels.map(l => ( - addLabel.mutate({ projectName, issueNumber, labelId: l.id })} - > -
- {l.name} - {issueLabels.some(il => il.label_name === l.name) && } - - ))} - - - Manage labels + +

+ {t("project.issue_detail.apply_labels")} +

+ {projectLabels.map((l) => ( + + addLabel.mutate({ projectName, issueNumber, labelId: l.id }) + } + > +
+ {l.name} + {issueLabels.some((il) => il.label_name === l.name) && ( + + )} + ))} + + + Manage labels +
-
+
{issueLabels.length === 0 ? ( -

{t("project.issue_detail.none_yet")}

+

+ {t("project.issue_detail.none_yet")} +

) : ( - issueLabels.map(l => ( + issueLabels.map((l) => ( {l.label_name} )) @@ -183,151 +247,236 @@ export function IssueSidebar({ projectName, issueNumber }: IssueSidebarProps) {
-
+ {/* Notifications */}
-

- {t("project.issue_detail.notifications")} +

+ {" "} + {t("project.issue_detail.notifications")} + + {subscribers.length} +

-

- {t("project.issue_detail.subscribers_count", { count: String(subscribers.length) })} +

+ {t("project.issue_detail.subscribers_count", { + count: String(subscribers.length), + })}

-
+ {/* Development */}
-

+

{t("project.issue_detail.development")} + + {prs.length + repos.length} +

- {prs.length > 0 && ( -
-

{t("project.issue_detail.pull_requests")}

- {prs.map(pr => ( -
-
- - #{pr.number} in {pr.repo} -
- -
- ))} + {prs.length > 0 && ( +
+

+ {t("project.issue_detail.pull_requests")} +

+ {prs.map((pr) => ( +
+
+ + + #{pr.number} in {pr.repo} + +
+
- )} - {repos.length > 0 && ( -
-

{t("project.issue_detail.linked_repos")}

- {repos.map(r => ( -
-
- - {r.repo} -
- -
- ))} + ))} +
+ )} + {repos.length > 0 && ( +
+

+ {t("project.issue_detail.linked_repos")} +

+ {repos.map((r) => ( +
+
+ + {r.repo} +
+
- )} -
+ )} +
+ - +
{/* Link PR Dialog */} - - {t("project.issue_detail.link_pr_title")} - -
-
- - setPrRepo(e.target.value)} placeholder="owner/repo" /> -
-
- - setPrNum(e.target.value)} placeholder="123" /> -
- + + {t("project.issue_detail.link_pr_title")} + +
+
+ + setPrRepo(e.target.value)} + placeholder="owner/repo" + />
+
+ + setPrNum(e.target.value)} + placeholder="123" + /> +
+ +
{/* Link Repo Dialog */} - - {t("project.issue_detail.link_repo_title")} - -
-
- - setRepoName(e.target.value)} placeholder="owner/repo" /> -
- + + + {t("project.issue_detail.link_repo_title")} + + +
+
+ + setRepoName(e.target.value)} + placeholder="owner/repo" + />
+ +
- ); + ) } diff --git a/src/app/project/layout.tsx b/src/app/project/layout.tsx index 6188bd1..b58f9fc 100644 --- a/src/app/project/layout.tsx +++ b/src/app/project/layout.tsx @@ -1,25 +1,25 @@ -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 { useIsMobile, useIsTablet } from "@/hooks/use-mobile"; -import { useProjectInfo } from "@/hooks/useProjectInfo"; -import type { ProjectInfoRelational } from "@/client/model"; -import { Button } from "@/components/ui/button"; -import { t } from "@/i18n/T"; +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 { useIsMobile, useIsTablet } from "@/hooks/use-mobile" +import { useProjectInfo } from "@/hooks/useProjectInfo" +import type { ProjectInfoRelational } from "@/client/model" +import { Button } from "@/components/ui/button" +import { t } from "@/i18n/T" interface ProjectContextType { - showMembers: boolean; - setShowMembers: (v: boolean) => void; - currentRoomName: string | null; - setCurrentRoomName: (name: string | null) => void; - projectInfo: ProjectInfoRelational | null; - isProjectMember: boolean; - isProjectPreview: boolean; + showMembers: boolean + setShowMembers: (v: boolean) => void + currentRoomName: string | null + setCurrentRoomName: (name: string | null) => void + projectInfo: ProjectInfoRelational | null + isProjectMember: boolean + isProjectPreview: boolean } const ProjectContext = createContext({ @@ -30,62 +30,67 @@ const ProjectContext = createContext({ projectInfo: null, isProjectMember: false, isProjectPreview: false, -}); +}) // 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, }: { - compact?: boolean; - message?: string; + compact?: boolean + message?: string }) { - const { projectInfo } = useProjectLayout(); - const projectName = projectInfo?.name; - const defaultMessage = t("project.layout.join_banner.join_to_participate"); + const { projectInfo } = useProjectLayout() + const projectName = projectInfo?.name + const defaultMessage = t("project.layout.join_banner.join_to_participate") return (
-

{t("project.layout.join_banner.preview_mode")}

-

{message ?? defaultMessage}

+

+ {t("project.layout.join_banner.preview_mode")} +

+

+ {message ?? defaultMessage} +

{projectName && ( )}
- ); + ) } export function ProjectLayout() { - const [showMembers, setShowMembers] = useState(false); - const [currentRoomName, setCurrentRoomName] = useState(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 [showMembers, setShowMembers] = useState(false) + const [currentRoomName, setCurrentRoomName] = useState(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 isProjectMember = !!projectInfo?.role; - const isProjectPreview = !!projectInfo && !projectInfo.role; - const canShowMembers = !isMobile && !isTablet && isProjectMember; + const isProjectMember = !!projectInfo?.role + const isProjectPreview = !!projectInfo && !projectInfo.role + const canShowMembers = !isMobile && !isTablet && isProjectMember - const mainShouldOwnScroll = !channelMatch && !chatMatch; + const mainShouldOwnScroll = !channelMatch && !chatMatch return ( - -
+ +
+ -
+
{isProjectPreview && ( )}
@@ -173,5 +186,5 @@ export function ProjectLayout() {
- ); + ) }