Skip to content
Merged
Show file tree
Hide file tree
Changes from 13 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 21 additions & 2 deletions docker-compose.yaml
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
version: '3.8'
services:
cache:
image: redis:6.2-alpine
Expand All @@ -8,6 +7,26 @@ services:
command: redis-server --save 20 1 --loglevel warning
volumes:
- cache:/data

postgres:
image: postgres:16.8
restart: always
ports:
- '5432:5432'
environment:
- POSTGRES_USER=oscar
- POSTGRES_PASSWORD=passw0rd
- POSTGRES_DB=shape-docs

app:
build: .
ports:
- '3000:3000'
env_file:
- .env
depends_on:
- cache

volumes:
cache:
driver: local
driver: local
2 changes: 1 addition & 1 deletion package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

25 changes: 7 additions & 18 deletions src/app/(authed)/(project-doc)/[...slug]/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,32 +4,21 @@ import { Box, Stack } from "@mui/material"
import { useTheme } from "@mui/material/styles"
import TrailingToolbarItem from "@/features/projects/view/toolbar/TrailingToolbarItem"
import MobileToolbar from "@/features/projects/view/toolbar/MobileToolbar"
import SecondaryHeaderPlaceholder from "@/features/sidebar/view/SecondarySplitHeaderPlaceholder"
import { useContext } from "react"
import { ProjectsContext } from "@/common"
import LoadingIndicator from "@/common/ui/LoadingIndicator"
import SecondarySplitHeader from "@/features/sidebar/view/SecondarySplitHeader"

export default function Page({ children }: { children: React.ReactNode }) {
const { refreshed } = useContext(ProjectsContext)

const theme = useTheme()

return (
<Stack sx={{ height: "100%" }}>
<>
{!refreshed ? <SecondaryHeaderPlaceholder/> :
<SecondarySplitHeader mobileToolbar={<MobileToolbar/>}>
<TrailingToolbarItem/>
</SecondarySplitHeader>
}
<Box sx={{ height: "0.5px", background: theme.palette.divider }} />
{refreshed ?
<main style={{ flexGrow: "1", overflowY: "auto" }}>
{children}
</main> :
<LoadingIndicator />
}
<SecondarySplitHeader mobileToolbar={<MobileToolbar/>}>
<TrailingToolbarItem/>
</SecondarySplitHeader>
<Box sx={{ height: "0.5px", background: theme.palette.divider }} />
<main style={{ flexGrow: "1", overflowY: "auto" }}>
{children}
</main>
</>
</Stack>
)
Expand Down
12 changes: 9 additions & 3 deletions src/app/(authed)/(project-doc)/[...slug]/page.tsx
Original file line number Diff line number Diff line change
@@ -1,14 +1,17 @@
"use client"

import { useEffect } from "react"
import { useContext, useEffect } from "react"
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"
import NotFound from "@/features/projects/view/NotFound"
import { ProjectsContext } from "@/common/context/ProjectsContext"
import LoadingIndicator from "@/common/ui/LoadingIndicator"

export default function Page() {
const { project, version, specification, navigateToSelectionIfNeeded } = useProjectSelection()
const { refreshing } = useContext(ProjectsContext)
// Ensure the URL reflects the current selection of project, version, and specification.
useEffect(() => {
navigateToSelectionIfNeeded()
Expand All @@ -30,10 +33,13 @@ export default function Page() {
{project && version && specification &&
<Documentation url={specification.url} />
}
{project && (!version || !specification) &&
{project && (!version || !specification) && !refreshing &&
<ErrorMessage text={`The selected ${!version ? "branch or tag" : "specification"} was not found.`}/>
}
{!project && <NotFound/>}
{refreshing && // project data is currently being fetched - show loading indicator
<LoadingIndicator />
}
{!project && !refreshing && <NotFound/>}
</>
)
}
38 changes: 23 additions & 15 deletions src/app/(authed)/layout.tsx
Original file line number Diff line number Diff line change
@@ -1,30 +1,38 @@
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 ProjectsContextProvider from "@/features/projects/view/ProjectsContextProvider"
import { SidebarTogglableContextProvider, SplitView } from "@/features/sidebar/view"
import { redirect } from "next/navigation";
import { SessionProvider } from "next-auth/react";
import { session, projectDataSource } from "@/composition";
import ErrorHandler from "@/common/ui/ErrorHandler";
import SessionBarrier from "@/features/auth/view/SessionBarrier";
import ProjectsContextProvider from "@/features/projects/view/ProjectsContextProvider";
import {
SidebarTogglableContextProvider,
SplitView,
} from "@/features/sidebar/view";

export default async function Layout({ children }: { children: React.ReactNode }) {
const isAuthenticated = await session.getIsAuthenticated()
export default async function Layout({
children,
}: {
children: React.ReactNode;
}) {
const isAuthenticated = await session.getIsAuthenticated();
if (!isAuthenticated) {
return redirect("/api/auth/signin")
return redirect("/api/auth/signin");
}
const projects = await projectRepository.get()

const projects = await projectDataSource.getProjects();


return (
<ErrorHandler>
<SessionProvider>
<SessionBarrier>
<ProjectsContextProvider initialProjects={projects}>
<SidebarTogglableContextProvider>
<SplitView>
{children}
</SplitView>
<SplitView>{children}</SplitView>
</SidebarTogglableContextProvider>
</ProjectsContextProvider>
</SessionBarrier>
</SessionProvider>
</ErrorHandler>
)
);
}
2 changes: 1 addition & 1 deletion src/app/api/blob/[owner]/[repository]/[...path]/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,4 +33,4 @@ export async function GET(req: NextRequest, { params }: { params: Promise<GetBlo
headers.set("Content-Type", "text/plain");
}
return new NextResponse(file, { status: 200, headers })
}
}
2 changes: 1 addition & 1 deletion src/app/api/hooks/github/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,4 @@ import { gitHubHookHandler } from "@/composition"
export const POST = async (req: NextRequest): Promise<NextResponse> => {
await gitHubHookHandler.handle(req)
return NextResponse.json({ status: "OK" })
}
}
9 changes: 9 additions & 0 deletions src/app/api/refresh-projects/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@

import { NextResponse } from "next/server"
import { projectDataSource } from "@/composition";


export async function POST() {
const projects = await projectDataSource.refreshProjects()
return NextResponse.json({ projects })
}
4 changes: 2 additions & 2 deletions src/common/context/ProjectsContext.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,13 @@ import { Project } from "@/features/projects/domain"
export const SidebarTogglableContext = createContext<boolean>(true)

type ProjectsContextValue = {
refreshed: boolean,
refreshing: boolean,
projects: Project[],
setProjects: (projects: Project[]) => void
}

export const ProjectsContext = createContext<ProjectsContextValue>({
refreshed: false,
refreshing: false,
projects: [],
setProjects: () => {}
})
6 changes: 3 additions & 3 deletions src/common/utils/fetcher.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
export class FetcherError extends Error {
export class FetcherError extends Error {
readonly status: number

constructor(status: number, message: string) {
Expand All @@ -12,9 +12,9 @@
input: RequestInfo,
init?: RequestInit
): Promise<JSON> {
const res = await fetch(input, init)
const res = await fetch(input, init)
if (!res.ok) {
throw new FetcherError(res.status, "An error occurred while fetching the data.")
}
return res.json()
}
}
2 changes: 1 addition & 1 deletion src/composition.ts
Original file line number Diff line number Diff line change
Expand Up @@ -230,4 +230,4 @@ export const gitHubHookHandler = new GitHubHookHandler({
})
})
})
})
})
2 changes: 1 addition & 1 deletion src/features/docs/view/Stoplight.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -75,4 +75,4 @@ const ElementsAPI = ({
})
}

