Skip to content
Open
Show file tree
Hide file tree
Changes from all 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
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
"use client"

import React from "react"
import { useArticleDetailRetrieve } from "api/hooks/articles"
import { LoadingSpinner, styled } from "ol-components"
import { ArticleEditor } from "@/page-components/TiptapEditor/contentTypes/article/ArticleEditor"
import { LearningResourceProvider } from "@/page-components/TiptapEditor/extensions/node/LearningResource/LearningResourceDataProvider"
import { notFound } from "next/navigation"

const PageContainer = styled.div({
display: "flex",
height: "100%",
})

const Spinner = styled(LoadingSpinner)({
margin: "auto",
position: "absolute",
top: "40%",
left: "50%",
transform: "translate(-50%, -50%)",
})

const UserArticleDetailPage = ({
articleId,
learningResourceIds = [],
}: {
articleId: string
learningResourceIds?: number[]
}) => {
const { data: article, isLoading } = useArticleDetailRetrieve(articleId)

if (isLoading) {
return (
<LearningResourceProvider resourceIds={learningResourceIds}>
<Spinner color="inherit" loading size={32} />
</LearningResourceProvider>
)
}
if (!article) {
return notFound()
}

return (
<PageContainer>
<LearningResourceProvider resourceIds={learningResourceIds}>
<ArticleEditor article={article} readOnly />
</LearningResourceProvider>
</PageContainer>
)
}

export { UserArticleDetailPage }
Original file line number Diff line number Diff line change
@@ -0,0 +1,182 @@
"use client"

import React, { useState, useRef, useEffect } from "react"
import {
Container,
styled,
theme,
Grid2,
Card,
Pagination,
PaginationItem,
LoadingSpinner,
Typography,
} from "ol-components"
import { Permission } from "api/hooks/user"
import { useArticleList } from "api/hooks/articles"
import type { WebsiteContent } from "api/v1"
import { LocalDate } from "ol-utilities"
import { RiArrowLeftLine, RiArrowRightLine } from "@remixicon/react"
import { extractFirstImageFromArticle } from "@/common/articleUtils"
import { userArticlesDraftView, userArticlesView } from "@/common/urls"
import RestrictedRoute from "@/components/RestrictedRoute/RestrictedRoute"

const PAGE_SIZE = 20

export const DEFAULT_BACKGROUND_IMAGE_URL =
"/images/backgrounds/banner_background.webp"

const PageWrapper = styled.div`
background: ${theme.custom.colors.white};
min-height: calc(100vh - 200px);
padding: 80px 0;
${theme.breakpoints.down("md")} {
padding: 40px 0;
}
`

const DraftArticleCard = styled(Card)`
display: flex;
flex-direction: column;
height: 100%;
`

const PaginationContainer = styled.div`
display: flex;
justify-content: center;
margin-top: 40px;
`

const LoadingContainer = styled.div`
display: flex;
justify-content: center;
align-items: center;
min-height: 400px;
`

const EmptyState = styled.div`
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
min-height: 400px;
gap: 16px;
`

const DraftBadge = styled.span`
color: ${theme.custom.colors.silverGrayDark};
font-weight: ${theme.typography.fontWeightMedium};
`

const DraftUserArticle: React.FC<{ article: WebsiteContent }> = ({
article,
}) => {
const articleUrl = article.is_published
? userArticlesView(article.slug || String(article.id))
: userArticlesDraftView(String(article.id))

const imageUrl = extractFirstImageFromArticle(article.content)

return (
<DraftArticleCard forwardClicksToLink>
{
<Card.Image
src={imageUrl || DEFAULT_BACKGROUND_IMAGE_URL}
alt={article.title}
/>
}
<Card.Title href={articleUrl} lines={2} style={{ marginBottom: "-13px" }}>
{article.title}
</Card.Title>
<Card.Footer>
<LocalDate date={article.created_on} />
{!article.is_published && (
<>
{" • "}
<DraftBadge>Draft</DraftBadge>
</>
)}
</Card.Footer>
</DraftArticleCard>
)
}

