diff --git a/docker-compose.yaml b/docker-compose.yaml new file mode 100644 index 00000000..e2212f02 --- /dev/null +++ b/docker-compose.yaml @@ -0,0 +1,13 @@ +version: '3.8' +services: + cache: + image: redis:6.2-alpine + restart: always + ports: + - '6379:6379' + command: redis-server --save 20 1 --loglevel warning + volumes: + - cache:/data +volumes: + cache: + driver: local \ No newline at end of file diff --git a/src/app/api/auth/forceLogout/route.ts b/src/app/api/auth/forceLogout/route.ts index d7a7818d..bc902b44 100644 --- a/src/app/api/auth/forceLogout/route.ts +++ b/src/app/api/auth/forceLogout/route.ts @@ -12,7 +12,11 @@ export async function GET(req: NextRequest) { // should be provided that is not needed as the user is not fully logged // in at this point. const url = new URL(AUTH0_ISSUER_BASE_URL + "/oidc/logout") - const redirectURI = req.nextUrl.protocol + "//" + req.nextUrl.host + const host = req.headers.get('host') + const redirectURI = req.nextUrl.protocol + "//" + host url.searchParams.append("post_logout_redirect_uri", redirectURI) - return NextResponse.redirect(url) + + const response = NextResponse.redirect(url) + response.cookies.delete("appSession") + return response } diff --git a/src/app/api/github/blob/[owner]/[repository]/[...path]/route.ts b/src/app/api/github/blob/[owner]/[repository]/[...path]/route.ts index ec082b6b..6c4ca236 100644 --- a/src/app/api/github/blob/[owner]/[repository]/[...path]/route.ts +++ b/src/app/api/github/blob/[owner]/[repository]/[...path]/route.ts @@ -8,12 +8,26 @@ interface GetBlobParams { } export async function GET(req: NextRequest, { params }: { params: GetBlobParams }) { + const path = params.path.join("/") const item = await gitHubClient.getRepositoryContent({ repositoryOwner: params.owner, repositoryName: params.repository, - path: params.path.join("/"), - ref: req.nextUrl.searchParams.get("ref") || undefined + path: path, + ref: req.nextUrl.searchParams.get("ref") ?? undefined }) const url = new URL(item.downloadURL) - return NextResponse.redirect(url) + const imageRegex = /\.(jpg|jpeg|png|webp|avif|gif)$/; + + if (new RegExp(imageRegex).exec(path)) { + const file = await fetch(url).then(r => r.blob()); + const headers = new Headers(); + const cacheExpirationInSeconds = 60 * 60 * 24 * 30; // 30 days + + headers.set("Content-Type", "image/*"); + headers.set("cache-control", `stale-while-revalidate=${cacheExpirationInSeconds}`); + + return new NextResponse(file, { status: 200, statusText: "OK", headers }) + } else { + return NextResponse.redirect(url) + } } diff --git a/src/app/api/hooks/github/route.ts b/src/app/api/hooks/github/route.ts index b408e86c..f6739101 100644 --- a/src/app/api/hooks/github/route.ts +++ b/src/app/api/hooks/github/route.ts @@ -13,12 +13,15 @@ const { GITHUB_WEBHOK_REPOSITORY_DISALLOWLIST } = process.env -const allowedRepositoryNames = (GITHUB_WEBHOK_REPOSITORY_ALLOWLIST || "") - .split(",") - .map(e => e.trim()) -const disallowedRepositoryNames = (GITHUB_WEBHOK_REPOSITORY_DISALLOWLIST || "") - .split(",") - .map(e => e.trim()) +const listFromCommaSeparatedString = (str?: string) => { + if (!str) { + return [] + } + return str.split(",").map(e => e.trim()) +} + +const allowedRepositoryNames = listFromCommaSeparatedString(GITHUB_WEBHOK_REPOSITORY_ALLOWLIST) +const disallowedRepositoryNames = listFromCommaSeparatedString(GITHUB_WEBHOK_REPOSITORY_DISALLOWLIST) const hookHandler = new GitHubHookHandler({ secret: GITHUB_WEBHOOK_SECRET, diff --git a/src/common/theme/theme.ts b/src/common/theme/theme.ts index 38b69d88..1e76754b 100644 --- a/src/common/theme/theme.ts +++ b/src/common/theme/theme.ts @@ -22,6 +22,15 @@ const theme = () => createTheme({ disableRipple: true } } + }, + breakpoints: { + values: { + xs: 0, + sm: 600, + md: 900, + lg: 1200, + xl: 1536, + } } }) diff --git a/src/features/hooks/domain/RepositoryNameCheckingPullRequestEventHandler.ts b/src/features/hooks/domain/RepositoryNameCheckingPullRequestEventHandler.ts index 82edc1f9..cf45fa81 100644 --- a/src/features/hooks/domain/RepositoryNameCheckingPullRequestEventHandler.ts +++ b/src/features/hooks/domain/RepositoryNameCheckingPullRequestEventHandler.ts @@ -16,18 +16,30 @@ export default class RepositoryNameCheckingPullRequestEventHandler implements IP } async pullRequestOpened(event: IPullRequestOpenedEvent): Promise { - if (!event.repositoryName.match(/-openapi$/)) { + if (!this.repositoryNameHasExpectedSuffix(event.repositoryName)) { return } - if ( - this.allowedRepositoryNames.length != 0 && - !this.allowedRepositoryNames.includes(event.repositoryName) - ) { + if (!this.isAllowedRepositoryName(event.repositoryName)) { return } - if (this.disallowedRepositoryNames.includes(event.repositoryName)) { + if (this.isDisallowedRepositoryName(event.repositoryName)) { return } return await this.eventHandler.pullRequestOpened(event) } + + private repositoryNameHasExpectedSuffix(repositoryName: string) { + return repositoryName.match(/-openapi$/) + } + + private isAllowedRepositoryName(repositoryName: string) { + if (this.allowedRepositoryNames.length == 0) { + return true + } + return this.allowedRepositoryNames.includes(repositoryName) + } + + private isDisallowedRepositoryName(repositoryName: string) { + return this.disallowedRepositoryNames.includes(repositoryName) + } } diff --git a/src/features/projects/view/MobileToolbar.tsx b/src/features/projects/view/MobileToolbar.tsx new file mode 100644 index 00000000..5c715c07 --- /dev/null +++ b/src/features/projects/view/MobileToolbar.tsx @@ -0,0 +1,43 @@ +import { Stack } from "@mui/material" +import Project from "../domain/Project" +import Version from "../domain/Version" +import OpenApiSpecification from "../domain/OpenApiSpecification" +import VersionSelector from "./docs/VersionSelector" +import SpecificationSelector from "./docs/SpecificationSelector" + +const MobileToolbar = ({ + project, + version, + specification, + onSelectVersion, + onSelectSpecification +}: { + project: Project + version: Version + specification: OpenApiSpecification + onSelectVersion: (versionId: string) => void, + onSelectSpecification: (specificationId: string) => void +}) => { + return ( + + + + + ) +} + +export default MobileToolbar diff --git a/src/features/projects/view/ProjectsPageSecondaryContent.tsx b/src/features/projects/view/ProjectsPageContent.tsx similarity index 92% rename from src/features/projects/view/ProjectsPageSecondaryContent.tsx rename to src/features/projects/view/ProjectsPageContent.tsx index 360e00f5..70c6f598 100644 --- a/src/features/projects/view/ProjectsPageSecondaryContent.tsx +++ b/src/features/projects/view/ProjectsPageContent.tsx @@ -2,7 +2,7 @@ import { ProjectPageStateContainer, ProjectPageState } from "../domain/ProjectPa import ProjectErrorContent from "./ProjectErrorContent" import DocumentationViewer from "./docs/DocumentationViewer" -const ProjectsPageSecondaryContent = ({ +const ProjectsPageContent = ({ stateContainer }: { stateContainer: ProjectPageStateContainer @@ -24,4 +24,4 @@ const ProjectsPageSecondaryContent = ({ } } -export default ProjectsPageSecondaryContent \ No newline at end of file +export default ProjectsPageContent \ No newline at end of file diff --git a/src/features/projects/view/ProjectsPageTrailingToolbarItem.tsx b/src/features/projects/view/ProjectsPageTrailingToolbarItem.tsx deleted file mode 100644 index b6f7537f..00000000 --- a/src/features/projects/view/ProjectsPageTrailingToolbarItem.tsx +++ /dev/null @@ -1,66 +0,0 @@ -import { Stack, IconButton, Typography, Link } from "@mui/material" -import { ProjectPageStateContainer, ProjectPageState } from "../domain/ProjectPageState" -import VersionSelector from "./docs/VersionSelector" -import SpecificationSelector from "./docs/SpecificationSelector" -import EditIcon from "@mui/icons-material/Edit" - -const ProjectsPageTrailingToolbarItem = ( - { - stateContainer, - onSelectVersion, - onSelectSpecification - }: { - stateContainer: ProjectPageStateContainer, - onSelectVersion: (versionId: string) => void, - onSelectSpecification: (specificationId: string) => void - } -) => { - switch (stateContainer.state) { - case ProjectPageState.HAS_SELECTION: - return ( - - {stateContainer.selection!.version.url && - - {stateContainer.selection!.project.name} - - } - {!stateContainer.selection!.version.url && - - {stateContainer.selection!.project.name} - - } - / - - / - - {stateContainer.selection!.specification.editURL && - - - - } - - ) - case ProjectPageState.LOADING: - case ProjectPageState.NO_PROJECT_SELECTED: - case ProjectPageState.PROJECT_NOT_FOUND: - case ProjectPageState.VERSION_NOT_FOUND: - case ProjectPageState.SPECIFICATION_NOT_FOUND: - return <> - } -} - -export default ProjectsPageTrailingToolbarItem \ No newline at end of file diff --git a/src/features/projects/view/TrailingToolbarItem.tsx b/src/features/projects/view/TrailingToolbarItem.tsx new file mode 100644 index 00000000..41caba90 --- /dev/null +++ b/src/features/projects/view/TrailingToolbarItem.tsx @@ -0,0 +1,97 @@ +import { SxProps } from "@mui/system" +import { Stack, IconButton, Typography, Link } from "@mui/material" +import Project from "../domain/Project" +import Version from "../domain/Version" +import OpenApiSpecification from "../domain/OpenApiSpecification" +import VersionSelector from "./docs/VersionSelector" +import SpecificationSelector from "./docs/SpecificationSelector" +import EditIcon from "@mui/icons-material/Edit" + +const TrailingToolbarItem = ({ + project, + version, + specification, + onSelectVersion, + onSelectSpecification +}: { + project: Project + version: Version + specification: OpenApiSpecification + onSelectVersion: (versionId: string) => void, + onSelectSpecification: (specificationId: string) => void +}) => { + return ( + <> + + + + + + / + + / + + {specification.editURL && + + + + } + + + ) +} + +export default TrailingToolbarItem + +const ProjectName = ({ + url, + text, + sx +}: { + url?: string + text: string + sx?: SxProps +}) => { + if (url) { + return ( + + {text} + + ) + } else { + return ( + + {text} + + ) + } +} \ No newline at end of file diff --git a/src/features/projects/view/client/ProjectsPage.tsx b/src/features/projects/view/client/ProjectsPage.tsx index 32a82e07..71a9cf5e 100644 --- a/src/features/projects/view/client/ProjectsPage.tsx +++ b/src/features/projects/view/client/ProjectsPage.tsx @@ -1,12 +1,15 @@ "use client" -import { useEffect } from "react" +import { useState, useEffect } from "react" import { useRouter } from "next/navigation" +import { useTheme } from "@mui/material/styles" +import useMediaQuery from "@mui/material/useMediaQuery" import SidebarContainer from "@/features/sidebar/view/client/SidebarContainer" import Project from "../../domain/Project" import ProjectList from "../ProjectList" -import ProjectsPageSecondaryContent from "../ProjectsPageSecondaryContent" -import ProjectsPageTrailingToolbarItem from "../ProjectsPageTrailingToolbarItem" +import ProjectsPageContent from "../ProjectsPageContent" +import TrailingToolbarItem from "../TrailingToolbarItem" +import MobileToolbar from "../MobileToolbar" import { getProjectPageState } from "../../domain/ProjectPageState" import projectNavigator from "../../domain/projectNavigator" import updateWindowTitle from "../../domain/updateWindowTitle" @@ -24,7 +27,10 @@ export default function ProjectsPage({ specificationId?: string }) { const router = useRouter() + const theme = useTheme() + const isDesktopLayout = useMediaQuery(theme.breakpoints.up("sm")) const { projects: clientProjects, error, isLoading: isClientLoading } = useProjects() + const [forceCloseSidebar, setForceCloseSidebar] = useState(false) const projects = isClientLoading ? (serverProjects || []) : clientProjects const isLoading = serverProjects === undefined && isClientLoading const stateContainer = getProjectPageState({ @@ -35,11 +41,6 @@ export default function ProjectsPage({ selectedVersionId: versionId, selectedSpecificationId: specificationId }) - const handleProjectSelected = (project: Project) => { - const version = project.versions[0] - const specification = version.specifications[0] - projectNavigator.navigate(router, project.id, version.id, specification.id) - } useEffect(() => { updateWindowTitle( document, @@ -55,30 +56,50 @@ export default function ProjectsPage({ const urlSelection = { projectId, versionId, specificationId } projectNavigator.navigateIfNeeded(router, urlSelection, stateContainer.selection) }, [router, projectId, versionId, specificationId, stateContainer.selection]) + const selectProject = (project: Project) => { + setForceCloseSidebar(!isDesktopLayout) + const version = project.versions[0] + const specification = version.specifications[0] + projectNavigator.navigate(router, project.id, version.id, specification.id) + } + const selectVersion = (versionId: string) => { + projectNavigator.navigateToVersion(router, stateContainer.selection!, versionId) + } + const selectSpecification = (specificationId: string) => { + projectNavigator.navigate(router, projectId!, versionId!, specificationId) + } return ( } - secondary={ - + toolbarTrailingItem={stateContainer.selection && + } - toolbarTrailing={ - { - projectNavigator.navigateToVersion(router, stateContainer.selection!, versionId) - }} - onSelectSpecification={(specificationId: string) => { - projectNavigator.navigate(router, projectId!, versionId!, specificationId) - }} + mobileToolbar={stateContainer.selection && + } - /> + > + + ) } diff --git a/src/features/projects/view/docs/SpecificationSelector.tsx b/src/features/projects/view/docs/SpecificationSelector.tsx index f0723652..834e78b8 100644 --- a/src/features/projects/view/docs/SpecificationSelector.tsx +++ b/src/features/projects/view/docs/SpecificationSelector.tsx @@ -1,24 +1,23 @@ +import { SxProps } from "@mui/system" import { SelectChangeEvent, Select, MenuItem, FormControl } from "@mui/material" import OpenApiSpecification from "../../domain/OpenApiSpecification" -interface SpecificationSelectorProps { +const SpecificationSelector = ({ + specifications, + selection, + onSelect, + sx +}: { specifications: OpenApiSpecification[] selection: string onSelect: (specificationId: string) => void -} - -const SpecificationSelector: React.FC< - SpecificationSelectorProps -> = ({ - specifications, - selection, - onSelect + sx?: SxProps }) => { const handleVersionChange = (event: SelectChangeEvent) => { onSelect(event.target.value) } return ( - + {versions.map(version => diff --git a/src/features/settings/view/SettingsButton.tsx b/src/features/settings/view/SettingsButton.tsx index f12bbca2..dc1bc0e9 100644 --- a/src/features/settings/view/SettingsButton.tsx +++ b/src/features/settings/view/SettingsButton.tsx @@ -31,7 +31,7 @@ const SettingsButton: React.FC = () => { > - + diff --git a/src/features/sidebar/view/BaseSidebarContainer.tsx b/src/features/sidebar/view/BaseSidebarContainer.tsx deleted file mode 100644 index 68a6561b..00000000 --- a/src/features/sidebar/view/BaseSidebarContainer.tsx +++ /dev/null @@ -1,159 +0,0 @@ -import { ReactNode } from "react" -import { Box, Drawer, Divider, IconButton, Toolbar } from "@mui/material" -import MuiAppBar, { AppBarProps as MuiAppBarProps } from "@mui/material/AppBar" -import { ChevronLeft, Menu } from "@mui/icons-material" -import { styled, useTheme } from "@mui/material/styles" - -const drawerWidth = 320 - -const Main = styled("main", { shouldForwardProp: (prop) => prop !== "open" })<{ - open?: boolean -}>(({ theme, open }) => ({ - display: "flex", - flexDirection: "column", - flexGrow: 1, - overflowY: "auto", - transition: theme.transitions.create("margin", { - easing: theme.transitions.easing.sharp, - duration: theme.transitions.duration.leavingScreen, - }), - marginLeft: `-${drawerWidth}px`, - ...(open && { - transition: theme.transitions.create("margin", { - easing: theme.transitions.easing.easeOut, - duration: theme.transitions.duration.enteringScreen, - }), - marginLeft: 0, - }) -})) - -const DrawerHeaderWrapper = styled("div")(({ theme }) => ({ - display: "flex", - alignItems: "center", - // necessary for content to be below app bar - ...theme.mixins.toolbar -})) - -const DrawerHeader = ({ - primaryHeader, - handleDrawerClose -}: { - primaryHeader: ReactNode, - handleDrawerClose: () => void -}) => { - return ( - - - - - {primaryHeader != null && - - {primaryHeader} - - } - - ) -} - -interface AppBarProps extends MuiAppBarProps { - open?: boolean -} - -const AppBar = styled(MuiAppBar, { - shouldForwardProp: (prop) => prop !== "open", -})(({ theme, open }) => ({ - transition: theme.transitions.create(["margin", "width"], { - easing: theme.transitions.easing.sharp, - duration: theme.transitions.duration.leavingScreen, - }), - ...(open && { - width: `calc(100% - ${drawerWidth}px)`, - marginLeft: `${drawerWidth}px`, - transition: theme.transitions.create(["margin", "width"], { - easing: theme.transitions.easing.easeOut, - duration: theme.transitions.duration.enteringScreen, - }), - }), -})) - -const BaseSidebarContainer = ({ - isDrawerOpen, - onToggleDrawerOpen, - primaryHeader, - primary, - secondaryHeader, - secondary -}: { - isDrawerOpen: boolean - onToggleDrawerOpen: (isDrawerOpen: boolean) => void - primaryHeader?: ReactNode - primary: ReactNode - secondaryHeader?: ReactNode - secondary: ReactNode -}) => { - const theme = useTheme() - return ( - - - - onToggleDrawerOpen(true)} - edge="start" - sx={{ - mr: 2, - color: theme.palette.text.primary, - ...(isDrawerOpen && { display: "none" }) - }} - > - - - {secondaryHeader != null && secondaryHeader} - - - - - onToggleDrawerOpen(false)} - primaryHeader={primaryHeader} - /> - {primary} - -
- - - {secondary} - -
- - ) -} - -export default BaseSidebarContainer diff --git a/src/features/sidebar/view/SidebarContent.tsx b/src/features/sidebar/view/Sidebar.tsx similarity index 79% rename from src/features/sidebar/view/SidebarContent.tsx rename to src/features/sidebar/view/Sidebar.tsx index cb65606b..e08fe8a1 100644 --- a/src/features/sidebar/view/SidebarContent.tsx +++ b/src/features/sidebar/view/Sidebar.tsx @@ -3,7 +3,11 @@ import { Box } from "@mui/material" import UserListItem from "@/features/user/view/UserListItem" import SettingsButton from "@/features/settings/view/SettingsButton" -const SidebarContent = ({ children }: { children: ReactNode }) => { +const Sidebar = ({ + children +}: { + children: ReactNode +}) => { return ( <> @@ -14,4 +18,4 @@ const SidebarContent = ({ children }: { children: ReactNode }) => { ) } -export default SidebarContent +export default Sidebar diff --git a/src/features/sidebar/view/SidebarHeader.tsx b/src/features/sidebar/view/SidebarHeader.tsx new file mode 100644 index 00000000..09e5b13c --- /dev/null +++ b/src/features/sidebar/view/SidebarHeader.tsx @@ -0,0 +1,13 @@ +import Image from "next/image" +import { Stack } from "@mui/material" +import Link from "next/link" + +export default function SidebarHeader() { + return ( + + + Duck + + + ) +} diff --git a/src/features/sidebar/view/base/Drawer.tsx b/src/features/sidebar/view/base/Drawer.tsx new file mode 100644 index 00000000..5a335588 --- /dev/null +++ b/src/features/sidebar/view/base/Drawer.tsx @@ -0,0 +1,44 @@ +import { ReactNode } from "react" +import { SxProps } from "@mui/system" +import { Drawer as MuiDrawer } from "@mui/material" + +export default function Drawer({ + variant, + width, + isOpen, + onClose, + keepMounted, + sx, + children +}: { + variant: "persistent" | "temporary", + width: number + isOpen: boolean + onClose?: () => void + keepMounted?: boolean + sx: SxProps, + children?: ReactNode +}) { + return ( + + {children} + + ) +} \ No newline at end of file diff --git a/src/features/sidebar/view/base/PrimaryHeader.tsx b/src/features/sidebar/view/base/PrimaryHeader.tsx new file mode 100644 index 00000000..9aa90c67 --- /dev/null +++ b/src/features/sidebar/view/base/PrimaryHeader.tsx @@ -0,0 +1,43 @@ +import { ReactNode } from "react" +import { Box, IconButton } from "@mui/material" +import ChevronLeftIcon from "@mui/icons-material/ChevronLeft" + +export default function PrimaryHeader({ + canCloseDrawer, + width, + onClose, + children +}: { + canCloseDrawer: boolean, + width: number, + onClose: () => void + children?: ReactNode +}) { + return ( + + + + + + {children} + + + ) +} diff --git a/src/features/sidebar/view/base/SecondaryContent.tsx b/src/features/sidebar/view/base/SecondaryContent.tsx new file mode 100644 index 00000000..d1aea2a1 --- /dev/null +++ b/src/features/sidebar/view/base/SecondaryContent.tsx @@ -0,0 +1,51 @@ +import { ReactNode } from "react" +import { SxProps } from "@mui/system" +import { Box, Toolbar } from "@mui/material" +import { styled } from "@mui/material/styles" + +interface MainProps { + drawerWidth: number + isDrawerOpen: boolean +} + +const Main = styled("main", { + shouldForwardProp: (prop) => prop !== "isDrawerOpen" +})(({ theme, drawerWidth, isDrawerOpen }) => ({ + display: "flex", + flexDirection: "column", + flexGrow: 1, + overflowY: "auto", + transition: theme.transitions.create("margin", { + easing: theme.transitions.easing.sharp, + duration: theme.transitions.duration.leavingScreen + }), + marginLeft: `-${drawerWidth}px`, + ...(isDrawerOpen && { + transition: theme.transitions.create("margin", { + easing: theme.transitions.easing.easeOut, + duration: theme.transitions.duration.enteringScreen, + }), + marginLeft: 0 + }) +})) + +export default function SecondaryContent({ + drawerWidth, + isDrawerOpen, + children, + sx +}: { + drawerWidth: number + isDrawerOpen: boolean + children: ReactNode + sx?: SxProps +}) { + return ( +
+ + + {children} + +
+ ) +} diff --git a/src/features/sidebar/view/base/SecondaryHeader.tsx b/src/features/sidebar/view/base/SecondaryHeader.tsx new file mode 100644 index 00000000..98d9b44c --- /dev/null +++ b/src/features/sidebar/view/base/SecondaryHeader.tsx @@ -0,0 +1,50 @@ +import { ReactNode } from "react" +import { SxProps } from "@mui/system" +import { Box, Divider, IconButton } from "@mui/material" +import { useTheme } from "@mui/material/styles" +import MenuIcon from "@mui/icons-material/Menu" + +export default function SecondaryHeader({ + showOpenDrawer, + onOpenDrawer, + trailingItem, + children, + sx +}: { + showOpenDrawer: boolean + onOpenDrawer: () => void + trailingItem?: ReactNode + children?: ReactNode + sx?: SxProps +}) { + const theme = useTheme() + return ( + + + + + + + {trailingItem} + + + {children} + + + ) +} diff --git a/src/features/sidebar/view/base/SecondaryWrapper.tsx b/src/features/sidebar/view/base/SecondaryWrapper.tsx new file mode 100644 index 00000000..f2d15c59 --- /dev/null +++ b/src/features/sidebar/view/base/SecondaryWrapper.tsx @@ -0,0 +1,50 @@ +import { ReactNode } from "react" +import { SxProps } from "@mui/system" +import { Stack } from "@mui/material" +import { styled } from "@mui/material/styles" + +interface WrapperStackProps { + drawerWidth: number + isDrawerOpen: boolean +} + +const WrapperStack = styled(Stack, { + shouldForwardProp: (prop) => prop !== "isDrawerOpen" +})(({ theme, drawerWidth, isDrawerOpen }) => ({ + transition: theme.transitions.create("margin", { + easing: theme.transitions.easing.sharp, + duration: theme.transitions.duration.leavingScreen + }), + marginLeft: `-${drawerWidth}px`, + ...(isDrawerOpen && { + transition: theme.transitions.create("margin", { + easing: theme.transitions.easing.easeOut, + duration: theme.transitions.duration.enteringScreen, + }), + marginLeft: 0 + }) +})) + +export default function SecondaryWrapper({ + drawerWidth, + isDrawerOpen, + children, + sx +}: { + drawerWidth: number + isDrawerOpen: boolean + children: ReactNode + sx?: SxProps +}) { + return ( + + {children} + + ) +} diff --git a/src/features/sidebar/view/base/responsive/Drawer.tsx b/src/features/sidebar/view/base/responsive/Drawer.tsx new file mode 100644 index 00000000..8a027f77 --- /dev/null +++ b/src/features/sidebar/view/base/responsive/Drawer.tsx @@ -0,0 +1,38 @@ +import { ReactNode } from "react" +import Drawer from "../Drawer" + +export default function RespnsiveDrawer({ + width, + isOpen, + onClose, + children +}: { + width: number + isOpen: boolean + onClose?: () => void + children?: ReactNode +}) { + return ( + <> + + {children} + + + {children} + + + ) +} \ No newline at end of file diff --git a/src/features/sidebar/view/base/responsive/SecondaryContent.tsx b/src/features/sidebar/view/base/responsive/SecondaryContent.tsx new file mode 100644 index 00000000..046ada23 --- /dev/null +++ b/src/features/sidebar/view/base/responsive/SecondaryContent.tsx @@ -0,0 +1,31 @@ +import { ReactNode } from "react" +import SecondaryContent from "../SecondaryContent" + +export default function RespnsiveDrawer({ + drawerWidth, + offsetContent, + children +}: { + drawerWidth: number + offsetContent: boolean + children: ReactNode +}) { + return ( + <> + + {children} + + + {children} + + + ) +} \ No newline at end of file diff --git a/src/features/sidebar/view/base/responsive/SecondaryHeader.tsx b/src/features/sidebar/view/base/responsive/SecondaryHeader.tsx new file mode 100644 index 00000000..c837d016 --- /dev/null +++ b/src/features/sidebar/view/base/responsive/SecondaryHeader.tsx @@ -0,0 +1,51 @@ +import { ReactNode } from "react" +import { SxProps } from "@mui/system" +import { Box, IconButton, Stack, Collapse } from "@mui/material" +import SecondaryHeader from "../SecondaryHeader" +import ExpandCircleDownIcon from "@mui/icons-material/ExpandCircleDown" + +export default function ResponsiveSecondaryHeader({ + showOpenDrawer, + onOpenDrawer, + showMobileToolbar, + onToggleMobileToolbar, + trailingItem, + mobileToolbar, + sx +}: { + showOpenDrawer: boolean + onOpenDrawer: () => void + showMobileToolbar: boolean + onToggleMobileToolbar: (showMobileToolbar: boolean) => void + trailingItem?: ReactNode + mobileToolbar?: ReactNode + sx?: SxProps +}) { + return ( + + {trailingItem} + + onToggleMobileToolbar(!showMobileToolbar) }> + + + + + } + > + {mobileToolbar && + + + {mobileToolbar} + + + } + + ) +} diff --git a/src/features/sidebar/view/base/responsive/SecondaryWrapper.tsx b/src/features/sidebar/view/base/responsive/SecondaryWrapper.tsx new file mode 100644 index 00000000..e217a2ad --- /dev/null +++ b/src/features/sidebar/view/base/responsive/SecondaryWrapper.tsx @@ -0,0 +1,32 @@ +import { ReactNode } from "react" +import SecondaryWrapper from "../SecondaryWrapper" + +export default function ResponsiveDrawer({ + drawerWidth, + offsetContent, + children +}: { + drawerWidth: number + offsetContent: boolean + children: ReactNode +}) { + const sx = { overflow: "hidden" } + return ( + <> + + {children} + + + {children} + + + ) +} \ No newline at end of file diff --git a/src/features/sidebar/view/base/responsive/SidebarContainer.tsx b/src/features/sidebar/view/base/responsive/SidebarContainer.tsx new file mode 100644 index 00000000..95629029 --- /dev/null +++ b/src/features/sidebar/view/base/responsive/SidebarContainer.tsx @@ -0,0 +1,51 @@ +import { ReactNode } from "react" +import { Stack } from "@mui/material" +import Drawer from "./Drawer" +import PrimaryHeader from "../PrimaryHeader" +import SecondaryWrapper from "./SecondaryWrapper" + +const SidebarContainer = ({ + canCloseDrawer, + isDrawerOpen, + onToggleDrawerOpen, + sidebarHeader, + sidebar, + header, + children +}: { + canCloseDrawer: boolean, + isDrawerOpen: boolean + onToggleDrawerOpen: (isDrawerOpen: boolean) => void + sidebarHeader?: ReactNode + sidebar: ReactNode + header?: ReactNode + children?: ReactNode +}) => { + const drawerWidth = 320 + return ( + + onToggleDrawerOpen(false)} + > + onToggleDrawerOpen(false)} + > + {sidebarHeader} + + {sidebar} + + + {header} +
+ {children} +
+
+
+ ) +} + +export default SidebarContainer diff --git a/src/features/sidebar/view/client/SidebarContainer.tsx b/src/features/sidebar/view/client/SidebarContainer.tsx index f9d2fb1d..d282fe47 100644 --- a/src/features/sidebar/view/client/SidebarContainer.tsx +++ b/src/features/sidebar/view/client/SidebarContainer.tsx @@ -1,55 +1,64 @@ "use client" import dynamic from "next/dynamic" -import { ReactNode } from "react" -import Image from "next/image" -import { Box, Stack } from "@mui/material" -import { useTheme } from "@mui/material/styles" +import { ReactNode, useEffect } from "react" import { useSessionStorage } from "usehooks-ts" -import BaseSidebarContainer from "../BaseSidebarContainer" -import SidebarContent from "../SidebarContent" +import ResponsiveSidebarContainer from "../base/responsive/SidebarContainer" +import ResponsiveSecondaryHeader from "../base/responsive/SecondaryHeader" +import Sidebar from "../Sidebar" +import SidebarHeader from "../SidebarHeader" const SidebarContainer = ({ - primary, - secondary, - toolbarTrailing + canCloseDrawer, + forceClose, + sidebar, + children, + toolbarTrailingItem, + mobileToolbar }: { - primary: ReactNode - secondary?: ReactNode - toolbarTrailing?: ReactNode + canCloseDrawer: boolean, + forceClose: boolean, + sidebar?: ReactNode + children?: ReactNode + toolbarTrailingItem?: ReactNode + mobileToolbar?: ReactNode }) => { const [open, setOpen] = useSessionStorage("isDrawerOpen", true) - const theme = useTheme() + const [showMobileToolbar, setShowMobileToolbar] = useSessionStorage("isMobileToolbarVisible", true) + useEffect(() => { + if (!canCloseDrawer) { + setOpen(true) + } + }, [canCloseDrawer, setOpen]) + useEffect(() => { + if (forceClose) { + setOpen(false) + } + }, [forceClose]) return ( - - Duck - + sidebarHeader={} + sidebar={ + + {sidebar} + } - primary={ - - {primary} - + header={ + setOpen(true)} + showMobileToolbar={showMobileToolbar} + onToggleMobileToolbar={setShowMobileToolbar} + trailingItem={toolbarTrailingItem} + mobileToolbar={mobileToolbar} + /> } - secondaryHeader={ - <> - {toolbarTrailing != undefined && - - {toolbarTrailing} - - } - - } - secondary={secondary} - /> + > + {children} + ) } diff --git a/src/features/sidebar/view/client/TrailingToolbar.tsx b/src/features/sidebar/view/client/TrailingToolbar.tsx new file mode 100644 index 00000000..60fdbf73 --- /dev/null +++ b/src/features/sidebar/view/client/TrailingToolbar.tsx @@ -0,0 +1,23 @@ +"use client" + +import { ReactNode } from "react" +import { Box } from "@mui/material" +import { useTheme } from "@mui/material/styles" + +export default function TrailingToolbar({ + children +}: { + children?: ReactNode +}) { + const theme = useTheme() + return ( + + {children} + + ) +} diff --git a/src/features/user/view/UserListItem.tsx b/src/features/user/view/UserListItem.tsx index 195263e5..a208b79f 100644 --- a/src/features/user/view/UserListItem.tsx +++ b/src/features/user/view/UserListItem.tsx @@ -15,7 +15,7 @@ const UserListItem: React.FC<{ display: "flex", flexDirection: "row", alignItems: "center", - padding: "15px" + padding: 2 }} > {!isLoading && user && user.picture &&