export default Stoplight
export default Stoplight
2 changes: 1 addition & 1 deletion src/features/docs/view/Swagger.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -62,4 +62,4 @@ const Swagger = ({ url }: { url: string }) => {
)
}

export default Swagger
export default Swagger
42 changes: 28 additions & 14 deletions src/features/projects/domain/CachingProjectDataSource.ts
Original file line number Diff line number Diff line change
@@ -1,19 +1,33 @@
import Project from "./Project"
import IProjectDataSource from "./IProjectDataSource"
import IProjectRepository from "./IProjectRepository"
import Project from "./Project";
import IProjectDataSource from "./IProjectDataSource";
import IProjectRepository from "./IProjectRepository";


export default class CachingProjectDataSource implements IProjectDataSource {
private dataSource: IProjectDataSource
private repository: IProjectRepository

constructor(config: { dataSource: IProjectDataSource, repository: IProjectRepository }) {
this.dataSource = config.dataSource
this.repository = config.repository
private dataSource: IProjectDataSource;
private repository: IProjectRepository;

constructor(config: {
dataSource: IProjectDataSource;
repository: IProjectRepository;
}) {
this.dataSource = config.dataSource;
this.repository = config.repository;
}

async getProjects(): Promise<Project[]> {
const projects = await this.dataSource.getProjects()
await this.repository.set(projects)
return projects
const cache = await this.repository.get();
if (cache && cache.length > 0) {
return cache;
}
const projects = await this.dataSource.getProjects();
await this.repository.set(projects);
return projects;
}

async refreshProjects(): Promise<Project[]> {
const projects = await this.dataSource.getProjects();
await this.repository.set(projects);
return projects;
}
}
}
6 changes: 3 additions & 3 deletions src/features/projects/domain/ProjectRepository.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,16 +15,16 @@ export default class ProjectRepository implements IProjectRepository {
this.repository = config.repository
}

async get(): Promise<Project[] | undefined> {
async get(): Promise<Project[] | undefined> {
const userId = await this.userIDReader.getUserId()
const string = await this.repository.get(userId)

if (!string) {
return undefined
}
try {
return ZodJSONCoder.decode(ProjectSchema.array(), string)
} catch (err) {
console.error(err)
} catch { // swallow decode errors and treat as missing cache
return undefined
}
}
Expand Down
Loading