const UserArticleDraftPage: React.FC = () => {
const [page, setPage] = useState(1)
const scrollRef = useRef<HTMLDivElement>(null)

const { data: articles, isLoading: isLoadingArticles } = useArticleList({
limit: PAGE_SIZE,
offset: (page - 1) * PAGE_SIZE,
draft: true,
})

useEffect(() => {
if (page > 1 && scrollRef.current) {
scrollRef.current.scrollIntoView({ behavior: "smooth", block: "start" })
}
}, [page])

const draftArticles = articles?.results
const totalPages = articles?.count ? Math.ceil(articles.count / PAGE_SIZE) : 0

if (isLoadingArticles) {
return <LoadingSpinner loading={isLoadingArticles} />
}

return (
<RestrictedRoute requires={Permission.ArticleEditor}>
<PageWrapper ref={scrollRef}>
<Container>
{isLoadingArticles ? (
<LoadingContainer>
<LoadingSpinner loading size={48} />
</LoadingContainer>
) : draftArticles && draftArticles.length > 0 ? (
<>
<Grid2 container columnSpacing="24px" rowSpacing="28px">
{draftArticles.map((article) => (
<Grid2
key={article.id}
size={{ xs: 12, sm: 6, md: 4, lg: 3, xl: 3 }}
>
<DraftUserArticle article={article} />
</Grid2>
))}
</Grid2>

{totalPages > 1 && (
<PaginationContainer>
<Pagination
count={totalPages}
page={page}
onChange={(_, newPage) => setPage(newPage)}
renderItem={(item) => (
<PaginationItem
slots={{
previous: RiArrowLeftLine,
next: RiArrowRightLine,
}}
{...item}
/>
)}
/>
</PaginationContainer>
)}
</>
) : (
<EmptyState>
<Typography variant="h4">No Draft Articles</Typography>
<Typography variant="body1" color="textSecondary">
You don&apos;t have any draft articles yet. Create a new article
to get started.
</Typography>
</EmptyState>
)}
</Container>
</PageWrapper>
</RestrictedRoute>
)
}

export { UserArticleDraftPage }
62 changes: 62 additions & 0 deletions frontends/main/src/app-pages/UserArticles/UserArticleEditPage.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
"use client"

import React from "react"
import { useRouter } from "next-nprogress-bar"
import { notFound } from "next/navigation"
import { Permission } from "api/hooks/user"
import { useArticleDetailRetrieve } from "api/hooks/articles"
import RestrictedRoute from "@/components/RestrictedRoute/RestrictedRoute"
import { styled, LoadingSpinner } from "ol-components"
import { ArticleEditor } from "@/page-components/TiptapEditor/contentTypes/article/ArticleEditor"
import { userArticlesDraftView, userArticlesView } from "@/common/urls"
import invariant from "tiny-invariant"

const PageContainer = styled.div(({ theme }) => ({
color: theme.custom.colors.darkGray2,
display: "flex",
height: "100%",
}))

const Spinner = styled(LoadingSpinner)({
margin: "auto",
position: "absolute",
top: "40%",
left: "50%",
transform: "translate(-50%, -50%)",
})

const UserArticleEditPage = ({ articleId }: { articleId: string }) => {
const {
data: article,
isLoading,
isFetching,
} = useArticleDetailRetrieve(articleId)
const router = useRouter()

if (isLoading || isFetching) {
return <Spinner color="inherit" loading={isLoading} size={32} />
}
if (!article) {
return notFound()
}

return (
<RestrictedRoute requires={Permission.ArticleEditor}>
<PageContainer>
<ArticleEditor
article={article}
onSave={(saved) => {
if (saved.is_published) {
invariant(saved.slug, "Published article must have a slug")
return router.push(userArticlesView(saved.slug))
} else {
router.push(userArticlesDraftView(String(saved.id)))
}
}}
/>
</PageContainer>
</RestrictedRoute>
)
}

export { UserArticleEditPage }
Loading
Loading