diff --git a/.env.example b/.env.example
index 0b7926ac..ad45d3e5 100644
--- a/.env.example
+++ b/.env.example
@@ -12,6 +12,7 @@ POSTGRESQL_PASSWORD=
POSTGRESQL_DB=shape-docs
REPOSITORY_NAME_SUFFIX=-openapi
HIDDEN_REPOSITORIES=
+NEW_PROJECT_TEMPLATE_REPOSITORY=shapehq/starter-openapi
GITHUB_WEBHOOK_SECRET=preshared secret also put in app configuration in GitHub
GITHUB_WEBHOK_REPOSITORY_ALLOWLIST=
GITHUB_WEBHOK_REPOSITORY_DISALLOWLIST=
diff --git a/__test__/auth/AuthjsAccountsOAuthTokenRepository.test.ts b/__test__/auth/AuthjsAccountsOAuthTokenRepository.test.ts
index 685c2c02..f31b7e7f 100644
--- a/__test__/auth/AuthjsAccountsOAuthTokenRepository.test.ts
+++ b/__test__/auth/AuthjsAccountsOAuthTokenRepository.test.ts
@@ -1,4 +1,4 @@
-import { AuthjsAccountsOAuthTokenRepository } from "../../src/features/auth/domain"
+import { AuthjsAccountsOAuthTokenRepository } from "@/features/auth/domain"
test("It gets token for user ID and provider", async () => {
let queryUserId: string | undefined
diff --git a/__test__/auth/CompositeLogOutHandler.test.ts b/__test__/auth/CompositeLogOutHandler.test.ts
index f95757bf..83c00fe1 100644
--- a/__test__/auth/CompositeLogOutHandler.test.ts
+++ b/__test__/auth/CompositeLogOutHandler.test.ts
@@ -1,4 +1,4 @@
-import { CompositeLogOutHandler } from "../../src/features/auth/domain"
+import { CompositeLogOutHandler } from "@/features/auth/domain"
test("It invokes all log out handlers", async () => {
let didCallLogOutHandler1 = false
diff --git a/__test__/auth/ErrorIgnoringLogOutHandler.test.ts b/__test__/auth/ErrorIgnoringLogOutHandler.test.ts
index a7bacdd0..d6375f3f 100644
--- a/__test__/auth/ErrorIgnoringLogOutHandler.test.ts
+++ b/__test__/auth/ErrorIgnoringLogOutHandler.test.ts
@@ -1,4 +1,4 @@
-import { ErrorIgnoringLogOutHandler } from "../../src/features/auth/domain"
+import { ErrorIgnoringLogOutHandler } from "@/features/auth/domain"
test("It ignores errors", async () => {
const sut = new ErrorIgnoringLogOutHandler({
diff --git a/__test__/auth/LockingAccessTokenRefresher.test.ts b/__test__/auth/LockingAccessTokenRefresher.test.ts
index 00ee777f..736f519c 100644
--- a/__test__/auth/LockingAccessTokenRefresher.test.ts
+++ b/__test__/auth/LockingAccessTokenRefresher.test.ts
@@ -1,4 +1,4 @@
-import { LockingOAuthTokenRefresher } from "../../src/features/auth/domain"
+import { LockingOAuthTokenRefresher } from "@/features/auth/domain"
test("It acquires a lock", async () => {
let didAcquireLock = false
diff --git a/__test__/auth/LogInHandler.test.ts b/__test__/auth/LogInHandler.test.ts
index d1dd0c0f..05f87227 100644
--- a/__test__/auth/LogInHandler.test.ts
+++ b/__test__/auth/LogInHandler.test.ts
@@ -1,4 +1,4 @@
-import { LogInHandler } from "../../src/features/auth/domain"
+import { LogInHandler } from "@/features/auth/domain"
test("It disallows logging in when account is undefined", async () => {
const sut = new LogInHandler({
diff --git a/__test__/auth/OAuthTokenDataSource.test.ts b/__test__/auth/OAuthTokenDataSource.test.ts
index 28390b61..857b4e8e 100644
--- a/__test__/auth/OAuthTokenDataSource.test.ts
+++ b/__test__/auth/OAuthTokenDataSource.test.ts
@@ -1,4 +1,4 @@
-import { OAuthTokenDataSource } from "../../src/features/auth/domain"
+import { OAuthTokenDataSource } from "@/features/auth/domain"
test("It reads OAuth token for user's ID", async () => {
let readUserId: string | undefined
diff --git a/__test__/auth/OAuthTokenRepository.test.ts b/__test__/auth/OAuthTokenRepository.test.ts
index e7f1a124..bf2c964f 100644
--- a/__test__/auth/OAuthTokenRepository.test.ts
+++ b/__test__/auth/OAuthTokenRepository.test.ts
@@ -1,4 +1,4 @@
-import { OAuthTokenRepository } from "../../src/features/auth/domain"
+import { OAuthTokenRepository } from "@/features/auth/domain"
test("It reads the auth token for the specified user", async () => {
let readProvider: string | undefined
diff --git a/__test__/auth/OAuthTokenSessionValidator.test.ts b/__test__/auth/OAuthTokenSessionValidator.test.ts
index 2abadbbd..60f75e3f 100644
--- a/__test__/auth/OAuthTokenSessionValidator.test.ts
+++ b/__test__/auth/OAuthTokenSessionValidator.test.ts
@@ -1,4 +1,4 @@
-import { OAuthTokenSessionValidator, SessionValidity } from "../../src/features/auth/domain"
+import { OAuthTokenSessionValidator, SessionValidity } from "@/features/auth/domain"
test("It reads the access token", async () => {
let didReadOAuthToken = false
diff --git a/__test__/auth/PersistingOAuthTokenRefresher.test.ts b/__test__/auth/PersistingOAuthTokenRefresher.test.ts
index a2537939..791c1b0c 100644
--- a/__test__/auth/PersistingOAuthTokenRefresher.test.ts
+++ b/__test__/auth/PersistingOAuthTokenRefresher.test.ts
@@ -1,4 +1,4 @@
-import { PersistingOAuthTokenRefresher, OAuthToken } from "../../src/features/auth/domain"
+import { PersistingOAuthTokenRefresher, OAuthToken } from "@/features/auth/domain"
test("It refreshes OAuth token using provided refresh token", async () => {
let usedRefreshToken: string | undefined
diff --git a/__test__/auth/UserDataCleanUpLogOutHandler.test.ts b/__test__/auth/UserDataCleanUpLogOutHandler.test.ts
index 6cbfc03d..9b7ec310 100644
--- a/__test__/auth/UserDataCleanUpLogOutHandler.test.ts
+++ b/__test__/auth/UserDataCleanUpLogOutHandler.test.ts
@@ -1,4 +1,4 @@
-import { UserDataCleanUpLogOutHandler } from "../../src/features/auth/domain"
+import { UserDataCleanUpLogOutHandler } from "@/features/auth/domain"
test("It deletes data for the read user ID", async () => {
let deletedUserId: string | undefined
diff --git a/__test__/common/github/OAuthTokenRefreshingGitHubClient.test.ts b/__test__/common/github/OAuthTokenRefreshingGitHubClient.test.ts
index 7e353354..548d85a3 100644
--- a/__test__/common/github/OAuthTokenRefreshingGitHubClient.test.ts
+++ b/__test__/common/github/OAuthTokenRefreshingGitHubClient.test.ts
@@ -1,10 +1,10 @@
-import { OAuthTokenRefreshingGitHubClient } from "@/common"
import {
+ OAuthTokenRefreshingGitHubClient,
GraphQLQueryRequest,
GetRepositoryContentRequest,
GetPullRequestCommentsRequest,
AddCommentToPullRequestRequest
-} from "@/common/github/IGitHubClient"
+} from "@/common"
test("It forwards a GraphQL request", async () => {
let forwardedRequest: GraphQLQueryRequest | undefined
diff --git a/__test__/common/utils/listFromCommaSeparatedString.test.ts b/__test__/common/utils/listFromCommaSeparatedString.test.ts
index e3f75d43..08fad85b 100644
--- a/__test__/common/utils/listFromCommaSeparatedString.test.ts
+++ b/__test__/common/utils/listFromCommaSeparatedString.test.ts
@@ -1,4 +1,4 @@
-import listFromCommaSeparatedString from "@/common/utils/listFromCommaSeparatedString"
+import { listFromCommaSeparatedString } from "@/common"
test("It returns an empty list given undefined", async () => {
const result = listFromCommaSeparatedString(undefined)
diff --git a/__test__/common/utils/saneParseInt.test.ts b/__test__/common/utils/saneParseInt.test.ts
index ebd217d4..5acc1e28 100644
--- a/__test__/common/utils/saneParseInt.test.ts
+++ b/__test__/common/utils/saneParseInt.test.ts
@@ -1,4 +1,4 @@
-import saneParseInt from "@/common/utils/saneParseInt"
+import { saneParseInt } from "@/common"
test("It parses an integer", async () => {
// @ts-ignore
diff --git a/__test__/hooks/FilteringPullRequestEventHandler.test.ts b/__test__/hooks/FilteringPullRequestEventHandler.test.ts
index 2c1e8045..65b5836f 100644
--- a/__test__/hooks/FilteringPullRequestEventHandler.test.ts
+++ b/__test__/hooks/FilteringPullRequestEventHandler.test.ts
@@ -1,4 +1,4 @@
-import { FilteringPullRequestEventHandler } from "../../src/features/hooks/domain"
+import { FilteringPullRequestEventHandler } from "@/features/hooks/domain"
test("It calls pullRequestOpened(_:) when event is included", async () => {
let didCall = false
diff --git a/__test__/hooks/PostCommentPullRequestEventHandler.test.ts b/__test__/hooks/PostCommentPullRequestEventHandler.test.ts
index 46f7ee55..153c3fa7 100644
--- a/__test__/hooks/PostCommentPullRequestEventHandler.test.ts
+++ b/__test__/hooks/PostCommentPullRequestEventHandler.test.ts
@@ -1,4 +1,4 @@
-import { PostCommentPullRequestEventHandler } from "../../src/features/hooks/domain"
+import { PostCommentPullRequestEventHandler } from "@/features/hooks/domain"
test("It comments when opening a pull request", async () => {
let didComment = false
diff --git a/__test__/hooks/PullRequestCommenter.test.ts b/__test__/hooks/PullRequestCommenter.test.ts
index 358c5af5..ff4cadde 100644
--- a/__test__/hooks/PullRequestCommenter.test.ts
+++ b/__test__/hooks/PullRequestCommenter.test.ts
@@ -1,4 +1,4 @@
-import { PullRequestCommenter } from "../../src/features/hooks/domain"
+import { PullRequestCommenter } from "@/features/hooks/domain"
test("It adds comment when none exist", async () => {
let didAddComment = false
diff --git a/__test__/hooks/RepositoryNameEventFilter.test.ts b/__test__/hooks/RepositoryNameEventFilter.test.ts
index 473ce34b..2b06ca1d 100644
--- a/__test__/hooks/RepositoryNameEventFilter.test.ts
+++ b/__test__/hooks/RepositoryNameEventFilter.test.ts
@@ -1,4 +1,4 @@
-import { RepositoryNameEventFilter } from "../../src/features/hooks/domain"
+import { RepositoryNameEventFilter } from "@/features/hooks/domain"
test("It does not include repositories that do not have the \"-openapi\" suffix", async () => {
const sut = new RepositoryNameEventFilter({
diff --git a/__test__/projects/CachingProjectDataSource.test.ts b/__test__/projects/CachingProjectDataSource.test.ts
index 0a9fc418..9365f53a 100644
--- a/__test__/projects/CachingProjectDataSource.test.ts
+++ b/__test__/projects/CachingProjectDataSource.test.ts
@@ -1,4 +1,4 @@
-import { Project, CachingProjectDataSource } from "../../src/features/projects/domain"
+import { Project, CachingProjectDataSource } from "@/features/projects/domain"
test("It caches projects read from the data source", async () => {
const projects: Project[] = [{
diff --git a/__test__/projects/FilteringGitHubRepositoryDataSource.test.ts b/__test__/projects/FilteringGitHubRepositoryDataSource.test.ts
index 1f408320..e834d7b0 100644
--- a/__test__/projects/FilteringGitHubRepositoryDataSource.test.ts
+++ b/__test__/projects/FilteringGitHubRepositoryDataSource.test.ts
@@ -1,4 +1,4 @@
-import { FilteringGitHubRepositoryDataSource } from "../../src/features/projects/domain"
+import { FilteringGitHubRepositoryDataSource } from "@/features/projects/domain"
test("It returns all repositories when no hidden repositories are provided", async () => {
const sut = new FilteringGitHubRepositoryDataSource({
diff --git a/__test__/projects/GitHubProjectDataSource.test.ts b/__test__/projects/GitHubProjectDataSource.test.ts
index e000a115..ac01eaa1 100644
--- a/__test__/projects/GitHubProjectDataSource.test.ts
+++ b/__test__/projects/GitHubProjectDataSource.test.ts
@@ -1,6 +1,4 @@
-import {
- GitHubProjectDataSource
- } from "../../src/features/projects/data"
+import { GitHubProjectDataSource } from "@/features/projects/data"
test("It loads repositories from data source", async () => {
let didLoadRepositories = false
diff --git a/__test__/projects/GitHubRepositoryDataSource.test.ts b/__test__/projects/GitHubRepositoryDataSource.test.ts
index c6763ab2..9235a28b 100644
--- a/__test__/projects/GitHubRepositoryDataSource.test.ts
+++ b/__test__/projects/GitHubRepositoryDataSource.test.ts
@@ -1,6 +1,4 @@
-import {
- GitHubRepositoryDataSource
- } from "../../src/features/projects/data"
+import { GitHubRepositoryDataSource } from "@/features/projects/data"
test("It loads repositories from data source", async () => {
let didLoadRepositories = false
diff --git a/__test__/projects/ProjectConfigParser.test.ts b/__test__/projects/ProjectConfigParser.test.ts
index 4cb0ee0e..4b2256fb 100644
--- a/__test__/projects/ProjectConfigParser.test.ts
+++ b/__test__/projects/ProjectConfigParser.test.ts
@@ -1,4 +1,4 @@
-import { ProjectConfigParser } from "../../src/features/projects/domain"
+import { ProjectConfigParser } from "@/features/projects/domain"
test("It parses an empty string", async () => {
const sut = new ProjectConfigParser()
diff --git a/__test__/projects/getSelection.test.ts b/__test__/projects/getProjectSelectionFromPath.test.ts
similarity index 93%
rename from __test__/projects/getSelection.test.ts
rename to __test__/projects/getProjectSelectionFromPath.test.ts
index a5a544ca..7991fb1d 100644
--- a/__test__/projects/getSelection.test.ts
+++ b/__test__/projects/getProjectSelectionFromPath.test.ts
@@ -1,7 +1,7 @@
-import { getSelection } from "../../src/features/projects/domain"
+import { getProjectSelectionFromPath } from "@/features/projects/domain"
test("It selects the first project when there is only one project and path is empty", () => {
- const sut = getSelection({
+ const sut = getProjectSelectionFromPath({
path: "",
projects: [{
id: "foo",
@@ -28,7 +28,7 @@ test("It selects the first project when there is only one project and path is em
})
test("It selects the first version and specification of the specified project", () => {
- const sut = getSelection({
+ const sut = getProjectSelectionFromPath({
path: "/acme/bar",
projects: [{
id: "foo",
@@ -71,7 +71,7 @@ test("It selects the first version and specification of the specified project",
})
test("It selects the first specification of the specified project and version", () => {
- const sut = getSelection({
+ const sut = getProjectSelectionFromPath({
path: "/acme/bar/baz2",
projects: [{
id: "foo",
@@ -110,7 +110,7 @@ test("It selects the first specification of the specified project and version",
})
test("It selects the specification of the specified version", () => {
- const sut = getSelection({
+ const sut = getProjectSelectionFromPath({
path: "/acme/bar/baz2",
projects: [{
id: "foo",
@@ -153,7 +153,7 @@ test("It selects the specification of the specified version", () => {
})
test("It selects the specified project, version, and specification", () => {
- const sut = getSelection({
+ const sut = getProjectSelectionFromPath({
path: "/acme/bar/baz2/hello2",
projects: [{
id: "foo",
@@ -196,7 +196,7 @@ test("It selects the specified project, version, and specification", () => {
})
test("It returns a undefined project, version, and specification when the selected project cannot be found", () => {
- const sut = getSelection({
+ const sut = getProjectSelectionFromPath({
path: "/acme/foo",
projects: [{
id: "bar",
@@ -213,7 +213,7 @@ test("It returns a undefined project, version, and specification when the select
})
test("It returns a undefined version and specification when the selected version cannot be found", () => {
- const sut = getSelection({
+ const sut = getProjectSelectionFromPath({
path: "/acme/foo/bar",
projects: [{
id: "foo",
@@ -236,7 +236,7 @@ test("It returns a undefined version and specification when the selected version
})
test("It returns a undefined specification when the selected specification cannot be found", () => {
- const sut = getSelection({
+ const sut = getProjectSelectionFromPath({
path: "/acme/foo/bar/baz",
projects: [{
id: "foo",
@@ -263,7 +263,7 @@ test("It returns a undefined specification when the selected specification canno
})
test("It moves specification ID to version ID if needed", () => {
- const sut = getSelection({
+ const sut = getProjectSelectionFromPath({
path: "/acme/foo/bar/baz",
projects: [{
id: "foo",
diff --git a/__test__/projects/projectNavigator.test.ts b/__test__/projects/projectNavigator.test.ts
index 4cc0cb6a..d1d71825 100644
--- a/__test__/projects/projectNavigator.test.ts
+++ b/__test__/projects/projectNavigator.test.ts
@@ -1,4 +1,4 @@
-import { ProjectNavigator } from "../../src/features/projects/domain"
+import { ProjectNavigator } from "@/features/projects/domain"
test("It navigates to the correct path", async () => {
let pushedPath: string | undefined
diff --git a/__test__/projects/updateWindowTitle.test.ts b/__test__/projects/updateWindowTitle.test.ts
index 5521f406..86509b38 100644
--- a/__test__/projects/updateWindowTitle.test.ts
+++ b/__test__/projects/updateWindowTitle.test.ts
@@ -1,4 +1,4 @@
-import { updateWindowTitle } from "../../src/features/projects/domain"
+import { updateWindowTitle } from "@/features/projects/domain"
test("It uses default title when there is no selection", async () => {
const store: { title: string } = { title: "" }
diff --git a/src/app/(authed)/(home)/[[...slug]]/layout.tsx b/src/app/(authed)/(home)/[[...slug]]/layout.tsx
new file mode 100644
index 00000000..36fb09c1
--- /dev/null
+++ b/src/app/(authed)/(home)/[[...slug]]/layout.tsx
@@ -0,0 +1,23 @@
+"use client"
+
+import SecondarySplitHeader from "@/features/sidebar/view/SecondarySplitHeader"
+import TrailingToolbarItem from "@/features/projects/view/toolbar/TrailingToolbarItem"
+import MobileToolbar from "@/features/projects/view/toolbar/MobileToolbar"
+import { useProjectSelection } from "@/features/projects/data"
+
+export default function Page({ children }: { children: React.ReactNode }) {
+ const { project } = useProjectSelection()
+ if (!project) {
+ return <>>
+ }
+ return (
+ <>
+ >
+
+
+
+ {children}
+
+ >
+ )
+}
\ No newline at end of file
diff --git a/src/app/(authed)/(home)/[[...slug]]/page.tsx b/src/app/(authed)/(home)/[[...slug]]/page.tsx
new file mode 100644
index 00000000..e70225d5
--- /dev/null
+++ b/src/app/(authed)/(home)/[[...slug]]/page.tsx
@@ -0,0 +1,43 @@
+"use client"
+
+import { useContext, useEffect } from "react"
+import { ProjectsContainerContext } from "@/common"
+import DelayedLoadingIndicator from "@/common/ui/DelayedLoadingIndicator"
+import ErrorMessage from "@/common/ui/ErrorMessage"
+import { updateWindowTitle } from "@/features/projects/domain"
+import { useProjectSelection } from "@/features/projects/data"
+import Documentation from "@/features/projects/view/Documentation"
+
+export default function Page() {
+ const { error, isLoading } = useContext(ProjectsContainerContext)
+ const { project, version, specification, navigateToSelectionIfNeeded } = useProjectSelection()
+ // Ensure the URL reflects the current selection of project, version, and specification.
+ useEffect(() => {
+ navigateToSelectionIfNeeded()
+ }, [project, version, specification, navigateToSelectionIfNeeded])
+ // Update the window title to match selected project.
+ const siteName = process.env.NEXT_PUBLIC_SHAPE_DOCS_TITLE || ""
+ useEffect(() => {
+ updateWindowTitle({
+ storage: document,
+ defaultTitle: siteName,
+ project,
+ version,
+ specification
+ })
+ }, [siteName, project, version, specification])
+ if (project && version && specification) {
+ return
+ } else if (project && !version) {
+ return
+ } else if (project && !specification) {
+ return
+ } else if (isLoading) {
+ return
+ } else if (error) {
+ return
+ } else {
+ // No project is selected so we will not show anything.
+ return <>>
+ }
+}
diff --git a/src/app/(authed)/(home)/layout.tsx b/src/app/(authed)/(home)/layout.tsx
new file mode 100644
index 00000000..49b27871
--- /dev/null
+++ b/src/app/(authed)/(home)/layout.tsx
@@ -0,0 +1,36 @@
+"use client"
+
+import { useContext } from "react"
+import { SplitView } from "@/features/sidebar/view"
+import { useProjects, useProjectSelection } from "@/features/projects/data"
+import {
+ ProjectsContainerContext,
+ ServerSideCachedProjectsContext
+} from "@/common"
+
+export default function Layout({ children }: { children: React.ReactNode }) {
+ const { projects, error, isLoading } = useProjects()
+ // Update projects provided to child components, using cached projects from the server if needed.
+ const serverSideCachedProjects = useContext(ServerSideCachedProjectsContext)
+ const newProjectsContainer = { projects, error, isLoading }
+ if (isLoading && serverSideCachedProjects) {
+ newProjectsContainer.isLoading = false
+ newProjectsContainer.projects = serverSideCachedProjects
+ }
+ return (
+
+
+ {children}
+
+
+ )
+}
+
+const SplitViewWrapper = ({ children }: { children: React.ReactNode }) => {
+ const { project } = useProjectSelection()
+ return (
+
+ {children}
+
+ )
+}
\ No newline at end of file
diff --git a/src/app/(authed)/(home)/new/page.tsx b/src/app/(authed)/(home)/new/page.tsx
new file mode 100644
index 00000000..bdbbd356
--- /dev/null
+++ b/src/app/(authed)/(home)/new/page.tsx
@@ -0,0 +1,56 @@
+import Link from "next/link"
+import { env, splitOwnerAndRepository } from "@/common"
+
+const Page = () => {
+ const repositoryNameSuffix = env.getOrThrow("REPOSITORY_NAME_SUFFIX")
+ const templateName = env.get("NEW_PROJECT_TEMPLATE_REPOSITORY")
+ const projectName = "Nordisk Film"
+ const suffixedRepositoryName = makeFullRepositoryName({
+ name: projectName,
+ suffix: repositoryNameSuffix
+ })
+ const newGitHubRepositoryLink = makeNewGitHubRepositoryLink({
+ templateName,
+ repositoryName: suffixedRepositoryName,
+ description: `Contains OpenAPI specifications for ${projectName}`
+ })
+ return (
+
+ {newGitHubRepositoryLink}
+
+ )
+}
+
+export default Page
+
+function makeFullRepositoryName({ name, suffix }: { name: string, suffix: string }) {
+ const safeRepositoryName = name
+ .trim()
+ .toLowerCase()
+ .replace(/[^a-z0-9-]+/g, "")
+ .replace(/\s+/g, "-")
+ return `${safeRepositoryName}${suffix}`
+}
+
+function makeNewGitHubRepositoryLink({
+ templateName,
+ repositoryName,
+ description
+}: {
+ templateName?: string,
+ repositoryName: string,
+ description: string
+}) {
+ let url = `https://github.com/new`
+ + `?name=${encodeURIComponent(repositoryName)}`
+ + `&description=${encodeURIComponent(description)}`
+ + `&visibility=private`
+ if (templateName) {
+ const templateRepository = splitOwnerAndRepository(templateName)
+ if (templateRepository) {
+ url += `&template_owner=${encodeURIComponent(templateRepository.owner)}`
+ url += `&template_name=${encodeURIComponent(templateRepository.repository)}`
+ }
+ }
+ return url
+}
diff --git a/src/app/(authed)/layout.tsx b/src/app/(authed)/layout.tsx
new file mode 100644
index 00000000..bc977cf5
--- /dev/null
+++ b/src/app/(authed)/layout.tsx
@@ -0,0 +1,25 @@
+import { redirect } from "next/navigation"
+import { SessionProvider } from "next-auth/react"
+import { session, projectRepository } from "@/composition"
+import ErrorHandler from "@/common/ui/ErrorHandler"
+import SessionBarrier from "@/features/auth/view/SessionBarrier"
+import ServerSideCachedProjectsProvider from "@/features/projects/view/ServerSideCachedProjectsProvider"
+
+export default async function Layout({ children }: { children: React.ReactNode }) {
+ const isAuthenticated = await session.getIsAuthenticated()
+ if (!isAuthenticated) {
+ return redirect("/api/auth/signin")
+ }
+ const projects = await projectRepository.get()
+ return (
+
+
+
+
+ {children}
+
+
+
+
+ )
+}
\ No newline at end of file
diff --git a/src/app/[[...slug]]/page.tsx b/src/app/[[...slug]]/page.tsx
deleted file mode 100644
index 092f7f40..00000000
--- a/src/app/[[...slug]]/page.tsx
+++ /dev/null
@@ -1,37 +0,0 @@
-import { redirect } from "next/navigation"
-import { SessionProvider } from "next-auth/react"
-import { session, projectRepository } from "@/composition"
-import ErrorHandler from "@/common/errors/client/ErrorHandler"
-import SessionBarrier from "@/features/auth/view/SessionBarrier"
-import ProjectsPage from "@/features/projects/view/ProjectsPage"
-
-type PageParams = { slug: string | string[] }
-
-export default async function Page({ params }: { params: PageParams }) {
- const isAuthenticated = await session.getIsAuthenticated()
- if (!isAuthenticated) {
- return redirect("/api/auth/signin")
- }
- return (
-
-
-
-
-
-
-
- )
-}
-
-function getPath(slug: string | string[] | undefined) {
- if (slug === undefined) {
- return "/"
- } else if (typeof slug === "string") {
- return "/" + slug
- } else {
- return slug.reduce((e, acc) => `${e}/${acc}`, "")
- }
-}
diff --git a/src/common/contexts.ts b/src/common/contexts.ts
new file mode 100644
index 00000000..e455e5b1
--- /dev/null
+++ b/src/common/contexts.ts
@@ -0,0 +1,19 @@
+"use client"
+
+import { createContext } from "react"
+import { Project, } from "@/features/projects/domain"
+
+export const SidebarContext = createContext<{ isToggleable: boolean }>({ isToggleable: true })
+
+type ProjectsContainer = {
+ readonly projects: Project[]
+ readonly isLoading: boolean
+ readonly error?: Error
+}
+
+export const ProjectsContainerContext = createContext({
+ isLoading: true,
+ projects: []
+})
+
+export const ServerSideCachedProjectsContext = createContext(undefined)
diff --git a/src/common/index.ts b/src/common/index.ts
index fdb90059..83b57c0c 100644
--- a/src/common/index.ts
+++ b/src/common/index.ts
@@ -1,3 +1,4 @@
+export * from "./contexts"
export * from "./db"
export * from "./errors"
export * from "./github"
diff --git a/src/common/state/useSidebarOpen.tsx b/src/common/state/useSidebarOpen.tsx
deleted file mode 100644
index 2d0c3b9e..00000000
--- a/src/common/state/useSidebarOpen.tsx
+++ /dev/null
@@ -1,5 +0,0 @@
-import { useSessionStorage } from "usehooks-ts"
-
-export default function useSidebarOpen() {
- return useSessionStorage("isSidebarOpen", true)
-}
\ No newline at end of file
diff --git a/src/common/theme/ThemeRegistry.tsx b/src/common/theme/ThemeRegistry.tsx
index ffb45ab7..95dbeadd 100644
--- a/src/common/theme/ThemeRegistry.tsx
+++ b/src/common/theme/ThemeRegistry.tsx
@@ -1,6 +1,6 @@
"use client"
-import { ReactNode, useState } from "react"
+import { useState } from "react"
import createCache, { Options } from "@emotion/cache"
import { useServerInsertedHTML } from "next/navigation"
import { CacheProvider } from "@emotion/react"
@@ -10,7 +10,7 @@ import theme from "./theme"
type ThemeRegistryProps = {
options: Options
- children: ReactNode
+ children: React.ReactNode
}
// This implementation is from emotion-js
diff --git a/src/common/loading/DelayedLoadingIndicator.tsx b/src/common/ui/DelayedLoadingIndicator.tsx
similarity index 82%
rename from src/common/loading/DelayedLoadingIndicator.tsx
rename to src/common/ui/DelayedLoadingIndicator.tsx
index ce90d90b..74071f9e 100644
--- a/src/common/loading/DelayedLoadingIndicator.tsx
+++ b/src/common/ui/DelayedLoadingIndicator.tsx
@@ -1,11 +1,9 @@
+"use client"
+
import { useState, useEffect } from "react"
import LoadingIndicator from "./LoadingIndicator"
-const DelayedLoadingIndicator = ({
- delay
-}: {
- delay?: number
-}) => {
+const DelayedLoadingIndicator = ({ delay }: { delay?: number }) => {
const [isVisible, setVisible] = useState(false)
useEffect(() => {
const timer = setTimeout(() => {
diff --git a/src/common/errors/client/ErrorHandler.tsx b/src/common/ui/ErrorHandler.tsx
similarity index 81%
rename from src/common/errors/client/ErrorHandler.tsx
rename to src/common/ui/ErrorHandler.tsx
index 7e928289..d206fa4b 100644
--- a/src/common/errors/client/ErrorHandler.tsx
+++ b/src/common/ui/ErrorHandler.tsx
@@ -3,11 +3,7 @@
import { SWRConfig } from "swr"
import { FetcherError } from "@/common"
-export default function ErrorHandler({
- children
-}: {
- children: React.ReactNode
-}) {
+export default function ErrorHandler({ children }: { children: React.ReactNode }) {
const onSWRError = (error: FetcherError) => {
if (typeof window === "undefined") {
return
diff --git a/src/features/projects/view/ErrorMessage.tsx b/src/common/ui/ErrorMessage.tsx
similarity index 100%
rename from src/features/projects/view/ErrorMessage.tsx
rename to src/common/ui/ErrorMessage.tsx
diff --git a/src/common/loading/LoadingIndicator.tsx b/src/common/ui/LoadingIndicator.tsx
similarity index 98%
rename from src/common/loading/LoadingIndicator.tsx
rename to src/common/ui/LoadingIndicator.tsx
index dc464219..f56dd4d8 100644
--- a/src/common/loading/LoadingIndicator.tsx
+++ b/src/common/ui/LoadingIndicator.tsx
@@ -1,3 +1,5 @@
+"use client"
+
import { useState, useEffect } from "react"
import { Box, Typography } from "@mui/material"
import { useTheme } from "@mui/material/styles"
diff --git a/src/common/ui/MenuItemHover.tsx b/src/common/ui/MenuItemHover.tsx
index 0326248e..d627a980 100644
--- a/src/common/ui/MenuItemHover.tsx
+++ b/src/common/ui/MenuItemHover.tsx
@@ -1,4 +1,3 @@
-import { ReactNode } from "react"
import { SxProps } from "@mui/system"
import { Box } from "@mui/material"
import useMediaQuery from "@mui/material/useMediaQuery"
@@ -9,7 +8,7 @@ const MenuItemHover = ({
sx
}: {
disabled?: boolean
- children: ReactNode
+ children: React.ReactNode
sx?: SxProps
}) => {
const isHoverSupported = useMediaQuery("(hover: hover)")
diff --git a/src/common/ui/ThickDivider.tsx b/src/common/ui/ThickDivider.tsx
index f73cf914..1dc1da3c 100644
--- a/src/common/ui/ThickDivider.tsx
+++ b/src/common/ui/ThickDivider.tsx
@@ -2,11 +2,7 @@ import { SxProps } from "@mui/system"
import { Box } from "@mui/material"
import { useTheme } from "@mui/material/styles"
-const ThickDivider = ({
- sx
-}: {
- sx?: SxProps
-}) => {
+const ThickDivider = ({ sx }: { sx?: SxProps }) => {
const theme = useTheme()
return (
{
- return window.navigator.userAgent.toLowerCase().includes("mac")
+ return window.navigator.userAgent.toLowerCase().includes("mac")
}
export default isMac
\ No newline at end of file
diff --git a/src/features/auth/view/SessionBarrier.tsx b/src/features/auth/view/SessionBarrier.tsx
index 8f1fdf0d..2e5e02ad 100644
--- a/src/features/auth/view/SessionBarrier.tsx
+++ b/src/features/auth/view/SessionBarrier.tsx
@@ -1,16 +1,17 @@
-import { ReactNode } from "react"
+import { redirect } from "next/navigation"
import { blockingSessionValidator } from "@/composition"
-import ClientSessionBarrier from "./client/SessionBarrier"
+import { SessionValidity } from "../domain"
export default async function SessionBarrier({
children
}: {
- children: ReactNode
+ children: React.ReactNode
}) {
const sessionValidity = await blockingSessionValidator.validateSession()
- return (
-
- {children}
-
- )
+ switch (sessionValidity) {
+ case SessionValidity.VALID:
+ return <>{children}>
+ case SessionValidity.INVALID_ACCESS_TOKEN:
+ return redirect("/api/auth/signout")
+ }
}
diff --git a/src/features/auth/view/client/InvalidSessionPage.tsx b/src/features/auth/view/client/InvalidSessionPage.tsx
deleted file mode 100644
index 8a796a46..00000000
--- a/src/features/auth/view/client/InvalidSessionPage.tsx
+++ /dev/null
@@ -1,45 +0,0 @@
-"use client"
-
-import { ReactNode } from "react"
-import { Stack, Typography } from "@mui/material"
-import SidebarContainer from "@/features/sidebar/view/client/SidebarContainer"
-import useSidebarOpen from "@/common/state/useSidebarOpen"
-
-export default function InvalidSessionPage({
- title,
- children
-}: {
- title?: ReactNode
- children?: ReactNode
-}) {
- const [isSidebarOpen, setSidebarOpen] = useSidebarOpen()
- return (
-
-
-
- {title &&
-
- {title}
-
- }
-
- {children}
-
-
-
-
- )
-}
diff --git a/src/features/auth/view/client/SessionBarrier.tsx b/src/features/auth/view/client/SessionBarrier.tsx
deleted file mode 100644
index 55408cfd..00000000
--- a/src/features/auth/view/client/SessionBarrier.tsx
+++ /dev/null
@@ -1,24 +0,0 @@
-"use client"
-
-import { ReactNode } from "react"
-import { SessionValidity } from "../../domain"
-import InvalidSessionPage from "./InvalidSessionPage"
-
-export default function SessionBarrier({
- sessionValidity,
- children
-}: {
- sessionValidity: SessionValidity
- children: ReactNode
-}) {
- switch (sessionValidity) {
- case SessionValidity.VALID:
- return <>{children}>
- case SessionValidity.INVALID_ACCESS_TOKEN:
- return (
-
- It was not possible to obtain access to the repositories on GitHub.
-
- )
- }
-}
diff --git a/src/features/auth/view/client/SessionProvider.tsx b/src/features/auth/view/client/SessionProvider.tsx
deleted file mode 100644
index 37c3aee7..00000000
--- a/src/features/auth/view/client/SessionProvider.tsx
+++ /dev/null
@@ -1,15 +0,0 @@
-"use client"
-
-import { SessionProvider as NextAuthSessionProvider } from "next-auth/react"
-
-export default function SessionProvider({
- children
-}: {
- children: React.ReactNode
-}) {
- return (
-
- {children}
-
- )
-}
diff --git a/src/features/docs/view/LoadingWrapper.tsx b/src/features/docs/view/LoadingWrapper.tsx
index 65c70157..0a86e04b 100644
--- a/src/features/docs/view/LoadingWrapper.tsx
+++ b/src/features/docs/view/LoadingWrapper.tsx
@@ -1,13 +1,12 @@
-import { ReactNode } from "react"
import { Box } from "@mui/material"
-import LoadingIndicator from "@/common/loading/LoadingIndicator"
+import LoadingIndicator from "@/common/ui/LoadingIndicator"
const LoadingWrapper = ({
showLoadingIndicator,
children
}: {
showLoadingIndicator: boolean,
- children: ReactNode
+ children: React.ReactNode
}) => {
return (
e.login)
- return [viewer.login].concat(organizations)
+ const organizationLogins = viewer.organizations.nodes
+ .map((e: { login: string }) => e.login)
+ return [viewer.login].concat(organizationLogins)
}
}
diff --git a/src/features/projects/data/GitHubRepositoryDataSource.ts b/src/features/projects/data/GitHubRepositoryDataSource.ts
index e94e228a..569a4bd1 100644
--- a/src/features/projects/data/GitHubRepositoryDataSource.ts
+++ b/src/features/projects/data/GitHubRepositoryDataSource.ts
@@ -69,11 +69,7 @@ export default class GitHubProjectDataSource implements IGitHubRepositoryDataSou
return await this.getRepositoriesForLogins({ logins })
}
- private async getRepositoriesForLogins({
- logins
- }: {
- logins: string[]
- }): Promise {
+ private async getRepositoriesForLogins({ logins }: { logins: string[] }): Promise {
let searchQueries: string[] = []
// Search for all private repositories the user has access to. This is needed to find
// repositories for external collaborators who do not belong to an organization.
diff --git a/src/features/projects/data/index.ts b/src/features/projects/data/index.ts
index 8155c986..affecff1 100644
--- a/src/features/projects/data/index.ts
+++ b/src/features/projects/data/index.ts
@@ -1,5 +1,6 @@
export { default as GitHubProjectDataSource } from "./GitHubProjectDataSource"
export * from "./GitHubProjectDataSource"
export { default as useProjects } from "./useProjects"
+export { default as useProjectSelection } from "./useProjectSelection"
export { default as GitHubLoginDataSource } from "./GitHubLoginDataSource"
export { default as GitHubRepositoryDataSource } from "./GitHubRepositoryDataSource"
diff --git a/src/features/projects/data/useProjectSelection.ts b/src/features/projects/data/useProjectSelection.ts
new file mode 100644
index 00000000..6c9229e1
--- /dev/null
+++ b/src/features/projects/data/useProjectSelection.ts
@@ -0,0 +1,71 @@
+"use client"
+
+import { useRouter, usePathname } from "next/navigation"
+import { useContext } from "react"
+import useMediaQuery from "@mui/material/useMediaQuery"
+import { useTheme } from "@mui/material/styles"
+import { ProjectsContainerContext } from "@/common"
+import { Project, ProjectNavigator, getProjectSelectionFromPath } from "../domain"
+import { useSidebarOpen } from "@/features/sidebar/data"
+
+export default function useProjectSelection() {
+ const router = useRouter()
+ const pathname = usePathname()
+ const { projects } = useContext(ProjectsContainerContext)
+ const selection = getProjectSelectionFromPath({ projects, path: pathname })
+ const pathnameReader = {
+ get pathname() {
+ return pathname
+ }
+ }
+ const projectNavigator = new ProjectNavigator({ router, pathnameReader })
+ const [, setSidebarOpen] = useSidebarOpen()
+ const theme = useTheme()
+ const isDesktopLayout = useMediaQuery(theme.breakpoints.up("sm"))
+ return {
+ get project() {
+ return selection.project
+ },
+ get version() {
+ return selection.version
+ },
+ get specification() {
+ return selection.specification
+ },
+ selectProject: (project: Project) => {
+ if (!isDesktopLayout) {
+ setSidebarOpen(false)
+ }
+ const version = project.versions[0]
+ const specification = version.specifications[0]
+ projectNavigator.navigate(
+ project.owner,
+ project.name,
+ version.id,
+ specification.id
+ )
+ },
+ selectVersion: (versionId: string) => {
+ projectNavigator.navigateToVersion(
+ selection.project!,
+ versionId,
+ selection.specification!.name
+ )
+ },
+ selectSpecification: (specificationId: string) => {
+ projectNavigator.navigate(
+ selection.project!.owner,
+ selection.project!.name,
+ selection.version!.id, specificationId
+ )
+ },
+ navigateToSelectionIfNeeded: () => {
+ projectNavigator.navigateIfNeeded({
+ projectOwner: selection.project?.owner,
+ projectName: selection.project?.name,
+ versionId: selection.version?.id,
+ specificationId: selection.specification?.id
+ })
+ }
+ }
+}
diff --git a/src/features/projects/domain/getSelection.ts b/src/features/projects/domain/getProjectSelectionFromPath.ts
similarity index 98%
rename from src/features/projects/domain/getSelection.ts
rename to src/features/projects/domain/getProjectSelectionFromPath.ts
index 087dc6f9..e98307eb 100644
--- a/src/features/projects/domain/getSelection.ts
+++ b/src/features/projects/domain/getProjectSelectionFromPath.ts
@@ -2,7 +2,7 @@ import Project from "./Project"
import Version from "./Version"
import OpenApiSpecification from "./OpenApiSpecification"
-export default function getSelection({
+export default function getProjectSelectionFromPath({
projects,
path
}: {
diff --git a/src/features/projects/domain/index.ts b/src/features/projects/domain/index.ts
index 6a7a706a..2b3f10cf 100644
--- a/src/features/projects/domain/index.ts
+++ b/src/features/projects/domain/index.ts
@@ -1,6 +1,6 @@
export { default as CachingProjectDataSource } from "./CachingProjectDataSource"
export { default as FilteringGitHubRepositoryDataSource } from "./FilteringGitHubRepositoryDataSource"
-export { default as getSelection } from "./getSelection"
+export { default as getProjectSelectionFromPath } from "./getProjectSelectionFromPath"
export type { default as IGitHubLoginDataSource } from "./IGitHubLoginDataSource"
export type { default as IGitHubRepositoryDataSource } from "./IGitHubRepositoryDataSource"
export * from "./IGitHubRepositoryDataSource"
@@ -16,4 +16,3 @@ export { default as ProjectNavigator } from "./ProjectNavigator"
export { default as ProjectRepository } from "./ProjectRepository"
export { default as updateWindowTitle } from "./updateWindowTitle"
export type { default as Version } from "./Version"
-export { default as useProjectNavigator } from "./useProjectNavigator"
diff --git a/src/features/projects/domain/useProjectNavigator.ts b/src/features/projects/domain/useProjectNavigator.ts
deleted file mode 100644
index ae2b5d43..00000000
--- a/src/features/projects/domain/useProjectNavigator.ts
+++ /dev/null
@@ -1,16 +0,0 @@
-"use client"
-import { useRouter } from "next/navigation"
-import ProjectNavigator from "./ProjectNavigator"
-
-export default function useProjectNavigator() {
- const router = useRouter()
- const pathnameReader = {
- get pathname() {
- if (typeof window === "undefined") {
- return ""
- }
- return window.location.pathname
- }
- }
- return new ProjectNavigator({ router, pathnameReader })
-}
diff --git a/src/features/projects/view/DocumentationIframe.tsx b/src/features/projects/view/DocumentationIframe.tsx
index 788601f4..64cfac3a 100644
--- a/src/features/projects/view/DocumentationIframe.tsx
+++ b/src/features/projects/view/DocumentationIframe.tsx
@@ -1,5 +1,5 @@
import { Box } from "@mui/material"
-import DelayedLoadingIndicator from "@/common/loading/DelayedLoadingIndicator"
+import DelayedLoadingIndicator from "@/common/ui/DelayedLoadingIndicator"
import { DocumentationVisualizer } from "@/features/settings/domain"
const DocumentationIframe = ({
diff --git a/src/features/projects/view/MainContent.tsx b/src/features/projects/view/MainContent.tsx
deleted file mode 100644
index df22698d..00000000
--- a/src/features/projects/view/MainContent.tsx
+++ /dev/null
@@ -1,34 +0,0 @@
-import { Project, Version, OpenApiSpecification } from "../domain"
-import DelayedLoadingIndicator from "@/common/loading/DelayedLoadingIndicator"
-import ErrorMessage from "./ErrorMessage"
-import Documentation from "./Documentation"
-
-const MainContent = ({
- isLoading,
- error,
- project,
- version,
- specification
-}: {
- isLoading: boolean,
- error?: Error,
- project?: Project,
- version?: Version,
- specification?: OpenApiSpecification
-}) => {
- if (project && version && specification) {
- return
- } else if (isLoading) {
- return
- } else if (error) {
- return
- } else if (!project) {
- return
- } else if (!version) {
- return
- } else {
- return
- }
-}
-
-export default MainContent
\ No newline at end of file
diff --git a/src/features/projects/view/ProjectsPage.tsx b/src/features/projects/view/ProjectsPage.tsx
deleted file mode 100644
index 298920de..00000000
--- a/src/features/projects/view/ProjectsPage.tsx
+++ /dev/null
@@ -1,18 +0,0 @@
-import { ProjectRepository } from "../domain"
-import ClientProjectsPage from "./client/ProjectsPage"
-
-export default async function ProjectsPage({
- projectRepository,
- path
-}: {
- projectRepository: ProjectRepository
- path: string
-}) {
- const projects = await projectRepository.get()
- return (
-
- )
-}
diff --git a/src/features/projects/view/ServerSideCachedProjectsProvider.tsx b/src/features/projects/view/ServerSideCachedProjectsProvider.tsx
new file mode 100644
index 00000000..810a6633
--- /dev/null
+++ b/src/features/projects/view/ServerSideCachedProjectsProvider.tsx
@@ -0,0 +1,20 @@
+"use client"
+
+import { Project } from "../domain"
+import { ServerSideCachedProjectsContext } from "@/common"
+
+const ServerSideCachedProjectsProvider = ({
+ projects,
+ children
+}: {
+ projects: Project[] | undefined
+ children: React.ReactNode
+}) => {
+ return (
+
+ {children}
+
+ )
+}
+
+export default ServerSideCachedProjectsProvider
diff --git a/src/features/projects/view/client/ProjectsPage.tsx b/src/features/projects/view/client/ProjectsPage.tsx
deleted file mode 100644
index efd27800..00000000
--- a/src/features/projects/view/client/ProjectsPage.tsx
+++ /dev/null
@@ -1,125 +0,0 @@
-"use client"
-
-import { useEffect } from "react"
-import { useTheme } from "@mui/material/styles"
-import useMediaQuery from "@mui/material/useMediaQuery"
-import SidebarContainer from "@/features/sidebar/view/client/SidebarContainer"
-import { useProjects } from "../../data"
-import ProjectList from "../ProjectList"
-import MainContent from "../MainContent"
-import MobileToolbar from "../toolbar/MobileToolbar"
-import TrailingToolbarItem from "../toolbar/TrailingToolbarItem"
-import useSidebarOpen from "@/common/state/useSidebarOpen"
-import {
- Project,
- getSelection,
- updateWindowTitle,
- useProjectNavigator
-} from "../../domain"
-
-export default function ProjectsPage({
- projects: serverProjects,
- path
-}: {
- projects?: Project[]
- path: string
-}) {
- const theme = useTheme()
- const projectNavigator = useProjectNavigator()
- const [isSidebarOpen, setSidebarOpen] = useSidebarOpen()
- const isDesktopLayout = useMediaQuery(theme.breakpoints.up("sm"))
- const { projects: clientProjects, error, isLoading: isClientLoading } = useProjects()
- const projects = isClientLoading ? (serverProjects || []) : clientProjects
- const { project, version, specification } = getSelection({ projects, path })
- const siteName = process.env.NEXT_PUBLIC_SHAPE_DOCS_TITLE || ""
- useEffect(() => {
- updateWindowTitle({
- storage: document,
- defaultTitle: siteName,
- project,
- version,
- specification
- })
- }, [project, version, specification, siteName])
- useEffect(() => {
- // Ensure the URL reflects the current selection of project, version, and specification.
- projectNavigator.navigateIfNeeded({
- projectOwner: project?.owner,
- projectName: project?.name,
- versionId: version?.id,
- specificationId: specification?.id
- })
- }, [projectNavigator, project, version, specification])
- useEffect(() => {
- // Show the sidebar if no project is selected.
- if (project === undefined) {
- setSidebarOpen(true)
- }
- }, [project, setSidebarOpen])
- const selectProject = (project: Project) => {
- if (!isDesktopLayout) {
- setSidebarOpen(false)
- }
- const version = project.versions[0]
- const specification = version.specifications[0]
- projectNavigator.navigate(project.owner, project.name, version.id, specification.id)
- }
- const selectVersion = (versionId: string) => {
- projectNavigator.navigateToVersion(project!, versionId, specification!.name)
- }
- const selectSpecification = (specificationId: string) => {
- projectNavigator.navigate(project!.owner, project!.name, version!.id, specificationId)
- }
- const canCloseSidebar = project !== undefined
- const toggleSidebar = (isOpen: boolean) => {
- if (!isOpen && canCloseSidebar) {
- setSidebarOpen(false)
- } else if (isOpen) {
- setSidebarOpen(true)
- }
- }
- return (
-
- }
- toolbarTrailingItem={project && version && specification &&
-
- }
- mobileToolbar={project && version && specification &&
-
- }
- >
- {/* If the user has not selected any project then we do not render any content */}
- {project &&
-
- }
-
- )
-}
\ No newline at end of file
diff --git a/src/features/projects/view/toolbar/MobileToolbar.tsx b/src/features/projects/view/toolbar/MobileToolbar.tsx
index 2564fb92..1fa7b6ae 100644
--- a/src/features/projects/view/toolbar/MobileToolbar.tsx
+++ b/src/features/projects/view/toolbar/MobileToolbar.tsx
@@ -1,21 +1,21 @@
+"use client"
+
import { Stack } from "@mui/material"
-import { Project, Version, OpenApiSpecification } from "../../domain"
import VersionSelector from "./VersionSelector"
import SpecificationSelector from "./SpecificationSelector"
+import { useProjectSelection } from "../../data"
-const MobileToolbar = ({
- project,
- version,
- specification,
- onSelectVersion,
- onSelectSpecification
-}: {
- project: Project
- version: Version
- specification: OpenApiSpecification
- onSelectVersion: (versionId: string) => void,
- onSelectSpecification: (specificationId: string) => void
-}) => {
+const MobileToolbar = () => {
+ const {
+ project,
+ version,
+ specification,
+ selectVersion,
+ selectSpecification
+ } = useProjectSelection()
+ if (!project || !version || !specification) {
+ return <>>
+ }
return (
diff --git a/src/features/projects/view/toolbar/SpecificationSelector.tsx b/src/features/projects/view/toolbar/SpecificationSelector.tsx
index c08aba1d..b1ba3394 100644
--- a/src/features/projects/view/toolbar/SpecificationSelector.tsx
+++ b/src/features/projects/view/toolbar/SpecificationSelector.tsx
@@ -1,5 +1,11 @@
import { SxProps } from "@mui/system"
-import { SelectChangeEvent, Select, MenuItem, FormControl } from "@mui/material"
+import {
+ SelectChangeEvent,
+ Select,
+ MenuItem,
+ FormControl,
+ Typography
+} from "@mui/material"
import MenuItemHover from "@/common/ui/MenuItemHover"
import { OpenApiSpecification } from "../../domain"
@@ -23,7 +29,13 @@ const SpecificationSelector = ({
{specifications.map(specification =>
)}
diff --git a/src/features/projects/view/toolbar/TrailingToolbarItem.tsx b/src/features/projects/view/toolbar/TrailingToolbarItem.tsx
index 537d4e20..85c9a556 100644
--- a/src/features/projects/view/toolbar/TrailingToolbarItem.tsx
+++ b/src/features/projects/view/toolbar/TrailingToolbarItem.tsx
@@ -1,24 +1,24 @@
+"use client"
+
import { SxProps } from "@mui/system"
import { Stack, IconButton, Typography, Link, Tooltip } from "@mui/material"
-import { Project, Version, OpenApiSpecification } from "../../domain"
import VersionSelector from "./VersionSelector"
import SpecificationSelector from "./SpecificationSelector"
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"
import { faPen } from "@fortawesome/free-solid-svg-icons"
+import { useProjectSelection } from "../../data"
-const TrailingToolbarItem = ({
- project,
- version,
- specification,
- onSelectVersion,
- onSelectSpecification
-}: {
- project: Project
- version: Version
- specification: OpenApiSpecification
- onSelectVersion: (versionId: string) => void,
- onSelectSpecification: (specificationId: string) => void
-}) => {
+const TrailingToolbarItem = () => {
+ const {
+ project,
+ version,
+ specification,
+ selectVersion,
+ selectSpecification
+ } = useProjectSelection()
+ if (!project || !version || !specification) {
+ return <>>
+ }
const projectNameURL = version.url || project.url
return (
<>
@@ -55,14 +55,14 @@ const TrailingToolbarItem = ({
/
{specification.editURL &&
diff --git a/src/features/projects/view/toolbar/VersionSelector.tsx b/src/features/projects/view/toolbar/VersionSelector.tsx
index d8cdc8fc..5989592e 100644
--- a/src/features/projects/view/toolbar/VersionSelector.tsx
+++ b/src/features/projects/view/toolbar/VersionSelector.tsx
@@ -1,5 +1,11 @@
import { SxProps } from "@mui/system"
-import { Select, MenuItem, SelectChangeEvent, FormControl } from "@mui/material"
+import {
+ Select,
+ MenuItem,
+ SelectChangeEvent,
+ FormControl,
+ Typography
+} from "@mui/material"
import MenuItemHover from "@/common/ui/MenuItemHover"
import { Version } from "../../domain"
@@ -26,7 +32,13 @@ const VersionSelector = ({
{versions.map(version =>
)}
diff --git a/src/features/sidebar/data/index.ts b/src/features/sidebar/data/index.ts
new file mode 100644
index 00000000..c0168b3a
--- /dev/null
+++ b/src/features/sidebar/data/index.ts
@@ -0,0 +1 @@
+export { default as useSidebarOpen } from "./useSidebarOpen"
diff --git a/src/common/state/sidebarOpen.tsx b/src/features/sidebar/data/useSidebarOpen.ts
similarity index 65%
rename from src/common/state/sidebarOpen.tsx
rename to src/features/sidebar/data/useSidebarOpen.ts
index 2d0c3b9e..3d4c1be7 100644
--- a/src/common/state/sidebarOpen.tsx
+++ b/src/features/sidebar/data/useSidebarOpen.ts
@@ -1,5 +1,5 @@
import { useSessionStorage } from "usehooks-ts"
export default function useSidebarOpen() {
- return useSessionStorage("isSidebarOpen", true)
+ return useSessionStorage("isSidebarOpen", true)
}
\ No newline at end of file
diff --git a/src/features/sidebar/view/SecondarySplitHeader.tsx b/src/features/sidebar/view/SecondarySplitHeader.tsx
new file mode 100644
index 00000000..785bce43
--- /dev/null
+++ b/src/features/sidebar/view/SecondarySplitHeader.tsx
@@ -0,0 +1,105 @@
+"use client"
+
+import { useState, useEffect, useContext } from "react"
+import { useSessionStorage } from "usehooks-ts"
+import { Box, IconButton, Stack, Tooltip, Divider, Collapse } from "@mui/material"
+import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"
+import { faBars, faChevronLeft } from "@fortawesome/free-solid-svg-icons"
+import { useTheme } from "@mui/material/styles"
+import { SidebarContext, isMac as checkIsMac } from "@/common"
+import { useSidebarOpen } from "@/features/sidebar/data"
+import ToggleMobileToolbarButton from "./internal/secondary/ToggleMobileToolbarButton"
+
+const Header = ({
+ mobileToolbar,
+ children
+}: {
+ mobileToolbar?: React.ReactNode
+ children?: React.ReactNode
+}) => {
+ const [isSidebarOpen, setSidebarOpen] = useSidebarOpen()
+ const [isMac, setIsMac] = useState(false)
+ const { isToggleable: isSidebarToggleable } = useContext(SidebarContext)
+ const [isMobileToolbarVisible, setMobileToolbarVisible] = useSessionStorage("isMobileToolbarVisible", true)
+ useEffect(() => {
+ // checkIsMac uses window so we delay the check.
+ setIsMac(checkIsMac())
+ }, [isMac, setIsMac])
+ const openCloseKeyboardShortcut = `(${isMac ? "⌘" : "^"} + .)`
+ const theme = useTheme()
+ return (
+
+
+ {isSidebarToggleable && !isSidebarOpen &&
+
+ setSidebarOpen(true)}
+ edge="start"
+ >
+
+
+
+ }
+ {isSidebarToggleable && isSidebarOpen &&
+
+ setSidebarOpen(false)}
+ edge="start"
+ >
+
+
+
+ }
+
+
+ {children}
+ {mobileToolbar &&
+ setMobileToolbarVisible(!isMobileToolbarVisible) }
+ />
+ }
+
+
+
+ {mobileToolbar &&
+
+
+ {mobileToolbar}
+
+
+ }
+
+
+ )
+}
+
+export default Header
diff --git a/src/features/sidebar/view/Sidebar.tsx b/src/features/sidebar/view/Sidebar.tsx
deleted file mode 100644
index d8873876..00000000
--- a/src/features/sidebar/view/Sidebar.tsx
+++ /dev/null
@@ -1,18 +0,0 @@
-import { ReactNode } from "react"
-import { Box } from "@mui/material"
-import SidebarHeader from "./SidebarHeader"
-import UserFooter from "@/features/user/view/UserFooter"
-
-const Sidebar = ({ children }: { children: ReactNode }) => {
- return (
- <>
-
-
- {children}
-
-
- >
- )
-}
-
-export default Sidebar
diff --git a/src/features/sidebar/view/SplitView.tsx b/src/features/sidebar/view/SplitView.tsx
new file mode 100644
index 00000000..78cadccf
--- /dev/null
+++ b/src/features/sidebar/view/SplitView.tsx
@@ -0,0 +1,52 @@
+"use client"
+
+import { useEffect } from "react"
+import { Stack } from "@mui/material"
+import { isMac, useKeyboardShortcut } from "@/common"
+import { useSidebarOpen } from "../data"
+import PrimaryContainer from "./internal/primary/Container"
+import SecondaryContainer from "./internal/secondary/Container"
+import Sidebar from "./internal/sidebar/Sidebar"
+
+const SplitView = ({
+ canToggleSidebar: _canToggleSidebar,
+ children
+}: {
+ canToggleSidebar?: boolean
+ children?: React.ReactNode
+}) => {
+ const [isSidebarOpen, setSidebarOpen] = useSidebarOpen()
+ const canToggleSidebar = _canToggleSidebar !== undefined ? _canToggleSidebar : true
+ useEffect(() => {
+ // Show the sidebar if no project is selected.
+ if (!canToggleSidebar) {
+ setSidebarOpen(true)
+ }
+ }, [canToggleSidebar, setSidebarOpen])
+ useKeyboardShortcut(event => {
+ const isActionKey = isMac() ? event.metaKey : event.ctrlKey
+ if (isActionKey && event.key === ".") {
+ event.preventDefault()
+ if (canToggleSidebar) {
+ setSidebarOpen(!isSidebarOpen)
+ }
+ }
+ }, [canToggleSidebar, setSidebarOpen])
+ const sidebarWidth = 320
+ return (
+
+ setSidebarOpen(false)}
+ >
+
+
+
+ {children}
+
+
+ )
+}
+
+export default SplitView
diff --git a/src/features/sidebar/view/TrailingToolbar.tsx b/src/features/sidebar/view/TrailingToolbar.tsx
deleted file mode 100644
index 1ab9f95c..00000000
--- a/src/features/sidebar/view/TrailingToolbar.tsx
+++ /dev/null
@@ -1,21 +0,0 @@
-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/sidebar/view/base/Drawer.tsx b/src/features/sidebar/view/base/Drawer.tsx
deleted file mode 100644
index 5a335588..00000000
--- a/src/features/sidebar/view/base/Drawer.tsx
+++ /dev/null
@@ -1,44 +0,0 @@
-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/SecondaryHeader.tsx b/src/features/sidebar/view/base/SecondaryHeader.tsx
deleted file mode 100644
index 0e4a0177..00000000
--- a/src/features/sidebar/view/base/SecondaryHeader.tsx
+++ /dev/null
@@ -1,85 +0,0 @@
-import { ReactNode } from "react"
-import { SxProps } from "@mui/system"
-import { Box, Divider, IconButton, Tooltip } from "@mui/material"
-import { useTheme } from "@mui/material/styles"
-import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"
-import { faBars, faChevronLeft } from "@fortawesome/free-solid-svg-icons"
-import { isMac, useKeyboardShortcut } from "@/common"
-
-export default function SecondaryHeader({
- showOpenSidebar,
- showCloseSidebar,
- onToggleSidebarOpen,
- trailingItem,
- children,
- sx
-}: {
- showOpenSidebar: boolean
- showCloseSidebar: boolean
- onToggleSidebarOpen: (isOpen: boolean) => void
- trailingItem?: ReactNode
- children?: ReactNode
- sx?: SxProps
-}) {
- useKeyboardShortcut(event => {
- const isActionKey = isMac() ? event.metaKey : event.ctrlKey
- if (isActionKey && event.key === ".") {
- event.preventDefault()
- if (showOpenSidebar) {
- onToggleSidebarOpen(true)
- } else if (showCloseSidebar) {
- onToggleSidebarOpen(false)
- }
- }
- }, [showOpenSidebar, showCloseSidebar, onToggleSidebarOpen])
- const openCloseShortcutString = isMac() ? " (⌘ + .)" : "(^ + .)"
- const theme = useTheme()
- return (
-
-
- {showOpenSidebar &&
-
- onToggleSidebarOpen(true)}
- edge="start"
- >
-
-
-
- }
- {showCloseSidebar &&
-
- onToggleSidebarOpen(false)}
- edge="start"
- >
-
-
-
- }
-
- {trailingItem}
-
-
- {children}
-
-
- )
-}
diff --git a/src/features/sidebar/view/base/responsive/Drawer.tsx b/src/features/sidebar/view/base/responsive/Drawer.tsx
deleted file mode 100644
index 8a027f77..00000000
--- a/src/features/sidebar/view/base/responsive/Drawer.tsx
+++ /dev/null
@@ -1,38 +0,0 @@
-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/SecondaryHeader.tsx b/src/features/sidebar/view/base/responsive/SecondaryHeader.tsx
deleted file mode 100644
index f48dd778..00000000
--- a/src/features/sidebar/view/base/responsive/SecondaryHeader.tsx
+++ /dev/null
@@ -1,70 +0,0 @@
-import { ReactNode } from "react"
-import { SxProps } from "@mui/system"
-import { Box, IconButton, Stack, Collapse } from "@mui/material"
-import SecondaryHeader from "../SecondaryHeader"
-import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"
-import { faChevronDown } from "@fortawesome/free-solid-svg-icons"
-
-export default function ResponsiveSecondaryHeader({
- showOpenSidebar,
- showCloseSidebar,
- onToggleSidebarOpen,
- showMobileToolbar,
- onToggleMobileToolbar,
- trailingItem,
- mobileToolbar,
- sx
-}: {
- showOpenSidebar: boolean
- showCloseSidebar: boolean
- onToggleSidebarOpen: (isOpen: boolean) => void
- showMobileToolbar: boolean
- onToggleMobileToolbar: (showMobileToolbar: boolean) => void
- trailingItem?: ReactNode
- mobileToolbar?: ReactNode
- sx?: SxProps
-}) {
- return (
-
- {trailingItem}
- {mobileToolbar &&
- onToggleMobileToolbar(!showMobileToolbar) }
- sx={{ display: { sm: "flex", md: "none" } }}
- >
-
-
- }
-
- }
- >
- {mobileToolbar &&
-
-
- {mobileToolbar}
-
-
- }
-
- )
-}
diff --git a/src/features/sidebar/view/base/responsive/SecondaryWrapper.tsx b/src/features/sidebar/view/base/responsive/SecondaryWrapper.tsx
deleted file mode 100644
index 171bf342..00000000
--- a/src/features/sidebar/view/base/responsive/SecondaryWrapper.tsx
+++ /dev/null
@@ -1,32 +0,0 @@
-import { ReactNode } from "react"
-import SecondaryWrapper from "../SecondaryWrapper"
-
-export default function ResponsiveSecondaryWrapper({
- sidebarWidth,
- offsetContent,
- children
-}: {
- sidebarWidth: 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
deleted file mode 100644
index 248660ab..00000000
--- a/src/features/sidebar/view/base/responsive/SidebarContainer.tsx
+++ /dev/null
@@ -1,39 +0,0 @@
-import { ReactNode } from "react"
-import { Stack } from "@mui/material"
-import Drawer from "./Drawer"
-import SecondaryWrapper from "./SecondaryWrapper"
-
-const SidebarContainer = ({
- isSidebarOpen,
- onToggleSidebarOpen,
- sidebar,
- header,
- children
-}: {
- isSidebarOpen: boolean,
- onToggleSidebarOpen: (isSidebarOpen: boolean) => void
- sidebar: ReactNode
- header?: ReactNode
- children?: ReactNode
-}) => {
- const sidebarWidth = 320
- return (
-
- onToggleSidebarOpen(false)}
- >
- {sidebar}
-
-
- {header}
-
- {children}
-
-
-
- )
-}
-
-export default SidebarContainer
diff --git a/src/features/sidebar/view/client/SidebarContainer.tsx b/src/features/sidebar/view/client/SidebarContainer.tsx
deleted file mode 100644
index ec7b6fc2..00000000
--- a/src/features/sidebar/view/client/SidebarContainer.tsx
+++ /dev/null
@@ -1,58 +0,0 @@
-"use client"
-
-import dynamic from "next/dynamic"
-import { ReactNode } from "react"
-import { useSessionStorage } from "usehooks-ts"
-import ResponsiveSidebarContainer from "../base/responsive/SidebarContainer"
-import ResponsiveSecondaryHeader from "../base/responsive/SecondaryHeader"
-import Sidebar from "../Sidebar"
-
-const SidebarContainer = ({
- isSidebarOpen,
- onToggleSidebarOpen,
- showHeader: _showHeader,
- sidebar,
- children,
- toolbarTrailingItem,
- mobileToolbar
-}: {
- isSidebarOpen: boolean,
- onToggleSidebarOpen: (isSidebarOpen: boolean) => void,
- showHeader?: boolean,
- sidebar?: ReactNode
- children?: ReactNode
- toolbarTrailingItem?: ReactNode
- mobileToolbar?: ReactNode
-}) => {
- const [showMobileToolbar, setShowMobileToolbar] = useSessionStorage("isMobileToolbarVisible", true)
- const showHeader = _showHeader || _showHeader === undefined
- return (
-
- {sidebar}
-
- }
- header={showHeader &&
-
- }
- >
- {children}
-
- )
-}
-
-// Disable server-side rendering as this component uses the window instance to manage its state.
-export default dynamic(() => Promise.resolve(SidebarContainer), {
- ssr: false
-})
diff --git a/src/features/sidebar/view/index.ts b/src/features/sidebar/view/index.ts
new file mode 100644
index 00000000..47482d01
--- /dev/null
+++ b/src/features/sidebar/view/index.ts
@@ -0,0 +1 @@
+export { default as SplitView } from "./SplitView"
diff --git a/src/features/sidebar/view/internal/primary/Container.tsx b/src/features/sidebar/view/internal/primary/Container.tsx
new file mode 100644
index 00000000..b6714e1e
--- /dev/null
+++ b/src/features/sidebar/view/internal/primary/Container.tsx
@@ -0,0 +1,81 @@
+import { SxProps } from "@mui/system"
+import { Drawer as MuiDrawer } from "@mui/material"
+
+const PrimaryContainer = ({
+ width,
+ isOpen,
+ onClose,
+ children
+}: {
+ width: number
+ isOpen: boolean
+ onClose?: () => void
+ children?: React.ReactNode
+}) => {
+ return (
+ <>
+ <_PrimaryContainer
+ variant="temporary"
+ width={width}
+ isOpen={isOpen}
+ onClose={onClose}
+ keepMounted={true}
+ sx={{ display: { xs: "block", sm: "none" } }}
+ >
+ {children}
+
+ <_PrimaryContainer
+ variant="persistent"
+ width={width}
+ isOpen={isOpen}
+ keepMounted={false}
+ sx={{ display: { xs: "none", sm: "block" } }}
+ >
+ {children}
+
+ >
+ )
+}
+
+export default PrimaryContainer
+
+const _PrimaryContainer = ({
+ variant,
+ width,
+ isOpen,
+ onClose,
+ keepMounted,
+ sx,
+ children
+}: {
+ variant: "persistent" | "temporary",
+ width: number
+ isOpen: boolean
+ onClose?: () => void
+ keepMounted?: boolean
+ sx: SxProps,
+ children?: React.ReactNode
+}) => {
+ return (
+
+ {children}
+
+ )
+}
\ No newline at end of file
diff --git a/src/features/sidebar/view/base/SecondaryWrapper.tsx b/src/features/sidebar/view/internal/secondary/Container.tsx
similarity index 54%
rename from src/features/sidebar/view/base/SecondaryWrapper.tsx
rename to src/features/sidebar/view/internal/secondary/Container.tsx
index fef93718..7de24922 100644
--- a/src/features/sidebar/view/base/SecondaryWrapper.tsx
+++ b/src/features/sidebar/view/internal/secondary/Container.tsx
@@ -1,15 +1,46 @@
-import { ReactNode } from "react"
import { SxProps } from "@mui/system"
import { Stack } from "@mui/material"
import { styled } from "@mui/material/styles"
-interface WrapperStackProps {
+const SecondaryContainer = ({
+ sidebarWidth,
+ offsetContent,
+ children
+}: {
sidebarWidth: number
- isSidebarOpen: boolean
+ offsetContent: boolean
+ children?: React.ReactNode
+}) => {
+ const sx = { overflow: "hidden" }
+ return (
+ <>
+ <_SecondaryContainer
+ sidebarWidth={0}
+ isSidebarOpen={false}
+ sx={{ ...sx, display: { xs: "flex", sm: "none" } }}
+ >
+ {children}
+
+ <_SecondaryContainer
+ sidebarWidth={sidebarWidth}
+ isSidebarOpen={offsetContent}
+ sx={{ ...sx, display: { xs: "none", sm: "flex" } }}
+ >
+ {children}
+
+ >
+ )
+}
+
+export default SecondaryContainer
+
+interface WrapperStackProps {
+ readonly sidebarWidth: number
+ readonly isSidebarOpen: boolean
}
const WrapperStack = styled(Stack, {
- shouldForwardProp: (prop) => prop !== "isSidebarOpen"
+ shouldForwardProp: (prop) => prop !== "isSidebarOpen" && prop !== "sidebarWidth"
})(({ theme, sidebarWidth, isSidebarOpen }) => ({
transition: theme.transitions.create("margin", {
easing: theme.transitions.easing.sharp,
@@ -25,7 +56,7 @@ const WrapperStack = styled(Stack, {
})
}))
-export default function SecondaryWrapper({
+const _SecondaryContainer = ({
sidebarWidth,
isSidebarOpen,
children,
@@ -33,9 +64,9 @@ export default function SecondaryWrapper({
}: {
sidebarWidth: number
isSidebarOpen: boolean
- children: ReactNode
+ children: React.ReactNode
sx?: SxProps
-}) {
+}) => {
return (
void
+}) => {
+ return <>
+
+
+
+ >
+}
+
+export default ToggleMobileToolbarButton
diff --git a/src/features/sidebar/view/SidebarHeader.tsx b/src/features/sidebar/view/internal/sidebar/Header.tsx
similarity index 66%
rename from src/features/sidebar/view/SidebarHeader.tsx
rename to src/features/sidebar/view/internal/sidebar/Header.tsx
index 916fdb5e..bcad2f45 100644
--- a/src/features/sidebar/view/SidebarHeader.tsx
+++ b/src/features/sidebar/view/internal/sidebar/Header.tsx
@@ -1,11 +1,10 @@
import Image from "next/image"
-// import { Box, Typography, IconButton, Tooltip } from "@mui/material"
-import { Box, Typography } from "@mui/material"
import Link from "next/link"
-// import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"
-// import { faPlus } from "@fortawesome/free-solid-svg-icons"
+import { Box, Typography, IconButton, Tooltip } from "@mui/material"
+import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"
+import { faPlus } from "@fortawesome/free-solid-svg-icons"
-export default function SidebarHeader() {
+const Header = () => {
const siteName = process.env.NEXT_PUBLIC_SHAPE_DOCS_TITLE
return (
- {/*
-
-
+
+
+
- */}
+
)
}
+
+export default Header
diff --git a/src/features/sidebar/view/internal/sidebar/Sidebar.tsx b/src/features/sidebar/view/internal/sidebar/Sidebar.tsx
new file mode 100644
index 00000000..fb08521b
--- /dev/null
+++ b/src/features/sidebar/view/internal/sidebar/Sidebar.tsx
@@ -0,0 +1,19 @@
+import { Box } from "@mui/material"
+import Header from "./Header"
+import UserButton from "./user/UserButton"
+import SettingsList from "./settings/SettingsList"
+import ProjectList from "./projects/ProjectList"
+
+const Sidebar = () => {
+ return <>
+
+
+
+
+
+
+
+ >
+}
+
+export default Sidebar
diff --git a/src/features/projects/view/ProjectAvatar.tsx b/src/features/sidebar/view/internal/sidebar/projects/ProjectAvatar.tsx
similarity index 83%
rename from src/features/projects/view/ProjectAvatar.tsx
rename to src/features/sidebar/view/internal/sidebar/projects/ProjectAvatar.tsx
index 2a6056d6..c55f7aac 100644
--- a/src/features/projects/view/ProjectAvatar.tsx
+++ b/src/features/sidebar/view/internal/sidebar/projects/ProjectAvatar.tsx
@@ -1,8 +1,8 @@
import { SxProps } from "@mui/system"
import { Avatar, Box } from "@mui/material"
import { alpha, useTheme } from "@mui/material/styles"
-import { Project } from "../domain"
-import ProjectAvatarSquircleClip from "./ProjectAvatarSquircleClip"
+import { Project } from "@/features/projects/domain"
+import ProjectAvatarSquircle from "./ProjectAvatarSquircle"
function ProjectAvatar({
project,
@@ -21,7 +21,7 @@ function ProjectAvatar({
height: height + borderRadius * 2,
position: "relative"
}}>
-
-
+
}
-
+
)
}
@@ -58,3 +58,4 @@ const PlaceholderAvatar = ({ text, sx }: { text: string, sx?: SxProps }) => {
)
}
+
diff --git a/src/features/projects/view/ProjectAvatarSquircleClip.tsx b/src/features/sidebar/view/internal/sidebar/projects/ProjectAvatarSquircle.tsx
similarity index 76%
rename from src/features/projects/view/ProjectAvatarSquircleClip.tsx
rename to src/features/sidebar/view/internal/sidebar/projects/ProjectAvatarSquircle.tsx
index 0ac099c5..341a058c 100644
--- a/src/features/projects/view/ProjectAvatarSquircleClip.tsx
+++ b/src/features/sidebar/view/internal/sidebar/projects/ProjectAvatarSquircle.tsx
@@ -1,9 +1,8 @@
-import { ReactNode } from "react"
import { SxProps } from "@mui/system"
import { Box } from "@mui/material"
import { getSvgPath } from "figma-squircle"
-const ProjectAvatarSquircleClip = ({
+const ProjectAvatarSquircle = ({
width,
height,
children,
@@ -11,7 +10,7 @@ const ProjectAvatarSquircleClip = ({
}: {
width: number,
height: number,
- children?: ReactNode,
+ children?: React.ReactNode,
sx?: SxProps
}) => {
const svgPath = getSvgPath({
@@ -27,4 +26,4 @@ const ProjectAvatarSquircleClip = ({
)
}
-export default ProjectAvatarSquircleClip
\ No newline at end of file
+export default ProjectAvatarSquircle
diff --git a/src/features/projects/view/ProjectList.tsx b/src/features/sidebar/view/internal/sidebar/projects/ProjectList.tsx
similarity index 68%
rename from src/features/projects/view/ProjectList.tsx
rename to src/features/sidebar/view/internal/sidebar/projects/ProjectList.tsx
index 30fb06ad..3fb6e231 100644
--- a/src/features/projects/view/ProjectList.tsx
+++ b/src/features/sidebar/view/internal/sidebar/projects/ProjectList.tsx
@@ -1,23 +1,13 @@
+import { useContext } from "react"
import { List, Box, Typography } from "@mui/material"
+import { ProjectsContainerContext } from "@/common"
+import { useProjectSelection } from "@/features/projects/data"
import ProjectListItem from "./ProjectListItem"
import ProjectListItemPlaceholder from "./ProjectListItemPlaceholder"
-import { Project } from "../domain"
-interface ProjectListProps {
- readonly isLoading: boolean
- readonly projects: Project[]
- readonly selectedProjectId?: string
- readonly onSelectProject: (project: Project) => void
-}
-
-const ProjectList = (
- {
- isLoading,
- projects,
- selectedProjectId,
- onSelectProject
- }: ProjectListProps
-) => {
+const ProjectList = () => {
+ const { projects, isLoading } = useContext(ProjectsContainerContext)
+ const projectSelection = useProjectSelection()
const loadingItemCount = 6
if (isLoading || projects.length > 0) {
return (
@@ -31,8 +21,8 @@ const ProjectList = (
))}
diff --git a/src/features/projects/view/ProjectListItem.tsx b/src/features/sidebar/view/internal/sidebar/projects/ProjectListItem.tsx
similarity index 84%
rename from src/features/projects/view/ProjectListItem.tsx
rename to src/features/sidebar/view/internal/sidebar/projects/ProjectListItem.tsx
index c9472256..4c05ee6b 100644
--- a/src/features/projects/view/ProjectListItem.tsx
+++ b/src/features/sidebar/view/internal/sidebar/projects/ProjectListItem.tsx
@@ -6,7 +6,7 @@ import {
Typography
} from "@mui/material"
import MenuItemHover from "@/common/ui/MenuItemHover"
-import { Project } from "../domain"
+import { Project } from "@/features/projects/domain"
import ProjectAvatar from "./ProjectAvatar"
const ProjectListItem = ({
@@ -35,7 +35,10 @@ const ProjectListItem = ({
style={{
fontSize: "1.1em",
fontWeight: isSelected ? 800 : 500,
- letterSpacing: 0.3
+ letterSpacing: 0.3,
+ whiteSpace: "nowrap",
+ overflow: "hidden",
+ textOverflow: "ellipsis"
}}
>
{project.displayName}
diff --git a/src/features/projects/view/ProjectListItemPlaceholder.tsx b/src/features/sidebar/view/internal/sidebar/projects/ProjectListItemPlaceholder.tsx
similarity index 78%
rename from src/features/projects/view/ProjectListItemPlaceholder.tsx
rename to src/features/sidebar/view/internal/sidebar/projects/ProjectListItemPlaceholder.tsx
index d5fcde1f..77de983a 100644
--- a/src/features/projects/view/ProjectListItemPlaceholder.tsx
+++ b/src/features/sidebar/view/internal/sidebar/projects/ProjectListItemPlaceholder.tsx
@@ -1,20 +1,20 @@
import { ListItem, ListItemText, Stack, Skeleton } from "@mui/material"
import MenuItemHover from "@/common/ui/MenuItemHover"
-import ProjectAvatarSquircleClip from "./ProjectAvatarSquircleClip"
+import ProjectAvatarSquircle from "./ProjectAvatarSquircle"
const ProjectListItemPlaceholder = () => {
return (
- {
animation="wave"
sx={{ width: 42, height: 42 }}
/>
-
+
void
icon?: IconProp
- children?: ReactNode
+ children?: React.ReactNode
}) => {
return (