From 8b80613b6ad21e1000c025eda79fdde3642d0beb Mon Sep 17 00:00:00 2001 From: polubis Date: Fri, 25 Jul 2025 08:23:26 +0200 Subject: [PATCH 01/19] wip --- src/api-4markdown-contracts/atoms.ts | 16 +++++++ .../contracts/index.ts | 18 ++++++- src/api-4markdown-contracts/dtos/index.ts | 1 + .../dtos/resource-completion.dto.ts | 27 +++++++++++ src/components/user-popover-content.tsx | 8 ++++ src/core/app-events.ts | 11 +++-- src/core/use-auth.ts | 3 ++ src/development-kit/create-selectors.ts | 19 ++++++++ .../acts/load-resource-completions.act.ts | 48 +++++++++++++++++++ src/modules/resource-completions/index.ts | 2 + .../resource-completions/store/index.ts | 14 ++++++ .../resource-completions/store/models.ts | 10 ++++ 12 files changed, 172 insertions(+), 5 deletions(-) create mode 100644 src/api-4markdown-contracts/dtos/resource-completion.dto.ts create mode 100644 src/development-kit/create-selectors.ts create mode 100644 src/modules/resource-completions/acts/load-resource-completions.act.ts create mode 100644 src/modules/resource-completions/index.ts create mode 100644 src/modules/resource-completions/store/index.ts create mode 100644 src/modules/resource-completions/store/models.ts diff --git a/src/api-4markdown-contracts/atoms.ts b/src/api-4markdown-contracts/atoms.ts index 6d3ddc3eb..a062d403d 100644 --- a/src/api-4markdown-contracts/atoms.ts +++ b/src/api-4markdown-contracts/atoms.ts @@ -14,6 +14,17 @@ type Url = string; type UserProfileId = Brand; type CommentId = Brand; +type DocumentId = Brand; +type MindmapNodeId = Brand; +type MindmapId = Brand; + +type ResourceId = DocumentId | MindmapNodeId | MindmapId; + +const RESOURCE_TYPES = ["document", "mindmap", "mindmap-node"] as const; + +type ResourceType = (typeof RESOURCE_TYPES)[number]; + +export { RESOURCE_TYPES }; export type { Id, Name, @@ -28,4 +39,9 @@ export type { Slug, UserProfileId, CommentId, + ResourceId, + DocumentId, + MindmapId, + MindmapNodeId, + ResourceType, }; diff --git a/src/api-4markdown-contracts/contracts/index.ts b/src/api-4markdown-contracts/contracts/index.ts index 945371742..9152b93b5 100644 --- a/src/api-4markdown-contracts/contracts/index.ts +++ b/src/api-4markdown-contracts/contracts/index.ts @@ -1,5 +1,12 @@ import { Brand, type Prettify } from "development-kit/utility-types"; -import type { Base64, Date, Id, Url, UserProfileId } from "../atoms"; +import type { + Base64, + Date, + Id, + ResourceId, + Url, + UserProfileId, +} from "../atoms"; import type { DocumentDto, PermanentDocumentDto, @@ -14,6 +21,7 @@ import type { RewriteAssistantPersona, YourAccountDto, CommentDto, + ResourceCompletionDto, } from "../dtos"; // @TODO[PRIO=1]: [Add better error handling and throwing custom errors]. @@ -234,6 +242,11 @@ type AddUserProfileCommentContract = Contract< } >; +type GetUserResourceCompletionsContract = Contract< + `getUserResourceCompletions`, + Record +>; + type API4MarkdownContracts = | CreateMindmapContract | GetYourDocumentsContract @@ -261,7 +274,8 @@ type API4MarkdownContracts = | CreateContentWithAIContract | GetYourAccountContract | GetUserProfileContract - | AddUserProfileCommentContract; + | AddUserProfileCommentContract + | GetUserResourceCompletionsContract; type API4MarkdownContractKey = API4MarkdownContracts["key"]; type API4MarkdownDto = Extract< diff --git a/src/api-4markdown-contracts/dtos/index.ts b/src/api-4markdown-contracts/dtos/index.ts index 7624cdad9..227523f66 100644 --- a/src/api-4markdown-contracts/dtos/index.ts +++ b/src/api-4markdown-contracts/dtos/index.ts @@ -7,3 +7,4 @@ export * from "./full-mindmap.dto"; export * from "./rewrite-assistant.dto"; export * from "./your-account.dto"; export * from "./comment.dto"; +export * from "./resource-completion.dto"; diff --git a/src/api-4markdown-contracts/dtos/resource-completion.dto.ts b/src/api-4markdown-contracts/dtos/resource-completion.dto.ts new file mode 100644 index 000000000..8732c923c --- /dev/null +++ b/src/api-4markdown-contracts/dtos/resource-completion.dto.ts @@ -0,0 +1,27 @@ +import type { + Date, + DocumentId, + MindmapId, + MindmapNodeId, + ResourceType, +} from "../atoms"; + +type ResourceCompletionDto = + | { + cdate: Date; + type: Extract; + resourceId: DocumentId; + } + | { + cdate: Date; + type: Extract; + resourceId: MindmapId; + } + | { + cdate: Date; + type: Extract; + resourceId: MindmapNodeId; + parentId: MindmapId; + }; + +export type { ResourceCompletionDto }; diff --git a/src/components/user-popover-content.tsx b/src/components/user-popover-content.tsx index 08c9c3f69..2d3f28fee 100644 --- a/src/components/user-popover-content.tsx +++ b/src/components/user-popover-content.tsx @@ -19,12 +19,20 @@ import { useYourAccountState } from "store/your-account"; import { reloadYourAccountAct } from "acts/reload-your-account.act"; import { navigate } from "gatsby"; import { meta } from "../../meta"; +import { useAppEvent } from "core/app-events"; +import { loadCompletionAct } from "modules/resource-completions"; const UserPopoverContent = ({ onClose }: { onClose(): void }) => { const yourUserProfile = useYourUserProfileState(); const userProfileForm = useSimpleFeature(); const yourAccount = useYourAccountState(); + useAppEvent((event) => { + if (event.type === "USER_AUTHENTICATED") { + loadCompletionAct(); + } + }); + const signOutConfirmation = useConfirm(() => { logOut(); onClose(); diff --git a/src/core/app-events.ts b/src/core/app-events.ts index 7b481e7d0..dd9b1ff7b 100644 --- a/src/core/app-events.ts +++ b/src/core/app-events.ts @@ -1,9 +1,13 @@ import React, { useRef } from "react"; import { Subject, Subscription } from "rxjs"; -type AppEvent = { - type: "SHOW_USER_PROFILE_FORM"; -}; +type AppEvent = + | { + type: "SHOW_USER_PROFILE_FORM"; + } + | { + type: "USER_AUTHENTICATED"; + }; const appEventsBus = new Subject(); const appEventsBus$ = appEventsBus.asObservable(); @@ -31,4 +35,5 @@ const useAppEvent = (callback: (event: AppEvent) => void): void => { }, []); }; +export type { AppEvent }; export { emit, subscribe, useAppEvent }; diff --git a/src/core/use-auth.ts b/src/core/use-auth.ts index 905d10446..60833164f 100644 --- a/src/core/use-auth.ts +++ b/src/core/use-auth.ts @@ -9,6 +9,7 @@ import { useYourAccountState } from "store/your-account"; import { initializeAPI } from "api-4markdown"; import { graphql, useStaticQuery } from "gatsby"; import { SiteMetadata } from "./models"; +import { emit } from "./app-events"; type SiteMetadataQuery = { site: { @@ -34,6 +35,8 @@ const useAuth = () => { React.useEffect(() => { const unsubscribe = api.onAuthChange((user) => { if (user) { + emit({ type: "USER_AUTHENTICATED" }); + authStoreActions.authorize({ avatar: user.photoURL, name: user.displayName, diff --git a/src/development-kit/create-selectors.ts b/src/development-kit/create-selectors.ts new file mode 100644 index 000000000..a7e2265f2 --- /dev/null +++ b/src/development-kit/create-selectors.ts @@ -0,0 +1,19 @@ +import { StoreApi, UseBoundStore } from "zustand"; + +type WithSelectors = S extends { getState: () => infer T } + ? S & { use: { [K in keyof T]: () => T[K] } } + : never; + +const createSelectors = >>( + _store: S, +) => { + const store = _store as WithSelectors; + store.use = {}; + for (const k of Object.keys(store.getState())) { + (store.use as any)[k] = () => store((s) => s[k as keyof typeof s]); + } + + return store; +}; + +export { createSelectors }; diff --git a/src/modules/resource-completions/acts/load-resource-completions.act.ts b/src/modules/resource-completions/acts/load-resource-completions.act.ts new file mode 100644 index 000000000..1a5e88d15 --- /dev/null +++ b/src/modules/resource-completions/acts/load-resource-completions.act.ts @@ -0,0 +1,48 @@ +import { getAPI, getCache, parseError, setCache } from "api-4markdown"; +import { useResourcesCompletionState } from "../store"; +import { API4MarkdownContractKey } from "api-4markdown-contracts"; + +const loadCompletionAct = async (): Promise => { + try { + const key: API4MarkdownContractKey = `getUserResourceCompletions`; + + const cachedCompletions = getCache(key); + + if (cachedCompletions !== null) { + useResourcesCompletionState.setState({ + idle: false, + busy: false, + error: null, + completions: cachedCompletions, + }); + return; + } + + useResourcesCompletionState.setState({ + idle: false, + busy: true, + error: null, + completions: {}, + }); + + const completions = await getAPI().call(key)(); + + useResourcesCompletionState.setState({ + idle: false, + busy: false, + error: null, + completions, + }); + + setCache(key, completions); + } catch (error) { + useResourcesCompletionState.setState({ + idle: false, + busy: false, + error: parseError(error), + completions: {}, + }); + } +}; + +export { loadCompletionAct }; diff --git a/src/modules/resource-completions/index.ts b/src/modules/resource-completions/index.ts new file mode 100644 index 000000000..2133e7fc6 --- /dev/null +++ b/src/modules/resource-completions/index.ts @@ -0,0 +1,2 @@ +export { useResourcesCompletionState } from "./store"; +export { loadCompletionAct } from "./acts/load-resource-completions.act"; diff --git a/src/modules/resource-completions/store/index.ts b/src/modules/resource-completions/store/index.ts new file mode 100644 index 000000000..34bb52c87 --- /dev/null +++ b/src/modules/resource-completions/store/index.ts @@ -0,0 +1,14 @@ +import { createSelectors } from "development-kit/create-selectors"; +import type { ResourcesCompletionState } from "./models"; +import { create } from "zustand"; + +const useResourcesCompletionState = createSelectors( + create(() => ({ + idle: true, + busy: false, + error: null, + completions: {}, + })), +); + +export { useResourcesCompletionState }; diff --git a/src/modules/resource-completions/store/models.ts b/src/modules/resource-completions/store/models.ts new file mode 100644 index 000000000..66196592a --- /dev/null +++ b/src/modules/resource-completions/store/models.ts @@ -0,0 +1,10 @@ +import { API4MarkdownDto, ParsedError } from "api-4markdown-contracts"; + +type ResourcesCompletionState = { + idle: boolean; + busy: boolean; + error: ParsedError | null; + completions: API4MarkdownDto<"getUserResourceCompletions">; +}; + +export type { ResourcesCompletionState }; From 068a0518b76bce6dfa6a3b01bb85619dff5e7917 Mon Sep 17 00:00:00 2001 From: polubis Date: Fri, 25 Jul 2025 08:28:51 +0200 Subject: [PATCH 02/19] wip --- src/actions/log-out.action.ts | 1 + src/components/user-popover-content.tsx | 8 -------- src/core/app-events.ts | 10 +++------- src/core/use-auth.ts | 8 ++++++-- src/development-kit/create-selectors.ts | 6 +++++- 5 files changed, 15 insertions(+), 18 deletions(-) diff --git a/src/actions/log-out.action.ts b/src/actions/log-out.action.ts index 89c60d744..27d36ae91 100644 --- a/src/actions/log-out.action.ts +++ b/src/actions/log-out.action.ts @@ -9,6 +9,7 @@ const logOut = async (): Promise => { `getYourDocuments`, `getYourAccount`, `getYourMindmaps`, + `getUserResourceCompletions`, ); } catch {} }; diff --git a/src/components/user-popover-content.tsx b/src/components/user-popover-content.tsx index 2d3f28fee..08c9c3f69 100644 --- a/src/components/user-popover-content.tsx +++ b/src/components/user-popover-content.tsx @@ -19,20 +19,12 @@ import { useYourAccountState } from "store/your-account"; import { reloadYourAccountAct } from "acts/reload-your-account.act"; import { navigate } from "gatsby"; import { meta } from "../../meta"; -import { useAppEvent } from "core/app-events"; -import { loadCompletionAct } from "modules/resource-completions"; const UserPopoverContent = ({ onClose }: { onClose(): void }) => { const yourUserProfile = useYourUserProfileState(); const userProfileForm = useSimpleFeature(); const yourAccount = useYourAccountState(); - useAppEvent((event) => { - if (event.type === "USER_AUTHENTICATED") { - loadCompletionAct(); - } - }); - const signOutConfirmation = useConfirm(() => { logOut(); onClose(); diff --git a/src/core/app-events.ts b/src/core/app-events.ts index dd9b1ff7b..7fd0d9ebd 100644 --- a/src/core/app-events.ts +++ b/src/core/app-events.ts @@ -1,13 +1,9 @@ import React, { useRef } from "react"; import { Subject, Subscription } from "rxjs"; -type AppEvent = - | { - type: "SHOW_USER_PROFILE_FORM"; - } - | { - type: "USER_AUTHENTICATED"; - }; +type AppEvent = { + type: "SHOW_USER_PROFILE_FORM"; +}; const appEventsBus = new Subject(); const appEventsBus$ = appEventsBus.asObservable(); diff --git a/src/core/use-auth.ts b/src/core/use-auth.ts index 60833164f..a2de5d6ba 100644 --- a/src/core/use-auth.ts +++ b/src/core/use-auth.ts @@ -9,7 +9,10 @@ import { useYourAccountState } from "store/your-account"; import { initializeAPI } from "api-4markdown"; import { graphql, useStaticQuery } from "gatsby"; import { SiteMetadata } from "./models"; -import { emit } from "./app-events"; +import { + loadCompletionAct, + useResourcesCompletionState, +} from "modules/resource-completions"; type SiteMetadataQuery = { site: { @@ -35,7 +38,7 @@ const useAuth = () => { React.useEffect(() => { const unsubscribe = api.onAuthChange((user) => { if (user) { - emit({ type: "USER_AUTHENTICATED" }); + loadCompletionAct(); authStoreActions.authorize({ avatar: user.photoURL, @@ -53,6 +56,7 @@ const useAuth = () => { authStoreActions.unauthorize(); useMindmapCreatorState.reset(); useYourAccountState.reset(); + useResourcesCompletionState.reset(); }); return () => { diff --git a/src/development-kit/create-selectors.ts b/src/development-kit/create-selectors.ts index a7e2265f2..0d2b5fd17 100644 --- a/src/development-kit/create-selectors.ts +++ b/src/development-kit/create-selectors.ts @@ -1,7 +1,7 @@ import { StoreApi, UseBoundStore } from "zustand"; type WithSelectors = S extends { getState: () => infer T } - ? S & { use: { [K in keyof T]: () => T[K] } } + ? S & { use: { [K in keyof T]: () => T[K] }; reset: () => void } : never; const createSelectors = >>( @@ -9,6 +9,10 @@ const createSelectors = >>( ) => { const store = _store as WithSelectors; store.use = {}; + store.reset = () => { + store.setState(store.getInitialState()); + }; + for (const k of Object.keys(store.getState())) { (store.use as any)[k] = () => store((s) => s[k as keyof typeof s]); } From 9eaf1f68d2f31ba9c125933a3ce757451fb367dd Mon Sep 17 00:00:00 2001 From: polubis Date: Tue, 29 Jul 2025 07:38:14 +0200 Subject: [PATCH 03/19] wip --- src/containers/document-layout.container.tsx | 14 +++++ .../resource-completion-marker.container.tsx | 40 ++++++++++++++ .../resource-completion-trigger.container.tsx | 53 +++++++++++++++++++ src/modules/resource-completions/index.ts | 2 + 4 files changed, 109 insertions(+) create mode 100644 src/modules/resource-completions/containers/resource-completion-marker.container.tsx create mode 100644 src/modules/resource-completions/containers/resource-completion-trigger.container.tsx diff --git a/src/containers/document-layout.container.tsx b/src/containers/document-layout.container.tsx index 9fe17c759..67604a346 100644 --- a/src/containers/document-layout.container.tsx +++ b/src/containers/document-layout.container.tsx @@ -16,6 +16,11 @@ import { ScrollToTop } from "components/scroll-to-top"; import { Markdown } from "components/markdown"; import { useSimpleFeature } from "@greenonsoftware/react-kit"; import { TableOfContent } from "components/table-of-content"; +import { + ResourceCompletionTriggerContainer, + ResourceCompletionMarkerContainer, +} from "modules/resource-completions"; +import { ResourceId } from "api-4markdown-contracts"; const MarkdownWidget = React.lazy(() => import("components/markdown-widget").then(({ MarkdownWidget }) => ({ @@ -40,6 +45,10 @@ const DocumentLayoutContainer = () => { <>
+
+ ); +}; + +export { ResourceCompletionTriggerContainer }; diff --git a/src/modules/resource-completions/index.ts b/src/modules/resource-completions/index.ts index 2133e7fc6..4341fc29b 100644 --- a/src/modules/resource-completions/index.ts +++ b/src/modules/resource-completions/index.ts @@ -1,2 +1,4 @@ export { useResourcesCompletionState } from "./store"; export { loadCompletionAct } from "./acts/load-resource-completions.act"; +export { ResourceCompletionTriggerContainer } from "./containers/resource-completion-trigger.container"; +export { ResourceCompletionMarkerContainer } from "./containers/resource-completion-marker.container"; From b1ff24cf06bde07a7f817362735e82855ba37f37 Mon Sep 17 00:00:00 2001 From: polubis Date: Tue, 29 Jul 2025 08:16:49 +0200 Subject: [PATCH 04/19] wip --- .../contracts/index.ts | 44 ++++++++++++- .../acts/load-resource-completions.act.ts | 29 +++----- .../acts/toggle-resource-completion.act.ts | 31 +++++++++ .../resource-completion-marker.container.tsx | 11 ++-- .../resource-completion-trigger.container.tsx | 66 ++++++++++++++----- .../resource-completions/store/index.ts | 14 ++-- .../resource-completions/store/models.ts | 19 +++--- .../resource-completions/store/selectors.ts | 20 ++++++ 8 files changed, 173 insertions(+), 61 deletions(-) create mode 100644 src/modules/resource-completions/acts/toggle-resource-completion.act.ts create mode 100644 src/modules/resource-completions/store/selectors.ts diff --git a/src/api-4markdown-contracts/contracts/index.ts b/src/api-4markdown-contracts/contracts/index.ts index 9152b93b5..e37077ab6 100644 --- a/src/api-4markdown-contracts/contracts/index.ts +++ b/src/api-4markdown-contracts/contracts/index.ts @@ -2,8 +2,12 @@ import { Brand, type Prettify } from "development-kit/utility-types"; import type { Base64, Date, + DocumentId, Id, + MindmapId, + MindmapNodeId, ResourceId, + ResourceType, Url, UserProfileId, } from "../atoms"; @@ -247,6 +251,43 @@ type GetUserResourceCompletionsContract = Contract< Record >; +type SetUserResourceCompletionContract = Contract< + "setUserResourceCompletion", + | { + resourceId: DocumentId; + type: Extract; + cdate: Date; + } + | { + resourceId: MindmapId; + type: Extract; + cdate: Date; + } + | { + resourceId: MindmapNodeId; + type: Extract; + cdate: Date; + parentId: MindmapId; + } + | null, + | { + cdate: Date; + type: Extract; + resourceId: DocumentId; + } + | { + cdate: Date; + type: Extract; + resourceId: MindmapId; + } + | { + cdate: Date; + type: Extract; + resourceId: MindmapNodeId; + parentId: MindmapId; + } +>; + type API4MarkdownContracts = | CreateMindmapContract | GetYourDocumentsContract @@ -275,7 +316,8 @@ type API4MarkdownContracts = | GetYourAccountContract | GetUserProfileContract | AddUserProfileCommentContract - | GetUserResourceCompletionsContract; + | GetUserResourceCompletionsContract + | SetUserResourceCompletionContract; type API4MarkdownContractKey = API4MarkdownContracts["key"]; type API4MarkdownDto = Extract< diff --git a/src/modules/resource-completions/acts/load-resource-completions.act.ts b/src/modules/resource-completions/acts/load-resource-completions.act.ts index 1a5e88d15..a8bbd8788 100644 --- a/src/modules/resource-completions/acts/load-resource-completions.act.ts +++ b/src/modules/resource-completions/acts/load-resource-completions.act.ts @@ -9,38 +9,27 @@ const loadCompletionAct = async (): Promise => { const cachedCompletions = getCache(key); if (cachedCompletions !== null) { - useResourcesCompletionState.setState({ - idle: false, - busy: false, - error: null, - completions: cachedCompletions, + useResourcesCompletionState.swap({ + is: `ok`, + data: cachedCompletions, }); return; } - useResourcesCompletionState.setState({ - idle: false, - busy: true, - error: null, - completions: {}, - }); + useResourcesCompletionState.swap({ is: `busy` }); const completions = await getAPI().call(key)(); - useResourcesCompletionState.setState({ - idle: false, - busy: false, - error: null, - completions, + useResourcesCompletionState.swap({ + is: `ok`, + data: completions, }); setCache(key, completions); } catch (error) { - useResourcesCompletionState.setState({ - idle: false, - busy: false, + useResourcesCompletionState.swap({ + is: `fail`, error: parseError(error), - completions: {}, }); } }; diff --git a/src/modules/resource-completions/acts/toggle-resource-completion.act.ts b/src/modules/resource-completions/acts/toggle-resource-completion.act.ts new file mode 100644 index 000000000..14e890995 --- /dev/null +++ b/src/modules/resource-completions/acts/toggle-resource-completion.act.ts @@ -0,0 +1,31 @@ +import { + API4MarkdownContractKey, + API4MarkdownDto, + API4MarkdownPayload, +} from "api-4markdown-contracts"; +import { getAPI, getCache, parseError, setCache } from "api-4markdown"; +import { AsyncResult } from "development-kit/utility-types"; + +const toggleResourceCompletionAct = async ( + payload: API4MarkdownPayload<"setUserResourceCompletion">, +): AsyncResult> => { + try { + const key: API4MarkdownContractKey = "setUserResourceCompletion"; + + const dto = await getAPI().call(key)(payload); + + setCache(key, dto); + + return { + is: `ok`, + data: dto, + }; + } catch (error) { + return { + is: `fail`, + error: parseError(error), + }; + } +}; + +export { toggleResourceCompletionAct }; diff --git a/src/modules/resource-completions/containers/resource-completion-marker.container.tsx b/src/modules/resource-completions/containers/resource-completion-marker.container.tsx index eea326e7d..3ac466eba 100644 --- a/src/modules/resource-completions/containers/resource-completion-marker.container.tsx +++ b/src/modules/resource-completions/containers/resource-completion-marker.container.tsx @@ -1,8 +1,10 @@ import React from "react"; import { useResourcesCompletionState } from "../store"; -import { ResourceCompletionDto, ResourceId } from "api-4markdown-contracts"; +import { ResourceId } from "api-4markdown-contracts"; import { BiCheckboxChecked } from "react-icons/bi"; import { c } from "design-system/c"; +import { rawResourcesCompletionSelector } from "../store/selectors"; +import { useShallow } from "zustand/react/shallow"; type ResourceCompletionMarkerContainerProps = { resourceId: ResourceId; @@ -13,10 +15,9 @@ const ResourceCompletionMarkerContainer = ({ resourceId, className, }: ResourceCompletionMarkerContainerProps) => { - const completions = useResourcesCompletionState.use.completions(); - const completion = completions[resourceId] as - | ResourceCompletionDto - | undefined; + const completion = useResourcesCompletionState( + useShallow((state) => rawResourcesCompletionSelector(state)[resourceId]), + ); if (!completion) { return null; diff --git a/src/modules/resource-completions/containers/resource-completion-trigger.container.tsx b/src/modules/resource-completions/containers/resource-completion-trigger.container.tsx index 91a3744b2..09151c6bc 100644 --- a/src/modules/resource-completions/containers/resource-completion-trigger.container.tsx +++ b/src/modules/resource-completions/containers/resource-completion-trigger.container.tsx @@ -1,34 +1,42 @@ import React from "react"; -import { BiCheckboxChecked, BiCheckboxMinus } from "react-icons/bi"; +import { BiCheckboxChecked, BiCheckboxMinus, BiError } from "react-icons/bi"; import { useResourcesCompletionState } from "../store"; import { Button } from "design-system/button"; -import { ResourceCompletionDto, ResourceId } from "api-4markdown-contracts"; +import { API4MarkdownPayload } from "api-4markdown-contracts"; import { useAuthStore } from "store/auth/auth.store"; import { logIn } from "actions/log-in.action"; +import { toggleResourceCompletionAct } from "../acts/toggle-resource-completion.act"; type ResourceCompletionTriggerContainerProps = { - resourceId: ResourceId; className?: string; -}; +} & API4MarkdownPayload<"setUserResourceCompletion">; -const ResourceCompletionTriggerContainer = ({ - resourceId, +const TriggerContainer = ({ className, + ...payload }: ResourceCompletionTriggerContainerProps) => { - const { completions } = useResourcesCompletionState(); - const completion = completions[resourceId] as - | ResourceCompletionDto - | undefined; + const completions = useResourcesCompletionState(); const triggerToggleCompletion = () => { - const authStore = useAuthStore.getState(); - - if (authStore.is !== "authorized") { - logIn(); - return; - } + toggleResourceCompletionAct(payload); }; + if (completions.is === "idle" || completions.is === "busy") { + return ( + + ); + } + + if (completions.is === "fail") { + return ( + + ); + } + return ( + ); +}; + export { ResourceCompletionTriggerContainer }; diff --git a/src/modules/resource-completions/store/index.ts b/src/modules/resource-completions/store/index.ts index 34bb52c87..12ff761e7 100644 --- a/src/modules/resource-completions/store/index.ts +++ b/src/modules/resource-completions/store/index.ts @@ -1,14 +1,8 @@ -import { createSelectors } from "development-kit/create-selectors"; +import { state } from "development-kit/state"; import type { ResourcesCompletionState } from "./models"; -import { create } from "zustand"; -const useResourcesCompletionState = createSelectors( - create(() => ({ - idle: true, - busy: false, - error: null, - completions: {}, - })), -); +const useResourcesCompletionState = state({ + is: `idle`, +}); export { useResourcesCompletionState }; diff --git a/src/modules/resource-completions/store/models.ts b/src/modules/resource-completions/store/models.ts index 66196592a..f18208c6e 100644 --- a/src/modules/resource-completions/store/models.ts +++ b/src/modules/resource-completions/store/models.ts @@ -1,10 +1,13 @@ -import { API4MarkdownDto, ParsedError } from "api-4markdown-contracts"; +import { API4MarkdownDto } from "api-4markdown-contracts"; +import { Transaction } from "development-kit/utility-types"; -type ResourcesCompletionState = { - idle: boolean; - busy: boolean; - error: ParsedError | null; - completions: API4MarkdownDto<"getUserResourceCompletions">; -}; +type ResourcesCompletionState = Transaction<{ + data: API4MarkdownDto<"getUserResourceCompletions">; +}>; -export type { ResourcesCompletionState }; +type OkResourcesCompletionState = Extract< + ResourcesCompletionState, + { is: `ok` } +>; + +export type { ResourcesCompletionState, OkResourcesCompletionState }; diff --git a/src/modules/resource-completions/store/selectors.ts b/src/modules/resource-completions/store/selectors.ts new file mode 100644 index 000000000..f8000ebde --- /dev/null +++ b/src/modules/resource-completions/store/selectors.ts @@ -0,0 +1,20 @@ +import { OkResourcesCompletionState, ResourcesCompletionState } from "./models"; + +const okResourcesCompletionSelector = ( + state: ResourcesCompletionState, +): OkResourcesCompletionState => { + if (state.is !== `ok`) + throw Error(`Invalid reading attempt. Cannot find resource completion`); + + return state; +}; + +const rawResourcesCompletionSelector = ( + state: ResourcesCompletionState, +): OkResourcesCompletionState["data"] => { + if (state.is !== `ok`) return {}; + + return state.data; +}; + +export { okResourcesCompletionSelector, rawResourcesCompletionSelector }; From b488194abf4204ec986361070de285a85f6d4245 Mon Sep 17 00:00:00 2001 From: polubis Date: Tue, 29 Jul 2025 08:46:31 +0200 Subject: [PATCH 05/19] wip --- .../contracts/index.ts | 21 +------- src/containers/document-layout.container.tsx | 5 +- .../acts/load-resource-completions.act.ts | 14 +++++- .../acts/toggle-resource-completion.act.ts | 49 ++++++++++++++----- .../resource-completion-trigger.container.tsx | 19 +++++-- 5 files changed, 68 insertions(+), 40 deletions(-) diff --git a/src/api-4markdown-contracts/contracts/index.ts b/src/api-4markdown-contracts/contracts/index.ts index e37077ab6..679e19cdf 100644 --- a/src/api-4markdown-contracts/contracts/index.ts +++ b/src/api-4markdown-contracts/contracts/index.ts @@ -253,35 +253,16 @@ type GetUserResourceCompletionsContract = Contract< type SetUserResourceCompletionContract = Contract< "setUserResourceCompletion", + ResourceCompletionDto | null, | { - resourceId: DocumentId; - type: Extract; - cdate: Date; - } - | { - resourceId: MindmapId; - type: Extract; - cdate: Date; - } - | { - resourceId: MindmapNodeId; - type: Extract; - cdate: Date; - parentId: MindmapId; - } - | null, - | { - cdate: Date; type: Extract; resourceId: DocumentId; } | { - cdate: Date; type: Extract; resourceId: MindmapId; } | { - cdate: Date; type: Extract; resourceId: MindmapNodeId; parentId: MindmapId; diff --git a/src/containers/document-layout.container.tsx b/src/containers/document-layout.container.tsx index 67604a346..febb14c40 100644 --- a/src/containers/document-layout.container.tsx +++ b/src/containers/document-layout.container.tsx @@ -20,7 +20,7 @@ import { ResourceCompletionTriggerContainer, ResourceCompletionMarkerContainer, } from "modules/resource-completions"; -import { ResourceId } from "api-4markdown-contracts"; +import { DocumentId, ResourceId } from "api-4markdown-contracts"; const MarkdownWidget = React.lazy(() => import("components/markdown-widget").then(({ MarkdownWidget }) => ({ @@ -94,7 +94,8 @@ const DocumentLayoutContainer = () => {
{author?.bio && author?.displayName && ( diff --git a/src/modules/resource-completions/acts/load-resource-completions.act.ts b/src/modules/resource-completions/acts/load-resource-completions.act.ts index a8bbd8788..bf4c831a4 100644 --- a/src/modules/resource-completions/acts/load-resource-completions.act.ts +++ b/src/modules/resource-completions/acts/load-resource-completions.act.ts @@ -1,11 +1,21 @@ -import { getAPI, getCache, parseError, setCache } from "api-4markdown"; +import { + getAPI, + getCache, + parseError, + removeCache, + setCache, +} from "api-4markdown"; import { useResourcesCompletionState } from "../store"; import { API4MarkdownContractKey } from "api-4markdown-contracts"; -const loadCompletionAct = async (): Promise => { +const loadCompletionAct = async (reload = false): Promise => { try { const key: API4MarkdownContractKey = `getUserResourceCompletions`; + if (reload) { + removeCache(key); + } + const cachedCompletions = getCache(key); if (cachedCompletions !== null) { diff --git a/src/modules/resource-completions/acts/toggle-resource-completion.act.ts b/src/modules/resource-completions/acts/toggle-resource-completion.act.ts index 14e890995..d2688591d 100644 --- a/src/modules/resource-completions/acts/toggle-resource-completion.act.ts +++ b/src/modules/resource-completions/acts/toggle-resource-completion.act.ts @@ -1,25 +1,48 @@ -import { - API4MarkdownContractKey, - API4MarkdownDto, - API4MarkdownPayload, -} from "api-4markdown-contracts"; -import { getAPI, getCache, parseError, setCache } from "api-4markdown"; +import { API4MarkdownPayload } from "api-4markdown-contracts"; +import { getAPI, parseError, setCache } from "api-4markdown"; import { AsyncResult } from "development-kit/utility-types"; +import { useResourcesCompletionState } from "../store"; +import { okResourcesCompletionSelector } from "../store/selectors"; const toggleResourceCompletionAct = async ( payload: API4MarkdownPayload<"setUserResourceCompletion">, -): AsyncResult> => { +): AsyncResult => { try { - const key: API4MarkdownContractKey = "setUserResourceCompletion"; + const completion = await getAPI().call("setUserResourceCompletion")( + payload, + ); - const dto = await getAPI().call(key)(payload); + const currentCompletions = { + ...okResourcesCompletionSelector(useResourcesCompletionState.get()).data, + }; - setCache(key, dto); + if (completion) { + const newCompletions = { + ...currentCompletions, + [payload.resourceId]: completion, + }; - return { + useResourcesCompletionState.swap({ + is: `ok`, + data: newCompletions, + }); + + setCache("getUserResourceCompletions", newCompletions); + + return { is: `ok` }; + } + + const { [payload.resourceId]: completionToRemove, ...newCompletions } = + currentCompletions; + + useResourcesCompletionState.swap({ is: `ok`, - data: dto, - }; + data: newCompletions, + }); + + setCache("getUserResourceCompletions", newCompletions); + + return { is: `ok` }; } catch (error) { return { is: `fail`, diff --git a/src/modules/resource-completions/containers/resource-completion-trigger.container.tsx b/src/modules/resource-completions/containers/resource-completion-trigger.container.tsx index 09151c6bc..0b9ff0161 100644 --- a/src/modules/resource-completions/containers/resource-completion-trigger.container.tsx +++ b/src/modules/resource-completions/containers/resource-completion-trigger.container.tsx @@ -6,6 +6,8 @@ import { API4MarkdownPayload } from "api-4markdown-contracts"; import { useAuthStore } from "store/auth/auth.store"; import { logIn } from "actions/log-in.action"; import { toggleResourceCompletionAct } from "../acts/toggle-resource-completion.act"; +import { Transaction } from "development-kit/utility-types"; +import { loadCompletionAct } from "../acts/load-resource-completions.act"; type ResourceCompletionTriggerContainerProps = { className?: string; @@ -16,9 +18,13 @@ const TriggerContainer = ({ ...payload }: ResourceCompletionTriggerContainerProps) => { const completions = useResourcesCompletionState(); + const [completionChange, setCompletionChange] = React.useState({ + is: `idle`, + }); - const triggerToggleCompletion = () => { - toggleResourceCompletionAct(payload); + const triggerToggleCompletion = async () => { + setCompletionChange({ is: `busy` }); + setCompletionChange(await toggleResourceCompletionAct(payload)); }; if (completions.is === "idle" || completions.is === "busy") { @@ -31,7 +37,13 @@ const TriggerContainer = ({ if (completions.is === "fail") { return ( - ); @@ -42,6 +54,7 @@ const TriggerContainer = ({ className={className} s={2} i={2} + disabled={completionChange.is === "busy"} auto onClick={triggerToggleCompletion} > From 859fea0d844e2902860e4a09e052b93809458949 Mon Sep 17 00:00:00 2001 From: polubis Date: Tue, 29 Jul 2025 08:47:00 +0200 Subject: [PATCH 06/19] wip --- .husky/pre-commit | 1 + 1 file changed, 1 insertion(+) diff --git a/.husky/pre-commit b/.husky/pre-commit index 6b587447b..3e25b9805 100644 --- a/.husky/pre-commit +++ b/.husky/pre-commit @@ -1,2 +1,3 @@ +npm run typecheck npm run lint:fix git add . \ No newline at end of file From a35ce8fcd052699dc60599d1078e4bd5bd479d72 Mon Sep 17 00:00:00 2001 From: polubis Date: Tue, 29 Jul 2025 08:56:40 +0200 Subject: [PATCH 07/19] wip --- .../resource-completion-trigger.container.tsx | 20 ++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/src/modules/resource-completions/containers/resource-completion-trigger.container.tsx b/src/modules/resource-completions/containers/resource-completion-trigger.container.tsx index 0b9ff0161..1ee040a56 100644 --- a/src/modules/resource-completions/containers/resource-completion-trigger.container.tsx +++ b/src/modules/resource-completions/containers/resource-completion-trigger.container.tsx @@ -13,6 +13,8 @@ type ResourceCompletionTriggerContainerProps = { className?: string; } & API4MarkdownPayload<"setUserResourceCompletion">; +const CHANGE_COMPLETION_KEY = `change-completion`; + const TriggerContainer = ({ className, ...payload @@ -21,12 +23,23 @@ const TriggerContainer = ({ const [completionChange, setCompletionChange] = React.useState({ is: `idle`, }); + const authStore = useAuthStore(); const triggerToggleCompletion = async () => { setCompletionChange({ is: `busy` }); setCompletionChange(await toggleResourceCompletionAct(payload)); }; + React.useEffect(() => { + if ( + authStore.is === "authorized" && + localStorage.getItem(CHANGE_COMPLETION_KEY) === `1` + ) { + localStorage.removeItem(CHANGE_COMPLETION_KEY); + toggleResourceCompletionAct(payload); + } + }, [authStore]); + if (completions.is === "idle" || completions.is === "busy") { return ( From 3017c70ff595d10c02a4baab7e803d2f3793219e Mon Sep 17 00:00:00 2001 From: polubis Date: Tue, 29 Jul 2025 08:58:40 +0200 Subject: [PATCH 08/19] wip --- .../resource-completion-trigger.container.tsx | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/src/modules/resource-completions/containers/resource-completion-trigger.container.tsx b/src/modules/resource-completions/containers/resource-completion-trigger.container.tsx index 1ee040a56..17bfb77a8 100644 --- a/src/modules/resource-completions/containers/resource-completion-trigger.container.tsx +++ b/src/modules/resource-completions/containers/resource-completion-trigger.container.tsx @@ -71,13 +71,21 @@ const TriggerContainer = ({ auto onClick={triggerToggleCompletion} > - {completions.data[payload.resourceId] ? ( + {completionChange.is === "fail" ? ( <> - Mark As Uncompleted + Ups, Change Failed ) : ( <> - Mark As Completed + {completions.data[payload.resourceId] ? ( + <> + Mark As Uncompleted + + ) : ( + <> + Mark As Completed + + )} )} From 2f4869e3cdd8315bca07f5ba72ee41c59e6b9ae1 Mon Sep 17 00:00:00 2001 From: polubis Date: Tue, 29 Jul 2025 09:01:31 +0200 Subject: [PATCH 09/19] wip --- src/components/markdown-widget.tsx | 57 ++++++++++++++++-------------- 1 file changed, 30 insertions(+), 27 deletions(-) diff --git a/src/components/markdown-widget.tsx b/src/components/markdown-widget.tsx index f973d076f..4b8d1bdd4 100644 --- a/src/components/markdown-widget.tsx +++ b/src/components/markdown-widget.tsx @@ -44,10 +44,7 @@ const MarkdownWidget = ({ const asideNavigation = useSimpleFeature(); const [activeHeading, setActiveHeading] = React.useState(null); - const headings = React.useMemo( - () => (asideNavigation.isOn ? extractHeadings(markdown) : []), - [markdown, asideNavigation.isOn], - ); + const headings = React.useMemo(() => extractHeadings(markdown), [markdown]); const chunks = React.useMemo((): string[] => { if (chunksMode.isOff) return []; @@ -216,13 +213,6 @@ const MarkdownWidget = ({ > {chunksMode.isOn ? : } - -
+ +
+ +
+ {finalHeadings.length > 1 && ( + + )} + {chunksMode.isOn && ( <> - } - onClose={closeNodePreviewAction} - markdown={nodePreview.data.content} - /> + + + + + } + onClose={closeNodePreviewAction} + markdown={nodePreview.data.content} + /> + ); }; diff --git a/src/modules/mindmap-preview/components/external-node-tile.tsx b/src/modules/mindmap-preview/components/external-node-tile.tsx index 3d84d0997..3e271ccd2 100644 --- a/src/modules/mindmap-preview/components/external-node-tile.tsx +++ b/src/modules/mindmap-preview/components/external-node-tile.tsx @@ -3,20 +3,36 @@ import { HandleX, HandleY } from "./handles"; import { type NodeProps } from "@xyflow/react"; import { NodeTile } from "./node-tile"; import { Button } from "design-system/button"; -import { BiWorld } from "react-icons/bi"; -import type { MindmapPreviewExternalNode } from "store/mindmap-preview/models"; +import { BiCheckboxChecked, BiCheckboxMinus, BiWorld } from "react-icons/bi"; +import { MindmapPreviewExternalNodeWithCompletion } from "../models"; -type ExternalNodeTileProps = NodeProps; +type ExternalNodeTileProps = + NodeProps; const ExternalNodeTile = ({ data }: ExternalNodeTileProps) => { return ( - + External Resource {data.name} {data.description && ( {data.description} )} + { return (
{children} @@ -35,10 +33,11 @@ NodeTile.Label = ({ children }: { children: ReactNode }) => { NodeTile.Description = ({ children }: { children: ReactNode }) => { return

{children}

; }; + // eslint-disable-next-line react/display-name NodeTile.Toolbox = ({ children }: { children: ReactNode }) => { return ( -
+
{children}
); diff --git a/src/modules/mindmap-preview/containers/embedded-node-tile.container.tsx b/src/modules/mindmap-preview/containers/embedded-node-tile.container.tsx index bdb889a66..1f3f9d3ef 100644 --- a/src/modules/mindmap-preview/containers/embedded-node-tile.container.tsx +++ b/src/modules/mindmap-preview/containers/embedded-node-tile.container.tsx @@ -3,46 +3,68 @@ import React from "react"; import { HandleX, HandleY } from "../components/handles"; import { NodeTile } from "../components/node-tile"; import { Button } from "design-system/button"; -import { BiBook } from "react-icons/bi"; -import type { MindmapPreviewEmbeddedNode } from "store/mindmap-preview/models"; +import { BiBook, BiCheckboxChecked, BiCheckboxMinus } from "react-icons/bi"; import { openNodePreviewAction } from "store/mindmap-preview/actions"; +import { MindmapPreviewEmbeddedNodeWithCompletion } from "../models"; -type EmbeddedNodeTileContainerProps = NodeProps; +type EmbeddedNodeTileContainerProps = + NodeProps; const EmbeddedNodeTileContainer = ({ id, positionAbsoluteX, positionAbsoluteY, data, -}: EmbeddedNodeTileContainerProps) => ( - - Embedded Resource - {data.name} - {data.description && ( - {data.description} - )} - - - - -); +}: EmbeddedNodeTileContainerProps) => { + return ( + + Embedded Resource + {data.name} + {data.description && ( + {data.description} + )} + + + + + + ); +}; const EmbeddedNodeTileContainerX = (props: EmbeddedNodeTileContainerProps) => ( diff --git a/src/modules/mindmap-preview/mindmap-preview.module.tsx b/src/modules/mindmap-preview/mindmap-preview.module.tsx index 061ddb1b7..01787eabd 100644 --- a/src/modules/mindmap-preview/mindmap-preview.module.tsx +++ b/src/modules/mindmap-preview/mindmap-preview.module.tsx @@ -18,7 +18,6 @@ import { import { SolidEdge } from "./components/solid-edge"; import type { MindmapPreviewEdge, - MindmapPreviewNode, MindmapPreviewOkMindmap, } from "store/mindmap-preview/models"; import { @@ -26,12 +25,25 @@ import { EmbeddedNodeTileContainerY, } from "./containers/embedded-node-tile.container"; import { closeNodePreviewAction } from "store/mindmap-preview/actions"; -import { MarkdownWidget } from "components/markdown-widget"; +import { + ResourceCompletionTriggerContainer, + useResourcesCompletionState, +} from "modules/resource-completions"; +import { MindmapId, MindmapNodeId } from "api-4markdown-contracts"; +import { rawResourcesCompletionSelector } from "modules/resource-completions/store/selectors"; +import { useShallow } from "zustand/react/shallow"; +import { MindmapPreviewNodeWithCompletion } from "./models"; + +const MarkdownWidget = React.lazy(() => + import("components/markdown-widget").then(({ MarkdownWidget }) => ({ + default: MarkdownWidget, + })), +); type MindmapNodeTypes = { [Orientation in MindmapPreviewOkMindmap["orientation"]]: { - [Type in MindmapPreviewNode["type"]]: ComponentType< - NodeProps> + [Type in MindmapPreviewNodeWithCompletion["type"]]: ComponentType< + NodeProps> >; }; }; @@ -62,11 +74,24 @@ const MindmapPreviewModule = () => { readyMindmapPreviewSelector(state.mindmap), ); const nodePreview = useMindmapPreviewState((state) => state.nodePreview); + const nodesWithCompletion = useResourcesCompletionState( + useShallow((state) => { + const completions = rawResourcesCompletionSelector(state); + + return mindmap.nodes.map((node) => ({ + ...node, + data: { + ...node.data, + completion: completions[node.id as MindmapNodeId] ?? null, + }, + })); + }), + ); return ( <> { {nodePreview.is === `on` && ( - + + + } + chunksActive={false} + onClose={closeNodePreviewAction} + markdown={nodePreview.data.content || `No content for this node`} + /> + )} ); diff --git a/src/modules/mindmap-preview/models/index.ts b/src/modules/mindmap-preview/models/index.ts new file mode 100644 index 000000000..05878cdbd --- /dev/null +++ b/src/modules/mindmap-preview/models/index.ts @@ -0,0 +1,36 @@ +import { ResourceCompletionDto } from "api-4markdown-contracts"; +import { Prettify } from "development-kit/utility-types"; +import { + MindmapPreviewEmbeddedNode, + MindmapPreviewExternalNode, +} from "store/mindmap-preview/models"; + +type MindmapPreviewEmbeddedNodeWithCompletion = Prettify< + Omit & { + data: Prettify< + MindmapPreviewEmbeddedNode["data"] & { + completion: ResourceCompletionDto | null; + } + >; + } +>; + +type MindmapPreviewExternalNodeWithCompletion = Prettify< + Omit & { + data: Prettify< + MindmapPreviewExternalNode["data"] & { + completion: ResourceCompletionDto | null; + } + >; + } +>; + +type MindmapPreviewNodeWithCompletion = + | MindmapPreviewEmbeddedNodeWithCompletion + | MindmapPreviewExternalNodeWithCompletion; + +export type { + MindmapPreviewNodeWithCompletion, + MindmapPreviewEmbeddedNodeWithCompletion, + MindmapPreviewExternalNodeWithCompletion, +}; From b31222b4aee9426b427fa85964da740fa371d5d7 Mon Sep 17 00:00:00 2001 From: polubis Date: Wed, 30 Jul 2025 17:31:05 +0200 Subject: [PATCH 14/19] wip --- src/components/education-documents-list.tsx | 23 +++- src/containers/document-layout.container.tsx | 69 ++++++++-- .../education-zone/education-zone.view.tsx | 28 +++- .../mindmap-preview.module.tsx | 25 +++- .../acts/load-resource-completions.act.ts | 6 +- .../resource-completion-marker.container.tsx | 71 ---------- .../resource-completion-trigger.container.tsx | 124 ------------------ .../hooks/use-is-resource-completed.ts | 19 +++ .../hooks/use-resource-completion-toggle.ts | 32 +++++ src/modules/resource-completions/index.ts | 4 +- .../resource-completions/store/models.ts | 4 +- 11 files changed, 177 insertions(+), 228 deletions(-) delete mode 100644 src/modules/resource-completions/containers/resource-completion-marker.container.tsx delete mode 100644 src/modules/resource-completions/containers/resource-completion-trigger.container.tsx create mode 100644 src/modules/resource-completions/hooks/use-is-resource-completed.ts create mode 100644 src/modules/resource-completions/hooks/use-resource-completion-toggle.ts diff --git a/src/components/education-documents-list.tsx b/src/components/education-documents-list.tsx index 379f8fdb0..4202c6344 100644 --- a/src/components/education-documents-list.tsx +++ b/src/components/education-documents-list.tsx @@ -5,7 +5,8 @@ import { Link } from "gatsby"; import type { RichEducationDocumentModel } from "models/page-models"; import React from "react"; import { meta } from "../../meta"; -import { ResourceCompletionMarkerContainer } from "modules/resource-completions"; +import { BiCheckboxChecked } from "react-icons/bi"; +import { useResourceCompletion } from "modules/resource-completions"; import { DocumentId } from "api-4markdown-contracts"; type EducationDocumentsListProps = { @@ -14,6 +15,21 @@ type EducationDocumentsListProps = { const now = new Date(); +const ResourceCompletionMarkerContainer = ({ id }: { id: DocumentId }) => { + const completion = useResourceCompletion(id); + + if (!completion) { + return null; + } + + return ( + + + ); +}; + const EducationDocumentsList = ({ documents }: EducationDocumentsListProps) => { return (
    @@ -60,10 +76,7 @@ const EducationDocumentsList = ({ documents }: EducationDocumentsListProps) => {

    {document.description}

    - + {document.tags.join(`, `)} diff --git a/src/containers/document-layout.container.tsx b/src/containers/document-layout.container.tsx index efa34171d..fd6b9fe90 100644 --- a/src/containers/document-layout.container.tsx +++ b/src/containers/document-layout.container.tsx @@ -1,7 +1,14 @@ import React from "react"; import { Badge } from "design-system/badge"; import { Avatar } from "design-system/avatar"; -import { BiBook, BiCheck, BiCopyAlt, BiLogoMarkdown } from "react-icons/bi"; +import { + BiBook, + BiCheck, + BiCheckboxChecked, + BiCheckboxMinus, + BiCopyAlt, + BiLogoMarkdown, +} from "react-icons/bi"; import { Button } from "design-system/button"; import { useCopy } from "development-kit/use-copy"; import { Status } from "design-system/status"; @@ -17,10 +24,10 @@ import { Markdown } from "components/markdown"; import { useSimpleFeature } from "@greenonsoftware/react-kit"; import { TableOfContent } from "components/table-of-content"; import { - ResourceCompletionTriggerContainer, - ResourceCompletionMarkerContainer, + useResourceCompletion, + useResourceCompletionToggle, } from "modules/resource-completions"; -import { DocumentId } from "api-4markdown-contracts"; +import { API4MarkdownPayload, DocumentId } from "api-4markdown-contracts"; const MarkdownWidget = React.lazy(() => import("components/markdown-widget").then(({ MarkdownWidget }) => ({ @@ -30,6 +37,49 @@ const MarkdownWidget = React.lazy(() => const CONTENT_ID = `document-layout-content`; +const ResourceCompletionTriggerContainer = () => { + const [{ document }] = useDocumentLayoutContext(); + const [toggleConfig] = React.useState< + API4MarkdownPayload<"setUserResourceCompletion"> + >(() => ({ + type: "document", + resourceId: document.id as DocumentId, + })); + const [state, completion, toggle] = useResourceCompletionToggle(toggleConfig); + // @TODO[PRIO=2]: [Handle error case with some toast or error message]. + return ( + + ); +}; + +const ResourceCompletionMarkerContainer = () => { + const [{ document }] = useDocumentLayoutContext(); + const completion = useResourceCompletion(document.id as DocumentId); + + if (!completion) { + return null; + } + + return ( +

    + + + You're browsing already completed resource. + +

    + ); +}; + const DocumentLayoutContainer = () => { const [{ document }] = useDocumentLayoutContext(); const { code, author } = document; @@ -45,11 +95,7 @@ const DocumentLayoutContainer = () => { <>
    - +
    - +
    {author?.bio && author?.displayName && (
    diff --git a/src/features/education-zone/education-zone.view.tsx b/src/features/education-zone/education-zone.view.tsx index 6d486f127..596ef7a1b 100644 --- a/src/features/education-zone/education-zone.view.tsx +++ b/src/features/education-zone/education-zone.view.tsx @@ -8,7 +8,11 @@ import { CreationLinkContainer } from "containers/creation-link.container"; import { Link } from "gatsby"; import { RATING_ICONS } from "core/rating-config"; import { meta } from "../../../meta"; -import { BiArrowToLeft, BiArrowToRight } from "react-icons/bi"; +import { + BiArrowToLeft, + BiArrowToRight, + BiCheckboxChecked, +} from "react-icons/bi"; import { paginate } from "development-kit/paginate"; import { EducationLayout } from "components/education-layout"; import { EducationDocumentsList } from "components/education-documents-list"; @@ -16,7 +20,7 @@ import { EducationTopTags } from "components/education-top-tags"; import type { EducationPageModel } from "models/page-models"; import { EducationRankLinkContainer } from "containers/education-rank-link.container"; import { DocumentId } from "api-4markdown-contracts"; -import { ResourceCompletionMarkerContainer } from "modules/resource-completions"; +import { useResourceCompletion } from "modules/resource-completions"; type EducationZoneViewProps = EducationPageModel; @@ -77,6 +81,23 @@ const Pagination = ({ ); }; +const ResourceCompletionMarkerContainer = ({ id }: { id: DocumentId }) => { + const completion = useResourceCompletion(id); + + if (!completion) { + return null; + } + + return ( + + + ); +}; + const ContentRank = ({ documents, }: Pick) => { @@ -128,8 +149,7 @@ const ContentRank = ({
    {RATING_ICONS.map(([Icon, category]) => (
    diff --git a/src/modules/mindmap-preview/mindmap-preview.module.tsx b/src/modules/mindmap-preview/mindmap-preview.module.tsx index 01787eabd..46b3df88a 100644 --- a/src/modules/mindmap-preview/mindmap-preview.module.tsx +++ b/src/modules/mindmap-preview/mindmap-preview.module.tsx @@ -26,13 +26,19 @@ import { } from "./containers/embedded-node-tile.container"; import { closeNodePreviewAction } from "store/mindmap-preview/actions"; import { - ResourceCompletionTriggerContainer, + useResourceCompletionToggle, useResourcesCompletionState, } from "modules/resource-completions"; -import { MindmapId, MindmapNodeId } from "api-4markdown-contracts"; +import { + API4MarkdownPayload, + MindmapId, + MindmapNodeId, +} from "api-4markdown-contracts"; import { rawResourcesCompletionSelector } from "modules/resource-completions/store/selectors"; import { useShallow } from "zustand/react/shallow"; import { MindmapPreviewNodeWithCompletion } from "./models"; +import { Button } from "design-system/button"; +import { BiCheckboxChecked, BiCheckboxMinus } from "react-icons/bi"; const MarkdownWidget = React.lazy(() => import("components/markdown-widget").then(({ MarkdownWidget }) => ({ @@ -65,6 +71,21 @@ const mindmapNodeTypes: MindmapNodeTypes = { }, }; +const ResourceCompletionTriggerContainer = ( + props: API4MarkdownPayload<"setUserResourceCompletion">, +) => { + const [state, completion, toggle] = useResourceCompletionToggle(props); + // @TODO[PRIO=2]: [Handle error case with some toast or error message]. + return ( + + ); +}; const edgeTypes: MindmapEdgeTypes = { solid: SolidEdge, }; diff --git a/src/modules/resource-completions/acts/load-resource-completions.act.ts b/src/modules/resource-completions/acts/load-resource-completions.act.ts index bf4c831a4..4372da230 100644 --- a/src/modules/resource-completions/acts/load-resource-completions.act.ts +++ b/src/modules/resource-completions/acts/load-resource-completions.act.ts @@ -8,14 +8,10 @@ import { import { useResourcesCompletionState } from "../store"; import { API4MarkdownContractKey } from "api-4markdown-contracts"; -const loadCompletionAct = async (reload = false): Promise => { +const loadCompletionAct = async (): Promise => { try { const key: API4MarkdownContractKey = `getUserResourceCompletions`; - if (reload) { - removeCache(key); - } - const cachedCompletions = getCache(key); if (cachedCompletions !== null) { diff --git a/src/modules/resource-completions/containers/resource-completion-marker.container.tsx b/src/modules/resource-completions/containers/resource-completion-marker.container.tsx deleted file mode 100644 index d1fe46e04..000000000 --- a/src/modules/resource-completions/containers/resource-completion-marker.container.tsx +++ /dev/null @@ -1,71 +0,0 @@ -import React from "react"; -import { useResourcesCompletionState } from "../store"; -import { ResourceId } from "api-4markdown-contracts"; -import { BiCheckboxChecked } from "react-icons/bi"; -import { rawResourcesCompletionSelector } from "../store/selectors"; -import { useShallow } from "zustand/react/shallow"; -import c from "classnames"; - -type ResourceCompletionMarkerContainerProps = { - resourceId: ResourceId; - variant: "badge" | "info" | "icon"; - className?: string; -}; - -const ResourceCompletionMarkerContainer = ({ - resourceId, - variant, - className, -}: ResourceCompletionMarkerContainerProps) => { - const completion = useResourcesCompletionState( - useShallow((state) => rawResourcesCompletionSelector(state)[resourceId]), - ); - - if (!completion) { - return null; - } - - if (variant === "icon") { - return ( - - - ); - } - - if (variant === "badge") { - return ( - - - ); - } - - return ( -

    - - - You're browsing already completed resource. - -

    - ); -}; - -export { ResourceCompletionMarkerContainer }; diff --git a/src/modules/resource-completions/containers/resource-completion-trigger.container.tsx b/src/modules/resource-completions/containers/resource-completion-trigger.container.tsx deleted file mode 100644 index 17bfb77a8..000000000 --- a/src/modules/resource-completions/containers/resource-completion-trigger.container.tsx +++ /dev/null @@ -1,124 +0,0 @@ -import React from "react"; -import { BiCheckboxChecked, BiCheckboxMinus, BiError } from "react-icons/bi"; -import { useResourcesCompletionState } from "../store"; -import { Button } from "design-system/button"; -import { API4MarkdownPayload } from "api-4markdown-contracts"; -import { useAuthStore } from "store/auth/auth.store"; -import { logIn } from "actions/log-in.action"; -import { toggleResourceCompletionAct } from "../acts/toggle-resource-completion.act"; -import { Transaction } from "development-kit/utility-types"; -import { loadCompletionAct } from "../acts/load-resource-completions.act"; - -type ResourceCompletionTriggerContainerProps = { - className?: string; -} & API4MarkdownPayload<"setUserResourceCompletion">; - -const CHANGE_COMPLETION_KEY = `change-completion`; - -const TriggerContainer = ({ - className, - ...payload -}: ResourceCompletionTriggerContainerProps) => { - const completions = useResourcesCompletionState(); - const [completionChange, setCompletionChange] = React.useState({ - is: `idle`, - }); - const authStore = useAuthStore(); - - const triggerToggleCompletion = async () => { - setCompletionChange({ is: `busy` }); - setCompletionChange(await toggleResourceCompletionAct(payload)); - }; - - React.useEffect(() => { - if ( - authStore.is === "authorized" && - localStorage.getItem(CHANGE_COMPLETION_KEY) === `1` - ) { - localStorage.removeItem(CHANGE_COMPLETION_KEY); - toggleResourceCompletionAct(payload); - } - }, [authStore]); - - if (completions.is === "idle" || completions.is === "busy") { - return ( - - ); - } - - if (completions.is === "fail") { - return ( - - ); - } - - return ( - - ); -}; - -const ResourceCompletionTriggerContainer = ({ - className, - ...payload -}: ResourceCompletionTriggerContainerProps) => { - const authStore = useAuthStore(); - - const triggerLoginWithCompletionChange = () => { - localStorage.setItem(CHANGE_COMPLETION_KEY, `1`); - logIn(); - }; - - if (authStore.is === "authorized") { - return ; - } - - return ( - - ); -}; - -export { ResourceCompletionTriggerContainer }; diff --git a/src/modules/resource-completions/hooks/use-is-resource-completed.ts b/src/modules/resource-completions/hooks/use-is-resource-completed.ts new file mode 100644 index 000000000..c53ec0dce --- /dev/null +++ b/src/modules/resource-completions/hooks/use-is-resource-completed.ts @@ -0,0 +1,19 @@ +import { useShallow } from "zustand/react/shallow"; +import { useResourcesCompletionState } from "../store"; +import { rawResourcesCompletionSelector } from "../store/selectors"; +import { ResourceCompletionDto, ResourceId } from "api-4markdown-contracts"; + +const useResourceCompletion = (resourceId: ResourceId) => { + const completion = useResourcesCompletionState( + useShallow( + (state) => + rawResourcesCompletionSelector(state)[resourceId] as + | ResourceCompletionDto + | undefined, + ), + ); + + return completion; +}; + +export { useResourceCompletion }; diff --git a/src/modules/resource-completions/hooks/use-resource-completion-toggle.ts b/src/modules/resource-completions/hooks/use-resource-completion-toggle.ts new file mode 100644 index 000000000..10b9d85f6 --- /dev/null +++ b/src/modules/resource-completions/hooks/use-resource-completion-toggle.ts @@ -0,0 +1,32 @@ +import React from "react"; +import { useAuthStore } from "store/auth/auth.store"; +import { toggleResourceCompletionAct } from "../acts/toggle-resource-completion.act"; +import { logIn } from "actions/log-in.action"; +import { API4MarkdownPayload } from "api-4markdown-contracts"; +import { Transaction } from "development-kit/utility-types"; +import { useResourceCompletion } from "./use-is-resource-completed"; + +const useResourceCompletionToggle = ( + payload: API4MarkdownPayload<"setUserResourceCompletion">, +) => { + const [state, setState] = React.useState({ + is: `idle`, + }); + + const completion = useResourceCompletion(payload.resourceId); + + const toggle = React.useCallback(async () => { + const authStore = useAuthStore.getState(); + + if (authStore.is === "authorized") { + setState({ is: `busy` }); + setState(await toggleResourceCompletionAct(payload)); + } else { + logIn(); + } + }, [payload]); + + return [state, completion, toggle] as const; +}; + +export { useResourceCompletionToggle }; diff --git a/src/modules/resource-completions/index.ts b/src/modules/resource-completions/index.ts index 4341fc29b..fc013949c 100644 --- a/src/modules/resource-completions/index.ts +++ b/src/modules/resource-completions/index.ts @@ -1,4 +1,4 @@ export { useResourcesCompletionState } from "./store"; export { loadCompletionAct } from "./acts/load-resource-completions.act"; -export { ResourceCompletionTriggerContainer } from "./containers/resource-completion-trigger.container"; -export { ResourceCompletionMarkerContainer } from "./containers/resource-completion-marker.container"; +export { useResourceCompletionToggle } from "./hooks/use-resource-completion-toggle"; +export { useResourceCompletion } from "./hooks/use-is-resource-completed"; diff --git a/src/modules/resource-completions/store/models.ts b/src/modules/resource-completions/store/models.ts index f18208c6e..ec800a5bb 100644 --- a/src/modules/resource-completions/store/models.ts +++ b/src/modules/resource-completions/store/models.ts @@ -1,8 +1,8 @@ -import { API4MarkdownDto } from "api-4markdown-contracts"; +import { ResourceCompletionDto, ResourceId } from "api-4markdown-contracts"; import { Transaction } from "development-kit/utility-types"; type ResourcesCompletionState = Transaction<{ - data: API4MarkdownDto<"getUserResourceCompletions">; + data: Record; }>; type OkResourcesCompletionState = Extract< From 252b4d3042dd8a0457abe2fb4f59895ea82065d0 Mon Sep 17 00:00:00 2001 From: polubis Date: Wed, 30 Jul 2025 17:48:48 +0200 Subject: [PATCH 15/19] wip --- src/api-4markdown-contracts/atoms.ts | 3 +- .../embedded-node-tile.container.tsx | 19 ++++++--- .../external-node-tile.container.tsx} | 40 +++++++++++++------ .../mindmap-preview.module.tsx | 32 ++++++--------- src/modules/mindmap-preview/models/index.ts | 6 +-- 5 files changed, 58 insertions(+), 42 deletions(-) rename src/modules/mindmap-preview/{components/external-node-tile.tsx => containers/external-node-tile.container.tsx} (54%) diff --git a/src/api-4markdown-contracts/atoms.ts b/src/api-4markdown-contracts/atoms.ts index a062d403d..2386c6afa 100644 --- a/src/api-4markdown-contracts/atoms.ts +++ b/src/api-4markdown-contracts/atoms.ts @@ -1,3 +1,4 @@ +import { SUID } from "development-kit/suid"; import { Brand } from "development-kit/utility-types"; type Id = string; @@ -15,7 +16,7 @@ type UserProfileId = Brand; type CommentId = Brand; type DocumentId = Brand; -type MindmapNodeId = Brand; +type MindmapNodeId = Brand; type MindmapId = Brand; type ResourceId = DocumentId | MindmapNodeId | MindmapId; diff --git a/src/modules/mindmap-preview/containers/embedded-node-tile.container.tsx b/src/modules/mindmap-preview/containers/embedded-node-tile.container.tsx index 1f3f9d3ef..f622e9a3c 100644 --- a/src/modules/mindmap-preview/containers/embedded-node-tile.container.tsx +++ b/src/modules/mindmap-preview/containers/embedded-node-tile.container.tsx @@ -6,6 +6,8 @@ import { Button } from "design-system/button"; import { BiBook, BiCheckboxChecked, BiCheckboxMinus } from "react-icons/bi"; import { openNodePreviewAction } from "store/mindmap-preview/actions"; import { MindmapPreviewEmbeddedNodeWithCompletion } from "../models"; +import { useResourceCompletionToggle } from "modules/resource-completions"; +import { MindmapNodeId } from "api-4markdown-contracts"; type EmbeddedNodeTileContainerProps = NodeProps; @@ -16,11 +18,15 @@ const EmbeddedNodeTileContainer = ({ positionAbsoluteY, data, }: EmbeddedNodeTileContainerProps) => { + const [state, completion, toggle] = useResourceCompletionToggle({ + type: "mindmap-node", + resourceId: id as MindmapNodeId, + parentId: data.mindmapId, + }); + return ( Embedded Resource {data.name} @@ -29,15 +35,16 @@ const EmbeddedNodeTileContainer = ({ )}