diff --git a/public/assets/common/bookmark.svg b/public/assets/common/bookmark.svg new file mode 100644 index 0000000000..427c831a23 --- /dev/null +++ b/public/assets/common/bookmark.svg @@ -0,0 +1,3 @@ + + + diff --git a/public/assets/common/icons/bookmark-fill.svg b/public/assets/common/icons/bookmark-fill.svg new file mode 100644 index 0000000000..587a476569 --- /dev/null +++ b/public/assets/common/icons/bookmark-fill.svg @@ -0,0 +1,3 @@ + + + diff --git a/public/assets/common/icons/bookmark.svg b/public/assets/common/icons/bookmark.svg new file mode 100644 index 0000000000..a60abab1d3 --- /dev/null +++ b/public/assets/common/icons/bookmark.svg @@ -0,0 +1,3 @@ + + + diff --git a/public/assets/phy/icons/redesign/page-bookmarks-bg.svg b/public/assets/phy/icons/redesign/page-bookmarks-bg.svg new file mode 100644 index 0000000000..228a5c63c3 --- /dev/null +++ b/public/assets/phy/icons/redesign/page-bookmarks-bg.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/public/assets/phy/icons/redesign/page-bookmarks-fg.svg b/public/assets/phy/icons/redesign/page-bookmarks-fg.svg new file mode 100644 index 0000000000..e70b61e062 --- /dev/null +++ b/public/assets/phy/icons/redesign/page-bookmarks-fg.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/src/IsaacApiTypes.tsx b/src/IsaacApiTypes.tsx index 0403c60c6b..cb11fa88cc 100644 --- a/src/IsaacApiTypes.tsx +++ b/src/IsaacApiTypes.tsx @@ -457,6 +457,7 @@ export interface ContentSummaryDTO { tags?: string[]; url?: string; state?: CompletionState; + bookmarked?: Date; supersededBy?: string; deprecated?: boolean; difficulty?: string; diff --git a/src/IsaacAppTypes.tsx b/src/IsaacAppTypes.tsx index b5afbb3cd0..b1d02bf80f 100644 --- a/src/IsaacAppTypes.tsx +++ b/src/IsaacAppTypes.tsx @@ -313,6 +313,20 @@ export interface ActiveModalProps { bodyContainerClassName?: string; } +export enum BookmarksOrder { + "date" = "date", + "-date" = "-date", + "title" = "title", + "-title" = "-title" +} + +export const BOOKMARKS_ORDER_NAMES: Record = { + [BookmarksOrder.date]: "Date added (newest first)", + [BookmarksOrder["-date"]]: "Date added (oldest first)", + [BookmarksOrder.title]: "Title (A-Z)", + [BookmarksOrder["-title"]]: "Title (Z-A)" +}; + export type ProgressSortOrder = number | "name" | "totalPartPercentage" | "totalAttemptedPartPercentage" | "totalQuestionPercentage" | "totalAttemptedQuestionPercentage"; export enum QuizzesBoardOrder { diff --git a/src/app/components/elements/BookmarkButton.tsx b/src/app/components/elements/BookmarkButton.tsx new file mode 100644 index 0000000000..6607825b2e --- /dev/null +++ b/src/app/components/elements/BookmarkButton.tsx @@ -0,0 +1,38 @@ +import React, { useState } from "react"; +import { IconButton } from "./AffixButton"; +import { useBookmarks } from "../../services/bookmarks"; +import classNames from "classnames"; +import { ContentDTO } from "../../../IsaacApiTypes"; +import { Tooltip } from "reactstrap"; + +export const BookmarkButton = ({ doc }: { doc?: ContentDTO }) => { + const { isBookmarked, bookmarkItem } = useBookmarks(); + const isQuestionBookmarked = doc?.type === "isaacQuestionPage" && doc.id ? isBookmarked(doc.id) : false; + const [showBookmarkTooltip, setShowBookmarkTooltip] = useState(false); + + return <> + { + if (!isQuestionBookmarked) { + setShowBookmarkTooltip(true); + setTimeout(() => setShowBookmarkTooltip(false), 1000); + } + + bookmarkItem(doc?.id); + }} + /> + setShowBookmarkTooltip(!showBookmarkTooltip)} + trigger="manual" + > +
Saved!
+
+ ; +}; diff --git a/src/app/components/elements/PageMetadata.tsx b/src/app/components/elements/PageMetadata.tsx index fb754ba982..8e516238e1 100644 --- a/src/app/components/elements/PageMetadata.tsx +++ b/src/app/components/elements/PageMetadata.tsx @@ -16,6 +16,8 @@ import { UserContextPicker } from './inputs/UserContextPicker'; import { LLMFreeTextQuestionIndicator } from './LLMFreeTextQuestionIndicator'; import { CrossTopicQuestionIndicator } from './CrossTopicQuestionIndicator'; import { selectors, useAppSelector } from '../../state'; +import { BookmarkButton } from './BookmarkButton'; +import { FeatureFlag, FeatureFlagWrapper } from '../../services/featureFlag'; type PageMetadataProps = { doc?: SeguePageDTO; @@ -51,6 +53,10 @@ export const ActionButtons = ({location, isQuestion, helpModalId, doc, ...rest}: const anyActionButtonShown = isPhy && helpModalId || above['sm'](deviceSize) || doc?.id; return anyActionButtonShown &&
+ {isPhy && isQuestion && + + + } {isPhy && helpModalId && } {above['sm'](deviceSize) && <> diff --git a/src/app/components/elements/list-groups/AbstractListViewItem.tsx b/src/app/components/elements/list-groups/AbstractListViewItem.tsx index 974fb4ab88..0dd3716fd9 100644 --- a/src/app/components/elements/list-groups/AbstractListViewItem.tsx +++ b/src/app/components/elements/list-groups/AbstractListViewItem.tsx @@ -5,7 +5,7 @@ import { ViewingContext} from "../../../../IsaacAppTypes"; import classNames from "classnames"; import { Badge, Button, Col, ListGroupItem, ListGroupItemProps } from "reactstrap"; import { CompletionState, GameboardDTO } from "../../../../IsaacApiTypes"; -import { above, below, isAda, isDefined, isPhy, isStaff, isTeacherOrAbove, siteSpecific, Subject, useDeviceSize } from "../../../services"; +import { above, below, isAda, isDefined, isLoggedIn, isPhy, isStaff, isTeacherOrAbove, siteSpecific, Subject, useDeviceSize } from "../../../services"; import { TitleIcon, TitleIconProps } from "../PageTitle"; import { Markup } from "../markup"; import { closeActiveModal, openActiveModal, selectors, useAppDispatch, useAppSelector, useLazyGetGroupsQuery, useLazyGetMySetAssignmentsQuery, useUnassignGameboardMutation } from "../../../state"; @@ -16,6 +16,8 @@ import { ContentPropertyTags } from "../ContentPropertyTags"; import { LLMFreeTextQuestionIndicator } from "../LLMFreeTextQuestionIndicator"; import { CrossTopicQuestionIndicator } from "../CrossTopicQuestionIndicator"; import { SupersededDeprecatedBoardContentWarning } from "../../navigation/SupersededDeprecatedWarning"; +import { useBookmarks } from "../../../services/bookmarks"; +import { FeatureFlag, useFeatureFlag } from "../../../services/featureFlag"; const Breadcrumb = ({breadcrumb}: {breadcrumb: string[]}) => { return <> @@ -134,6 +136,7 @@ type ALVIType = { quizTag?: string; // this is for quick quizzes only, which are currently just gameboards; may change in future hasCaret?: boolean; linkTags?: ListViewTagProps[]; + allowBookmarking?: boolean; // if set, displays a bookmark for logged-in users that will save the alvi to the user's bookmarks on click } | { // quizzes – have exclusive "preview" and "view test" buttons alviType: "quiz"; @@ -180,8 +183,13 @@ export type AbstractListViewProps = ALVILayout & { export const AbstractListViewItem = ({title, icon, subject, subtitle, breadcrumb, tags, style, url, state, className, componentTag, ...typedProps}: AbstractListViewItemProps & AbstractListViewProps) => { const deviceSize = useDeviceSize(); + const { isBookmarked, bookmarkItem } = useBookmarks(); const user = useAppSelector(selectors.user.orNull); + const bookmarksFeatureFlag = useFeatureFlag(FeatureFlag.ENABLE_SCI_BOOKMARKS); + + const contentId = (url?.includes("/questions/") || url?.includes("/concepts/")) && url.split("/").slice(-1)[0]; + const isItem = typedProps.alviType === "item"; const isGameboard = typedProps.alviType === "gameboard"; const isQuiz = typedProps.alviType === "quiz"; @@ -273,6 +281,10 @@ export const AbstractListViewItem = ({title, icon, subject, subtitle, breadcrumb {isQuiz && } + {isItem && contentId && typedProps.allowBookmarking && isLoggedIn(user) && bookmarksFeatureFlag &&