Convert content group to folders
This commit is contained in:
parent
82c605086b
commit
6a3410d251
|
@ -30,6 +30,11 @@ module.exports = {
|
|||
destination: "https://gallery.accords-library.com/posts",
|
||||
permanent: false,
|
||||
},
|
||||
{
|
||||
source: "/contents/folder",
|
||||
destination: "/contents",
|
||||
permanent: false,
|
||||
},
|
||||
];
|
||||
},
|
||||
};
|
||||
|
|
|
@ -25,7 +25,7 @@ import { cIf, cJoin } from "helpers/className";
|
|||
import { AppStaticProps } from "graphql/getAppStaticProps";
|
||||
import { useAppLayout } from "contexts/AppLayoutContext";
|
||||
import { Button } from "components/Inputs/Button";
|
||||
import { OpenGraph } from "helpers/openGraph";
|
||||
import { OpenGraph, TITLE_PREFIX } from "helpers/openGraph";
|
||||
import { getDefaultPreferredLanguages } from "helpers/locales";
|
||||
import useIsClient from "hooks/useIsClient";
|
||||
import { useBoolean } from "hooks/useBoolean";
|
||||
|
@ -359,7 +359,11 @@ export const AppLayout = ({
|
|||
)
|
||||
)}
|
||||
>
|
||||
{openGraph.title}
|
||||
{isDefinedAndNotEmpty(
|
||||
openGraph.title.substring(TITLE_PREFIX.length)
|
||||
)
|
||||
? openGraph.title.substring(TITLE_PREFIX.length)
|
||||
: "Accord’s Library"}
|
||||
</p>
|
||||
{isDefined(subPanel) && !turnSubIntoContent && (
|
||||
<Ico
|
||||
|
|
|
@ -21,6 +21,7 @@ interface Props {
|
|||
onClick?: MouseEventHandler<HTMLDivElement>;
|
||||
draggable?: boolean;
|
||||
badgeNumber?: number;
|
||||
disabled?: boolean;
|
||||
size?: "normal" | "small";
|
||||
}
|
||||
|
||||
|
@ -37,6 +38,7 @@ export const Button = ({
|
|||
href,
|
||||
alwaysNewTab = false,
|
||||
badgeNumber,
|
||||
disabled,
|
||||
size = "normal",
|
||||
}: Props): JSX.Element => (
|
||||
<ConditionalWrapper
|
||||
|
@ -49,6 +51,7 @@ export const Button = ({
|
|||
draggable={draggable}
|
||||
id={id}
|
||||
onClick={onClick}
|
||||
onFocus={(event) => event.target.blur()}
|
||||
className={cJoin(
|
||||
`group grid cursor-pointer select-none grid-flow-col place-content-center
|
||||
place-items-center gap-2 rounded-full border-[1px] border-dark py-3 px-4
|
||||
|
@ -60,6 +63,7 @@ export const Button = ({
|
|||
active:hover:bg-black active:hover:!text-light active:hover:drop-shadow-black-lg`
|
||||
),
|
||||
cIf(size === "small", "px-3 py-1 text-xs"),
|
||||
cIf(disabled, "cursor-not-allowed"),
|
||||
className
|
||||
)}
|
||||
>
|
||||
|
|
|
@ -36,7 +36,7 @@ export const Link = ({
|
|||
} else if (isValidClick && href) {
|
||||
if (event.button !== MouseButton.Right) {
|
||||
if (alwaysNewTab) {
|
||||
window.open(href, "_blank");
|
||||
window.open(href, "_blank", "noopener");
|
||||
} else if (event.button === MouseButton.Left) {
|
||||
if (href.startsWith("#")) {
|
||||
router.replace(href);
|
||||
|
|
|
@ -10,6 +10,7 @@ import { cJoin } from "helpers/className";
|
|||
interface Props {
|
||||
className?: string;
|
||||
page: number;
|
||||
pagesCount: number;
|
||||
onChange: (value: number) => void;
|
||||
}
|
||||
|
||||
|
@ -18,14 +19,33 @@ interface Props {
|
|||
export const PageSelector = ({
|
||||
page,
|
||||
className,
|
||||
pagesCount,
|
||||
onChange,
|
||||
}: Props): JSX.Element => (
|
||||
<ButtonGroup
|
||||
className={cJoin("flex flex-row place-content-center", className)}
|
||||
buttonsProps={[
|
||||
{ onClick: () => onChange(page - 1), icon: Icon.NavigateBefore },
|
||||
{ text: (page + 1).toString() },
|
||||
{ onClick: () => onChange(page + 1), icon: Icon.NavigateNext },
|
||||
{
|
||||
onClick: () => onChange(0),
|
||||
disabled: page === 0,
|
||||
icon: Icon.FirstPage,
|
||||
},
|
||||
{
|
||||
onClick: () => page > 0 && onChange(page - 1),
|
||||
disabled: page === 0,
|
||||
icon: Icon.NavigateBefore,
|
||||
},
|
||||
{ text: `${page + 1} / ${pagesCount}` },
|
||||
{
|
||||
onClick: () => page < pagesCount - 1 && onChange(page + 1),
|
||||
disabled: page === pagesCount - 1,
|
||||
icon: Icon.NavigateNext,
|
||||
},
|
||||
{
|
||||
onClick: () => onChange(pagesCount - 1),
|
||||
disabled: page === pagesCount - 1,
|
||||
icon: Icon.LastPage,
|
||||
},
|
||||
]}
|
||||
/>
|
||||
);
|
||||
|
|
|
@ -4,9 +4,22 @@ import { PageSelector } from "./Inputs/PageSelector";
|
|||
import { Ico, Icon } from "./Ico";
|
||||
import { AppStaticProps } from "graphql/getAppStaticProps";
|
||||
import { cJoin } from "helpers/className";
|
||||
import { isDefined, isDefinedAndNotEmpty, iterateMap } from "helpers/others";
|
||||
import { isDefined, isDefinedAndNotEmpty } from "helpers/others";
|
||||
import { AnchorIds, useScrollTopOnChange } from "hooks/useScrollTopOnChange";
|
||||
|
||||
interface Group<T> {
|
||||
name: string;
|
||||
items: T[];
|
||||
totalCount: number;
|
||||
}
|
||||
|
||||
const defaultGroupSortingFunction = <T,>(a: Group<T>, b: Group<T>) =>
|
||||
a.name.localeCompare(b.name);
|
||||
const defaultGroupCountingFunction = () => 1;
|
||||
const defaultFilteringFunction = () => true;
|
||||
const defaultSortingFunction = () => 0;
|
||||
const defaultGroupingFunction = () => [""];
|
||||
|
||||
interface Props<T> {
|
||||
// Items
|
||||
items: T[];
|
||||
|
@ -24,7 +37,7 @@ interface Props<T> {
|
|||
searchingCaseInsensitive?: boolean;
|
||||
// Grouping
|
||||
groupingFunction?: (item: T) => string[];
|
||||
groupSortingFunction?: (a: string, b: string) => number;
|
||||
groupSortingFunction?: (a: Group<T>, b: Group<T>) => number;
|
||||
groupCountingFunction?: (item: T) => number;
|
||||
// Filtering
|
||||
filteringFunction?: (item: T) => boolean;
|
||||
|
@ -40,28 +53,28 @@ export const SmartList = <T,>({
|
|||
getItemId,
|
||||
renderItem: RenderItem,
|
||||
renderWhenEmpty: RenderWhenEmpty,
|
||||
paginationItemPerPage = 0,
|
||||
paginationItemPerPage = Infinity,
|
||||
paginationSelectorTop = true,
|
||||
paginationSelectorBottom = true,
|
||||
paginationScroolTop = true,
|
||||
searchingTerm,
|
||||
searchingBy,
|
||||
searchingCaseInsensitive = true,
|
||||
groupingFunction = () => [""],
|
||||
groupSortingFunction = (a, b) => a.localeCompare(b),
|
||||
groupCountingFunction = () => 1,
|
||||
filteringFunction = () => true,
|
||||
sortingFunction = () => 0,
|
||||
groupingFunction = defaultGroupingFunction,
|
||||
groupSortingFunction = defaultGroupSortingFunction,
|
||||
groupCountingFunction = defaultGroupCountingFunction,
|
||||
filteringFunction = defaultFilteringFunction,
|
||||
sortingFunction = defaultSortingFunction,
|
||||
className,
|
||||
langui,
|
||||
}: Props<T>): JSX.Element => {
|
||||
const [page, setPage] = useState(0);
|
||||
|
||||
useScrollTopOnChange(AnchorIds.ContentPanel, [page], paginationScroolTop);
|
||||
|
||||
type Group = Map<string, T[]>;
|
||||
|
||||
useEffect(() => setPage(0), [searchingTerm]);
|
||||
useEffect(
|
||||
() => setPage(0),
|
||||
[searchingTerm, groupingFunction, groupSortingFunction, items]
|
||||
);
|
||||
|
||||
const searchFilter = useCallback(() => {
|
||||
if (isDefinedAndNotEmpty(searchingTerm) && isDefined(searchingBy)) {
|
||||
|
@ -85,87 +98,104 @@ export const SmartList = <T,>({
|
|||
[filteredItems, sortingFunction]
|
||||
);
|
||||
|
||||
const paginatedItems = useMemo(() => {
|
||||
if (paginationItemPerPage > 0) {
|
||||
const memo = [];
|
||||
for (
|
||||
let index = 0;
|
||||
paginationItemPerPage * index < sortedItem.length;
|
||||
index++
|
||||
) {
|
||||
memo.push(
|
||||
sortedItem.slice(
|
||||
index * paginationItemPerPage,
|
||||
(index + 1) * paginationItemPerPage
|
||||
)
|
||||
);
|
||||
}
|
||||
return memo;
|
||||
}
|
||||
return [sortedItem];
|
||||
}, [paginationItemPerPage, sortedItem]);
|
||||
const groups = useMemo(() => {
|
||||
const memo: Group<T>[] = [];
|
||||
|
||||
const groupedList = useMemo(() => {
|
||||
const groups: Group = new Map();
|
||||
paginatedItems[page]?.forEach((item) => {
|
||||
sortedItem.forEach((item) => {
|
||||
groupingFunction(item).forEach((category) => {
|
||||
if (groups.has(category)) {
|
||||
groups.get(category)?.push(item);
|
||||
const index = memo.findIndex((group) => group.name === category);
|
||||
if (index === -1) {
|
||||
memo.push({
|
||||
name: category,
|
||||
items: [item],
|
||||
totalCount: groupCountingFunction(item),
|
||||
});
|
||||
} else {
|
||||
groups.set(category, [item]);
|
||||
memo[index].items.push(item);
|
||||
memo[index].totalCount += groupCountingFunction(item);
|
||||
}
|
||||
});
|
||||
});
|
||||
return groups;
|
||||
}, [groupingFunction, page, paginatedItems]);
|
||||
return memo.sort(groupSortingFunction);
|
||||
}, [
|
||||
groupCountingFunction,
|
||||
groupSortingFunction,
|
||||
groupingFunction,
|
||||
sortedItem,
|
||||
]);
|
||||
|
||||
const pageCount = useMemo(
|
||||
() =>
|
||||
paginationItemPerPage > 0
|
||||
? Math.floor(filteredItems.length / paginationItemPerPage)
|
||||
: 1,
|
||||
[paginationItemPerPage, filteredItems.length]
|
||||
);
|
||||
const pages = useMemo(() => {
|
||||
const memo: Group<T>[][] = [];
|
||||
let currentPage: Group<T>[] = [];
|
||||
let remainingSlots = paginationItemPerPage;
|
||||
let loopSafeguard = 1000;
|
||||
|
||||
const changePage = useCallback(
|
||||
(newPage: number) =>
|
||||
setPage(() => {
|
||||
if (newPage <= 0) {
|
||||
return 0;
|
||||
const newPage = () => {
|
||||
memo.push(currentPage);
|
||||
currentPage = [];
|
||||
remainingSlots = paginationItemPerPage;
|
||||
};
|
||||
|
||||
for (const group of groups) {
|
||||
let remainingItems = group.items.length;
|
||||
while (remainingItems > 0 && loopSafeguard >= 0) {
|
||||
loopSafeguard--;
|
||||
const currentIndex = group.items.length - remainingItems;
|
||||
|
||||
if (
|
||||
remainingSlots <= 0 ||
|
||||
(currentIndex === 0 &&
|
||||
remainingItems > remainingSlots &&
|
||||
remainingItems <= paginationItemPerPage)
|
||||
) {
|
||||
newPage();
|
||||
}
|
||||
if (newPage >= pageCount) {
|
||||
return pageCount;
|
||||
}
|
||||
return newPage;
|
||||
}),
|
||||
[pageCount]
|
||||
);
|
||||
|
||||
const slicedGroup: Group<T> = {
|
||||
name: group.name,
|
||||
items: group.items.slice(currentIndex, currentIndex + remainingSlots),
|
||||
totalCount: group.totalCount,
|
||||
};
|
||||
|
||||
remainingItems -= slicedGroup.items.length;
|
||||
remainingSlots -= slicedGroup.items.length;
|
||||
currentPage.push(slicedGroup);
|
||||
}
|
||||
}
|
||||
|
||||
if (currentPage.length > 0) {
|
||||
newPage();
|
||||
}
|
||||
|
||||
return memo;
|
||||
}, [groups, paginationItemPerPage]);
|
||||
|
||||
return (
|
||||
<>
|
||||
{pageCount > 1 && paginationSelectorTop && (
|
||||
<PageSelector className="mb-12" page={page} onChange={changePage} />
|
||||
{pages.length > 1 && paginationSelectorTop && (
|
||||
<PageSelector
|
||||
className="mb-12"
|
||||
page={page}
|
||||
pagesCount={pages.length}
|
||||
onChange={setPage}
|
||||
/>
|
||||
)}
|
||||
|
||||
<div className="mb-8">
|
||||
{groupedList.size > 0 ? (
|
||||
iterateMap(
|
||||
groupedList,
|
||||
(name, groupItems) =>
|
||||
groupItems.length > 0 && (
|
||||
<Fragment key={name}>
|
||||
{name.length > 0 && (
|
||||
{pages[page]?.length > 0 ? (
|
||||
pages[page]?.map(
|
||||
(group) =>
|
||||
group.items.length > 0 && (
|
||||
<Fragment key={group.name}>
|
||||
{group.name.length > 0 && (
|
||||
<h2
|
||||
className="flex flex-row place-items-center gap-2 pb-2 pt-10 text-2xl
|
||||
first-of-type:pt-0"
|
||||
first-of-type:pt-0"
|
||||
>
|
||||
{name}
|
||||
{group.name}
|
||||
<Chip
|
||||
text={`${groupItems.reduce(
|
||||
(acc, item) => acc + groupCountingFunction(item),
|
||||
0
|
||||
)} ${
|
||||
groupItems.length <= 1
|
||||
text={`${group.totalCount} ${
|
||||
group.items.length <= 1
|
||||
? langui.result?.toLowerCase() ?? ""
|
||||
: langui.results?.toLowerCase() ?? ""
|
||||
}`}
|
||||
|
@ -179,13 +209,12 @@ export const SmartList = <T,>({
|
|||
className
|
||||
)}
|
||||
>
|
||||
{groupItems.map((item) => (
|
||||
{group.items.map((item) => (
|
||||
<RenderItem item={item} key={getItemId(item)} />
|
||||
))}
|
||||
</div>
|
||||
</Fragment>
|
||||
),
|
||||
([a], [b]) => groupSortingFunction(a, b)
|
||||
)
|
||||
)
|
||||
) : isDefined(RenderWhenEmpty) ? (
|
||||
<RenderWhenEmpty />
|
||||
|
@ -194,8 +223,13 @@ export const SmartList = <T,>({
|
|||
)}
|
||||
</div>
|
||||
|
||||
{pageCount > 1 && paginationSelectorBottom && (
|
||||
<PageSelector className="mb-12" page={page} onChange={changePage} />
|
||||
{pages.length > 1 && paginationSelectorBottom && (
|
||||
<PageSelector
|
||||
className="mb-12"
|
||||
page={page}
|
||||
pagesCount={pages.length}
|
||||
onChange={setPage}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
|
@ -217,7 +251,7 @@ const DefaultRenderWhenEmpty = ({ langui }: DefaultRenderWhenEmptyProps) => (
|
|||
border-dark p-8 text-dark opacity-40"
|
||||
>
|
||||
<Ico icon={Icon.ChevronLeft} className="!text-[300%] mobile:hidden" />
|
||||
<p className="max-w-xs text-2xl"> {langui.no_results_message} </p>
|
||||
<p className="max-w-xs text-2xl">{langui.no_results_message}</p>
|
||||
<Ico icon={Icon.ChevronRight} className="!text-[300%] desktop:hidden" />
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -4,7 +4,9 @@ import { ScanSet } from "./Library/ScanSet";
|
|||
import { NavOption } from "./PanelComponents/NavOption";
|
||||
import { ChroniclePreview } from "./Chronicles/ChroniclePreview";
|
||||
import { ChroniclesList } from "./Chronicles/ChroniclesList";
|
||||
import { Button } from "./Inputs/Button";
|
||||
import { useSmartLanguage } from "hooks/useSmartLanguage";
|
||||
import { PreviewFolder } from "pages/contents/folder/[slug]";
|
||||
|
||||
export type TranslatedProps<P, K extends keyof P> = Omit<P, K> & {
|
||||
translations: (Pick<P, K> & { language: string })[];
|
||||
|
@ -158,3 +160,43 @@ export const TranslatedChroniclesList = ({
|
|||
/>
|
||||
);
|
||||
};
|
||||
|
||||
// ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─
|
||||
|
||||
export const TranslatedButton = ({
|
||||
translations,
|
||||
fallback,
|
||||
...otherProps
|
||||
}: TranslatedProps<Parameters<typeof Button>[0], "text">): JSX.Element => {
|
||||
const [selectedTranslation] = useSmartLanguage({
|
||||
items: translations,
|
||||
languageExtractor,
|
||||
});
|
||||
|
||||
return (
|
||||
<Button text={selectedTranslation?.text ?? fallback.text} {...otherProps} />
|
||||
);
|
||||
};
|
||||
|
||||
// ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─
|
||||
|
||||
export const TranslatedPreviewFolder = ({
|
||||
translations,
|
||||
fallback,
|
||||
...otherProps
|
||||
}: TranslatedProps<
|
||||
Parameters<typeof PreviewFolder>[0],
|
||||
"title"
|
||||
>): JSX.Element => {
|
||||
const [selectedTranslation] = useSmartLanguage({
|
||||
items: translations,
|
||||
languageExtractor,
|
||||
});
|
||||
|
||||
return (
|
||||
<PreviewFolder
|
||||
title={selectedTranslation?.title ?? fallback.title}
|
||||
{...otherProps}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -214,9 +214,22 @@ query getContentText($slug: String, $language_code: String) {
|
|||
}
|
||||
}
|
||||
}
|
||||
group {
|
||||
folders(pagination: { limit: -1 }) {
|
||||
data {
|
||||
id
|
||||
attributes {
|
||||
slug
|
||||
titles(pagination: { limit: -1 }) {
|
||||
language {
|
||||
data {
|
||||
attributes {
|
||||
code
|
||||
}
|
||||
}
|
||||
}
|
||||
title
|
||||
}
|
||||
sequence
|
||||
contents(pagination: { limit: -1 }) {
|
||||
data {
|
||||
attributes {
|
||||
|
|
|
@ -62,18 +62,6 @@ query getContents($language_code: String) {
|
|||
}
|
||||
}
|
||||
}
|
||||
group {
|
||||
data {
|
||||
attributes {
|
||||
combine
|
||||
contents(pagination: { limit: -1 }) {
|
||||
data {
|
||||
id
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
thumbnail {
|
||||
data {
|
||||
attributes {
|
||||
|
|
|
@ -0,0 +1,132 @@
|
|||
query getContentsFolder($slug: String, $language_code: String) {
|
||||
contentsFolders(filters: { slug: { eq: $slug } }) {
|
||||
data {
|
||||
attributes {
|
||||
slug
|
||||
titles(pagination: { limit: -1 }) {
|
||||
id
|
||||
language {
|
||||
data {
|
||||
attributes {
|
||||
code
|
||||
}
|
||||
}
|
||||
}
|
||||
title
|
||||
}
|
||||
parent_folder {
|
||||
data {
|
||||
attributes {
|
||||
slug
|
||||
titles(pagination: { limit: -1 }) {
|
||||
id
|
||||
language {
|
||||
data {
|
||||
attributes {
|
||||
code
|
||||
}
|
||||
}
|
||||
}
|
||||
title
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
contents(pagination: { limit: -1 }) {
|
||||
data {
|
||||
id
|
||||
attributes {
|
||||
slug
|
||||
translations(pagination: { limit: -1 }) {
|
||||
pre_title
|
||||
title
|
||||
subtitle
|
||||
language {
|
||||
data {
|
||||
attributes {
|
||||
code
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
categories(pagination: { limit: -1 }) {
|
||||
data {
|
||||
id
|
||||
attributes {
|
||||
name
|
||||
short
|
||||
}
|
||||
}
|
||||
}
|
||||
type {
|
||||
data {
|
||||
attributes {
|
||||
slug
|
||||
titles(
|
||||
filters: { language: { code: { eq: $language_code } } }
|
||||
) {
|
||||
title
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
ranged_contents(pagination: { limit: -1 }) {
|
||||
data {
|
||||
id
|
||||
attributes {
|
||||
slug
|
||||
scan_set {
|
||||
id
|
||||
}
|
||||
library_item {
|
||||
data {
|
||||
attributes {
|
||||
slug
|
||||
title
|
||||
subtitle
|
||||
thumbnail {
|
||||
data {
|
||||
attributes {
|
||||
...uploadImage
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
thumbnail {
|
||||
data {
|
||||
attributes {
|
||||
...uploadImage
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
subfolders(pagination: { limit: -1 }) {
|
||||
data {
|
||||
id
|
||||
attributes {
|
||||
slug
|
||||
titles(pagination: { limit: -1 }) {
|
||||
id
|
||||
language {
|
||||
data {
|
||||
attributes {
|
||||
code
|
||||
}
|
||||
}
|
||||
}
|
||||
title
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,9 @@
|
|||
query getContentsFoldersSlugs {
|
||||
contentsFolders(pagination: { limit: -1 }) {
|
||||
data {
|
||||
attributes {
|
||||
slug
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -170,6 +170,8 @@ query getWebsiteInterface($language_code: String) {
|
|||
no_source_warning
|
||||
copy_anchor_link
|
||||
anchor_link_copied
|
||||
folders
|
||||
empty_folder_message
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -15,7 +15,7 @@ const DEFAULT_OG_THUMBNAIL = {
|
|||
alt: "Accord's Library Logo",
|
||||
};
|
||||
|
||||
const TITLE_PREFIX = "Accord’s Library";
|
||||
export const TITLE_PREFIX = "Accord’s Library - ";
|
||||
|
||||
export interface OpenGraph {
|
||||
title: string;
|
||||
|
@ -29,7 +29,7 @@ export const getOpenGraph = (
|
|||
description?: string | null | undefined,
|
||||
thumbnail?: UploadImageFragment | null | undefined
|
||||
): OpenGraph => ({
|
||||
title: `${TITLE_PREFIX}${isDefinedAndNotEmpty(title) ? ` - ${title}` : ""}`,
|
||||
title: `${TITLE_PREFIX}${isDefinedAndNotEmpty(title) ? `${title}` : ""}`,
|
||||
description: isDefinedAndNotEmpty(description)
|
||||
? description
|
||||
: langui.default_description ?? "",
|
||||
|
|
|
@ -99,33 +99,31 @@ const Videos = ({ langui, videos, ...otherProps }: Props): JSX.Element => {
|
|||
items={filterHasAttributes(videos, ["id", "attributes"] as const)}
|
||||
getItemId={(item) => item.id}
|
||||
renderItem={({ item }) => (
|
||||
<>
|
||||
<PreviewCard
|
||||
href={`/archives/videos/v/${item.attributes.uid}`}
|
||||
title={item.attributes.title}
|
||||
thumbnail={getVideoThumbnailURL(item.attributes.uid)}
|
||||
thumbnailAspectRatio="16/9"
|
||||
thumbnailForceAspectRatio
|
||||
keepInfoVisible={keepInfoVisible}
|
||||
metadata={{
|
||||
releaseDate: item.attributes.published_date,
|
||||
views: item.attributes.views,
|
||||
author: item.attributes.channel?.data?.attributes?.title,
|
||||
position: "Top",
|
||||
}}
|
||||
hoverlay={{
|
||||
__typename: "Video",
|
||||
duration: item.attributes.duration,
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
<PreviewCard
|
||||
href={`/archives/videos/v/${item.attributes.uid}`}
|
||||
title={item.attributes.title}
|
||||
thumbnail={getVideoThumbnailURL(item.attributes.uid)}
|
||||
thumbnailAspectRatio="16/9"
|
||||
thumbnailForceAspectRatio
|
||||
keepInfoVisible={keepInfoVisible}
|
||||
metadata={{
|
||||
releaseDate: item.attributes.published_date,
|
||||
views: item.attributes.views,
|
||||
author: item.attributes.channel?.data?.attributes?.title,
|
||||
position: "Top",
|
||||
}}
|
||||
hoverlay={{
|
||||
__typename: "Video",
|
||||
duration: item.attributes.duration,
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
langui={langui}
|
||||
className="desktop:grid-cols-[repeat(auto-fill,_minmax(15rem,1fr))] mobile:grid-cols-2
|
||||
thin:grid-cols-1"
|
||||
paginationItemPerPage={20}
|
||||
paginationItemPerPage={25}
|
||||
searchingTerm={searchName}
|
||||
searchingBy={(item) => item.attributes.title}
|
||||
langui={langui}
|
||||
/>
|
||||
</ContentPanel>
|
||||
),
|
||||
|
|
|
@ -33,7 +33,10 @@ import { ContentWithTranslations } from "helpers/types";
|
|||
import { useMediaMobile } from "hooks/useMediaQuery";
|
||||
import { AnchorIds, useScrollTopOnChange } from "hooks/useScrollTopOnChange";
|
||||
import { useSmartLanguage } from "hooks/useSmartLanguage";
|
||||
import { TranslatedPreviewLine } from "components/Translated";
|
||||
import {
|
||||
TranslatedPreviewFolder,
|
||||
TranslatedPreviewLine,
|
||||
} from "components/Translated";
|
||||
import { getOpenGraph } from "helpers/openGraph";
|
||||
import {
|
||||
getDefaultPreferredLanguages,
|
||||
|
@ -74,32 +77,51 @@ const Content = ({
|
|||
|
||||
const { previousContent, nextContent } = useMemo(
|
||||
() => ({
|
||||
previousContent: content.group?.data?.attributes?.contents
|
||||
? getPreviousContent(
|
||||
content.group.data.attributes.contents.data,
|
||||
content.slug
|
||||
)
|
||||
: undefined,
|
||||
nextContent: content.group?.data?.attributes?.contents
|
||||
? getNextContent(
|
||||
content.group.data.attributes.contents.data,
|
||||
content.slug
|
||||
)
|
||||
: undefined,
|
||||
previousContent:
|
||||
content.folders?.data[0]?.attributes?.contents &&
|
||||
content.folders.data[0].attributes.sequence
|
||||
? getPreviousContent(
|
||||
content.folders.data[0].attributes.contents.data,
|
||||
content.slug
|
||||
)
|
||||
: undefined,
|
||||
nextContent:
|
||||
content.folders?.data[0]?.attributes?.contents &&
|
||||
content.folders.data[0].attributes.sequence
|
||||
? getNextContent(
|
||||
content.folders.data[0].attributes.contents.data,
|
||||
content.slug
|
||||
)
|
||||
: undefined,
|
||||
}),
|
||||
[content.group, content.slug]
|
||||
[content.folders, content.slug]
|
||||
);
|
||||
|
||||
const subPanel = useMemo(
|
||||
() => (
|
||||
<SubPanel>
|
||||
<ReturnButton
|
||||
href={`/contents`}
|
||||
title={langui.contents}
|
||||
langui={langui}
|
||||
displayOn={ReturnButtonType.Desktop}
|
||||
horizontalLine
|
||||
/>
|
||||
{content.folders?.data && content.folders.data.length > 0 && (
|
||||
<>
|
||||
<h2 className="mb-2 text-xl">{langui.folders}</h2>
|
||||
{filterHasAttributes(content.folders.data, [
|
||||
"attributes",
|
||||
"id",
|
||||
] as const).map((folder) => (
|
||||
<TranslatedPreviewFolder
|
||||
key={folder.id}
|
||||
href={`/contents/folder/${folder.attributes.slug}`}
|
||||
translations={filterHasAttributes(folder.attributes.titles, [
|
||||
"language.data.attributes.code",
|
||||
] as const).map((title) => ({
|
||||
language: title.language.data.attributes.code,
|
||||
title: title.title,
|
||||
}))}
|
||||
fallback={{ title: prettySlug(folder.attributes.slug) }}
|
||||
/>
|
||||
))}
|
||||
<HorizontalLine />
|
||||
</>
|
||||
)}
|
||||
|
||||
{selectedTranslation?.text_set?.source_language?.data?.attributes
|
||||
?.code !== undefined && (
|
||||
|
@ -308,6 +330,7 @@ const Content = ({
|
|||
</SubPanel>
|
||||
),
|
||||
[
|
||||
content.folders?.data,
|
||||
content.ranged_contents?.data,
|
||||
currencies,
|
||||
languages,
|
||||
|
@ -320,7 +343,7 @@ const Content = ({
|
|||
() => (
|
||||
<ContentPanel>
|
||||
<ReturnButton
|
||||
href={`/contents`}
|
||||
href={`/contents/folder`}
|
||||
title={langui.contents}
|
||||
langui={langui}
|
||||
displayOn={ReturnButtonType.Mobile}
|
||||
|
@ -579,19 +602,19 @@ export const getStaticPaths: GetStaticPaths = async (context) => {
|
|||
* ─────────────────────────────────────╯ PRIVATE METHODS ╰───────────────────────────────────────
|
||||
*/
|
||||
|
||||
type Group = NonNullable<
|
||||
type FolderContents = NonNullable<
|
||||
NonNullable<
|
||||
NonNullable<
|
||||
NonNullable<ContentWithTranslations["group"]>["data"]
|
||||
NonNullable<ContentWithTranslations["folders"]>["data"][number]
|
||||
>["attributes"]
|
||||
>["contents"]
|
||||
>["data"];
|
||||
|
||||
const getPreviousContent = (group: Group, currentSlug: string) => {
|
||||
for (let index = 0; index < group.length; index++) {
|
||||
const content = group[index];
|
||||
const getPreviousContent = (contents: FolderContents, currentSlug: string) => {
|
||||
for (let index = 0; index < contents.length; index++) {
|
||||
const content = contents[index];
|
||||
if (content.attributes?.slug === currentSlug && index > 0) {
|
||||
return group[index - 1];
|
||||
return contents[index - 1];
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
|
@ -599,11 +622,14 @@ const getPreviousContent = (group: Group, currentSlug: string) => {
|
|||
|
||||
// ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─
|
||||
|
||||
const getNextContent = (group: Group, currentSlug: string) => {
|
||||
for (let index = 0; index < group.length; index++) {
|
||||
const content = group[index];
|
||||
if (content.attributes?.slug === currentSlug && index < group.length - 1) {
|
||||
return group[index + 1];
|
||||
const getNextContent = (contents: FolderContents, currentSlug: string) => {
|
||||
for (let index = 0; index < contents.length; index++) {
|
||||
const content = contents[index];
|
||||
if (
|
||||
content.attributes?.slug === currentSlug &&
|
||||
index < contents.length - 1
|
||||
) {
|
||||
return contents[index + 1];
|
||||
}
|
||||
}
|
||||
return undefined;
|
|
@ -0,0 +1,315 @@
|
|||
import { GetStaticProps } from "next";
|
||||
import { useState, useMemo, useCallback } from "react";
|
||||
import { AppLayout, AppLayoutRequired } from "components/AppLayout";
|
||||
import { Select } from "components/Inputs/Select";
|
||||
import { Switch } from "components/Inputs/Switch";
|
||||
import { PanelHeader } from "components/PanelComponents/PanelHeader";
|
||||
import {
|
||||
ContentPanel,
|
||||
ContentPanelWidthSizes,
|
||||
} from "components/Panels/ContentPanel";
|
||||
import { SubPanel } from "components/Panels/SubPanel";
|
||||
import { AppStaticProps, getAppStaticProps } from "graphql/getAppStaticProps";
|
||||
import { getReadySdk } from "graphql/sdk";
|
||||
import { prettyInlineTitle, prettySlug } from "helpers/formatters";
|
||||
import { TextInput } from "components/Inputs/TextInput";
|
||||
import { WithLabel } from "components/Inputs/WithLabel";
|
||||
import { Button } from "components/Inputs/Button";
|
||||
import { useMediaHoverable } from "hooks/useMediaQuery";
|
||||
import { Icon } from "components/Ico";
|
||||
import { filterDefined, filterHasAttributes } from "helpers/others";
|
||||
import { GetContentsQuery } from "graphql/generated";
|
||||
import { SmartList } from "components/SmartList";
|
||||
import { SelectiveNonNullable } from "helpers/types/SelectiveNonNullable";
|
||||
import { useBoolean } from "hooks/useBoolean";
|
||||
import { TranslatedPreviewCard } from "components/Translated";
|
||||
import { getOpenGraph } from "helpers/openGraph";
|
||||
import { HorizontalLine } from "components/HorizontalLine";
|
||||
|
||||
/*
|
||||
* ╭─────────────╮
|
||||
* ────────────────────────────────────────╯ CONSTANTS ╰──────────────────────────────────────────
|
||||
*/
|
||||
|
||||
const DEFAULT_FILTERS_STATE = {
|
||||
groupingMethod: -1,
|
||||
keepInfoVisible: false,
|
||||
searchName: "",
|
||||
};
|
||||
|
||||
/*
|
||||
* ╭────────╮
|
||||
* ──────────────────────────────────────────╯ PAGE ╰─────────────────────────────────────────────
|
||||
*/
|
||||
|
||||
interface Props extends AppStaticProps, AppLayoutRequired {
|
||||
contents: NonNullable<GetContentsQuery["contents"]>["data"];
|
||||
}
|
||||
|
||||
const Contents = ({
|
||||
langui,
|
||||
contents,
|
||||
languages,
|
||||
...otherProps
|
||||
}: Props): JSX.Element => {
|
||||
const hoverable = useMediaHoverable();
|
||||
|
||||
const [groupingMethod, setGroupingMethod] = useState<number>(
|
||||
DEFAULT_FILTERS_STATE.groupingMethod
|
||||
);
|
||||
const {
|
||||
state: keepInfoVisible,
|
||||
toggleState: toggleKeepInfoVisible,
|
||||
setState: setKeepInfoVisible,
|
||||
} = useBoolean(DEFAULT_FILTERS_STATE.keepInfoVisible);
|
||||
|
||||
const [searchName, setSearchName] = useState(
|
||||
DEFAULT_FILTERS_STATE.searchName
|
||||
);
|
||||
|
||||
const groupingFunction = useCallback(
|
||||
(
|
||||
item: SelectiveNonNullable<
|
||||
NonNullable<GetContentsQuery["contents"]>["data"][number],
|
||||
"attributes" | "id"
|
||||
>
|
||||
): string[] => {
|
||||
switch (groupingMethod) {
|
||||
case 0: {
|
||||
const categories = filterHasAttributes(
|
||||
item.attributes.categories?.data,
|
||||
["attributes"] as const
|
||||
);
|
||||
if (categories.length > 0) {
|
||||
return categories.map((category) => category.attributes.name);
|
||||
}
|
||||
return [langui.no_category ?? "No category"];
|
||||
}
|
||||
case 1: {
|
||||
return [
|
||||
item.attributes.type?.data?.attributes?.titles?.[0]?.title ??
|
||||
item.attributes.type?.data?.attributes?.slug
|
||||
? prettySlug(item.attributes.type.data.attributes.slug)
|
||||
: langui.no_type ?? "No type",
|
||||
];
|
||||
}
|
||||
default: {
|
||||
return [""];
|
||||
}
|
||||
}
|
||||
},
|
||||
[groupingMethod, langui]
|
||||
);
|
||||
|
||||
const filteringFunction = useCallback(
|
||||
(
|
||||
item: SelectiveNonNullable<Props["contents"][number], "attributes" | "id">
|
||||
) => {
|
||||
if (searchName.length > 1) {
|
||||
if (
|
||||
filterDefined(item.attributes.translations).find((translation) =>
|
||||
prettyInlineTitle(
|
||||
translation.pre_title,
|
||||
translation.title,
|
||||
translation.subtitle
|
||||
)
|
||||
.toLowerCase()
|
||||
.includes(searchName.toLowerCase())
|
||||
)
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
},
|
||||
[searchName]
|
||||
);
|
||||
|
||||
const subPanel = useMemo(
|
||||
() => (
|
||||
<SubPanel>
|
||||
<PanelHeader
|
||||
icon={Icon.Workspaces}
|
||||
title={langui.contents}
|
||||
description={langui.contents_description}
|
||||
/>
|
||||
|
||||
<Button
|
||||
href="/contents"
|
||||
text={"Switch to folder view"}
|
||||
icon={Icon.Folder}
|
||||
/>
|
||||
|
||||
<HorizontalLine />
|
||||
|
||||
<TextInput
|
||||
className="mb-6 w-full"
|
||||
placeholder={langui.search_title ?? "Search..."}
|
||||
value={searchName}
|
||||
onChange={setSearchName}
|
||||
/>
|
||||
|
||||
<WithLabel
|
||||
label={langui.group_by}
|
||||
input={
|
||||
<Select
|
||||
className="w-full"
|
||||
options={[langui.category ?? "Category", langui.type ?? "Type"]}
|
||||
value={groupingMethod}
|
||||
onChange={setGroupingMethod}
|
||||
allowEmpty
|
||||
/>
|
||||
}
|
||||
/>
|
||||
|
||||
{hoverable && (
|
||||
<WithLabel
|
||||
label={langui.always_show_info}
|
||||
input={
|
||||
<Switch onClick={toggleKeepInfoVisible} value={keepInfoVisible} />
|
||||
}
|
||||
/>
|
||||
)}
|
||||
|
||||
<Button
|
||||
className="mt-8"
|
||||
text={langui.reset_all_filters}
|
||||
icon={Icon.Replay}
|
||||
onClick={() => {
|
||||
setSearchName(DEFAULT_FILTERS_STATE.searchName);
|
||||
setGroupingMethod(DEFAULT_FILTERS_STATE.groupingMethod);
|
||||
setKeepInfoVisible(DEFAULT_FILTERS_STATE.keepInfoVisible);
|
||||
}}
|
||||
/>
|
||||
</SubPanel>
|
||||
),
|
||||
[
|
||||
groupingMethod,
|
||||
hoverable,
|
||||
keepInfoVisible,
|
||||
langui.always_show_info,
|
||||
langui.category,
|
||||
langui.contents,
|
||||
langui.contents_description,
|
||||
langui.group_by,
|
||||
langui.reset_all_filters,
|
||||
langui.search_title,
|
||||
langui.type,
|
||||
searchName,
|
||||
setKeepInfoVisible,
|
||||
toggleKeepInfoVisible,
|
||||
]
|
||||
);
|
||||
|
||||
const contentPanel = useMemo(
|
||||
() => (
|
||||
<ContentPanel width={ContentPanelWidthSizes.Full}>
|
||||
<SmartList
|
||||
items={filterHasAttributes(contents, ["attributes", "id"] as const)}
|
||||
getItemId={(item) => item.id}
|
||||
renderItem={({ item }) => (
|
||||
<TranslatedPreviewCard
|
||||
href={`/contents/${item.attributes.slug}`}
|
||||
translations={filterHasAttributes(item.attributes.translations, [
|
||||
"language.data.attributes.code",
|
||||
] as const).map((translation) => ({
|
||||
pre_title: translation.pre_title,
|
||||
title: translation.title,
|
||||
subtitle: translation.subtitle,
|
||||
language: translation.language.data.attributes.code,
|
||||
}))}
|
||||
fallback={{ title: prettySlug(item.attributes.slug) }}
|
||||
thumbnail={item.attributes.thumbnail?.data?.attributes}
|
||||
thumbnailAspectRatio="3/2"
|
||||
thumbnailForceAspectRatio
|
||||
topChips={
|
||||
item.attributes.type?.data?.attributes
|
||||
? [
|
||||
item.attributes.type.data.attributes.titles?.[0]
|
||||
? item.attributes.type.data.attributes.titles[0]?.title
|
||||
: prettySlug(item.attributes.type.data.attributes.slug),
|
||||
]
|
||||
: undefined
|
||||
}
|
||||
bottomChips={item.attributes.categories?.data.map(
|
||||
(category) => category.attributes?.short ?? ""
|
||||
)}
|
||||
keepInfoVisible={keepInfoVisible}
|
||||
/>
|
||||
)}
|
||||
className="grid-cols-2 desktop:grid-cols-[repeat(auto-fill,_minmax(15rem,1fr))]"
|
||||
groupingFunction={groupingFunction}
|
||||
filteringFunction={filteringFunction}
|
||||
searchingTerm={searchName}
|
||||
searchingBy={(item) =>
|
||||
`
|
||||
${item.attributes.slug}
|
||||
${filterDefined(item.attributes.translations)
|
||||
.map((translation) =>
|
||||
prettyInlineTitle(
|
||||
translation.pre_title,
|
||||
translation.title,
|
||||
translation.subtitle
|
||||
)
|
||||
)
|
||||
.join(" ")}`
|
||||
}
|
||||
paginationItemPerPage={50}
|
||||
langui={langui}
|
||||
/>
|
||||
</ContentPanel>
|
||||
),
|
||||
[
|
||||
contents,
|
||||
filteringFunction,
|
||||
groupingFunction,
|
||||
keepInfoVisible,
|
||||
langui,
|
||||
searchName,
|
||||
]
|
||||
);
|
||||
|
||||
return (
|
||||
<AppLayout
|
||||
subPanel={subPanel}
|
||||
contentPanel={contentPanel}
|
||||
subPanelIcon={Icon.Search}
|
||||
languages={languages}
|
||||
langui={langui}
|
||||
{...otherProps}
|
||||
/>
|
||||
);
|
||||
};
|
||||
export default Contents;
|
||||
|
||||
/*
|
||||
* ╭──────────────────────╮
|
||||
* ───────────────────────────────────╯ NEXT DATA FETCHING ╰──────────────────────────────────────
|
||||
*/
|
||||
|
||||
export const getStaticProps: GetStaticProps = async (context) => {
|
||||
const sdk = getReadySdk();
|
||||
const contents = await sdk.getContents({
|
||||
language_code: context.locale ?? "en",
|
||||
});
|
||||
if (!contents.contents) return { notFound: true };
|
||||
contents.contents.data.sort((a, b) => {
|
||||
const titleA = a.attributes?.slug ?? "";
|
||||
const titleB = b.attributes?.slug ?? "";
|
||||
return titleA.localeCompare(titleB);
|
||||
});
|
||||
|
||||
const appStaticProps = await getAppStaticProps(context);
|
||||
const props: Props = {
|
||||
...appStaticProps,
|
||||
contents: contents.contents.data,
|
||||
openGraph: getOpenGraph(
|
||||
appStaticProps.langui,
|
||||
appStaticProps.langui.contents ?? "Contents"
|
||||
),
|
||||
};
|
||||
return {
|
||||
props: props,
|
||||
};
|
||||
};
|
|
@ -0,0 +1,322 @@
|
|||
import { GetStaticPaths, GetStaticPathsResult, GetStaticProps } from "next";
|
||||
import { useMemo } from "react";
|
||||
import { AppLayout, AppLayoutRequired } from "components/AppLayout";
|
||||
import {
|
||||
ContentPanel,
|
||||
ContentPanelWidthSizes,
|
||||
} from "components/Panels/ContentPanel";
|
||||
import { AppStaticProps, getAppStaticProps } from "graphql/getAppStaticProps";
|
||||
import { getOpenGraph } from "helpers/openGraph";
|
||||
import { getReadySdk } from "graphql/sdk";
|
||||
import { filterHasAttributes } from "helpers/others";
|
||||
import { GetContentsFolderQuery } from "graphql/generated";
|
||||
import {
|
||||
getDefaultPreferredLanguages,
|
||||
staticSmartLanguage,
|
||||
} from "helpers/locales";
|
||||
import { prettySlug } from "helpers/formatters";
|
||||
import { SmartList } from "components/SmartList";
|
||||
import {
|
||||
TranslatedButton,
|
||||
TranslatedPreviewCard,
|
||||
TranslatedPreviewFolder,
|
||||
} from "components/Translated";
|
||||
import { Ico, Icon } from "components/Ico";
|
||||
import { Button } from "components/Inputs/Button";
|
||||
import { Link } from "components/Inputs/Link";
|
||||
import { PanelHeader } from "components/PanelComponents/PanelHeader";
|
||||
import { SubPanel } from "components/Panels/SubPanel";
|
||||
|
||||
/*
|
||||
* ╭────────╮
|
||||
* ──────────────────────────────────────────╯ PAGE ╰─────────────────────────────────────────────
|
||||
*/
|
||||
|
||||
interface Props extends AppStaticProps, AppLayoutRequired {
|
||||
folder: NonNullable<
|
||||
NonNullable<
|
||||
GetContentsFolderQuery["contentsFolders"]
|
||||
>["data"][number]["attributes"]
|
||||
>;
|
||||
}
|
||||
|
||||
const ContentsFolder = ({
|
||||
langui,
|
||||
openGraph,
|
||||
folder,
|
||||
...otherProps
|
||||
}: Props): JSX.Element => {
|
||||
const subPanel = useMemo(
|
||||
() => (
|
||||
<SubPanel>
|
||||
<PanelHeader
|
||||
icon={Icon.Workspaces}
|
||||
title={langui.contents}
|
||||
description={langui.contents_description}
|
||||
/>
|
||||
|
||||
<Button
|
||||
href="/contents/all"
|
||||
text={"Switch to grid view"}
|
||||
icon={Icon.Apps}
|
||||
/>
|
||||
</SubPanel>
|
||||
),
|
||||
[langui.contents, langui.contents_description]
|
||||
);
|
||||
|
||||
const contentPanel = useMemo(
|
||||
() => (
|
||||
<ContentPanel width={ContentPanelWidthSizes.Full}>
|
||||
<div className="mb-10 grid grid-flow-col place-items-center justify-start gap-x-2">
|
||||
{folder.parent_folder?.data?.attributes && (
|
||||
<>
|
||||
{folder.parent_folder.data.attributes.slug === "root" ? (
|
||||
<Button href="/contents" icon={Icon.Home} />
|
||||
) : (
|
||||
<TranslatedButton
|
||||
href={`/contents/folder/${folder.parent_folder.data.attributes.slug}`}
|
||||
translations={filterHasAttributes(
|
||||
folder.parent_folder.data.attributes.titles,
|
||||
["language.data.attributes.code"] as const
|
||||
).map((title) => ({
|
||||
language: title.language.data.attributes.code,
|
||||
text: title.title,
|
||||
}))}
|
||||
fallback={{
|
||||
text: prettySlug(folder.parent_folder.data.attributes.slug),
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
<Ico icon={Icon.ChevronRight} />
|
||||
</>
|
||||
)}
|
||||
|
||||
{folder.slug === "root" ? (
|
||||
<Button href="/contents" icon={Icon.Home} active />
|
||||
) : (
|
||||
<TranslatedButton
|
||||
translations={filterHasAttributes(folder.titles, [
|
||||
"language.data.attributes.code",
|
||||
] as const).map((title) => ({
|
||||
language: title.language.data.attributes.code,
|
||||
text: title.title,
|
||||
}))}
|
||||
fallback={{
|
||||
text: prettySlug(folder.slug),
|
||||
}}
|
||||
active
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<SmartList
|
||||
items={filterHasAttributes(folder.subfolders?.data, [
|
||||
"id",
|
||||
"attributes",
|
||||
] as const)}
|
||||
getItemId={(item) => item.id}
|
||||
renderItem={({ item }) => (
|
||||
<TranslatedPreviewFolder
|
||||
href={`/contents/folder/${item.attributes.slug}`}
|
||||
translations={filterHasAttributes(item.attributes.titles, [
|
||||
"language.data.attributes.code",
|
||||
] as const).map((title) => ({
|
||||
title: title.title,
|
||||
language: title.language.data.attributes.code,
|
||||
}))}
|
||||
fallback={{ title: prettySlug(item.attributes.slug) }}
|
||||
/>
|
||||
)}
|
||||
className="grid-cols-2 items-stretch
|
||||
desktop:grid-cols-[repeat(auto-fill,_minmax(15rem,1fr))]"
|
||||
renderWhenEmpty={() => <></>}
|
||||
langui={langui}
|
||||
groupingFunction={() => [langui.folders ?? "Folders"]}
|
||||
/>
|
||||
|
||||
<SmartList
|
||||
items={filterHasAttributes(folder.contents?.data, [
|
||||
"id",
|
||||
"attributes",
|
||||
] as const)}
|
||||
getItemId={(item) => item.id}
|
||||
renderItem={({ item }) => (
|
||||
<TranslatedPreviewCard
|
||||
href={`/contents/${item.attributes.slug}`}
|
||||
translations={filterHasAttributes(item.attributes.translations, [
|
||||
"language.data.attributes.code",
|
||||
] as const).map((translation) => ({
|
||||
pre_title: translation.pre_title,
|
||||
title: translation.title,
|
||||
subtitle: translation.subtitle,
|
||||
language: translation.language.data.attributes.code,
|
||||
}))}
|
||||
fallback={{ title: prettySlug(item.attributes.slug) }}
|
||||
thumbnail={item.attributes.thumbnail?.data?.attributes}
|
||||
thumbnailAspectRatio="3/2"
|
||||
thumbnailForceAspectRatio
|
||||
topChips={
|
||||
item.attributes.type?.data?.attributes
|
||||
? [
|
||||
item.attributes.type.data.attributes.titles?.[0]
|
||||
? item.attributes.type.data.attributes.titles[0]?.title
|
||||
: prettySlug(item.attributes.type.data.attributes.slug),
|
||||
]
|
||||
: undefined
|
||||
}
|
||||
bottomChips={item.attributes.categories?.data.map(
|
||||
(category) => category.attributes?.short ?? ""
|
||||
)}
|
||||
keepInfoVisible
|
||||
/>
|
||||
)}
|
||||
className="grid-cols-2 desktop:grid-cols-[repeat(auto-fill,_minmax(15rem,1fr))]"
|
||||
renderWhenEmpty={() => <></>}
|
||||
langui={langui}
|
||||
groupingFunction={() => [langui.contents ?? "Contents"]}
|
||||
/>
|
||||
|
||||
{folder.contents?.data.length === 0 &&
|
||||
folder.subfolders?.data.length === 0 && (
|
||||
<NoContentNorFolderMessage langui={langui} />
|
||||
)}
|
||||
</ContentPanel>
|
||||
),
|
||||
[
|
||||
folder.contents,
|
||||
folder.parent_folder?.data?.attributes,
|
||||
folder.slug,
|
||||
folder.subfolders,
|
||||
folder.titles,
|
||||
langui,
|
||||
]
|
||||
);
|
||||
|
||||
return (
|
||||
<AppLayout
|
||||
subPanel={subPanel}
|
||||
contentPanel={contentPanel}
|
||||
openGraph={openGraph}
|
||||
langui={langui}
|
||||
{...otherProps}
|
||||
/>
|
||||
);
|
||||
};
|
||||
export default ContentsFolder;
|
||||
|
||||
/*
|
||||
* ╭──────────────────────╮
|
||||
* ───────────────────────────────────╯ NEXT DATA FETCHING ╰──────────────────────────────────────
|
||||
*/
|
||||
|
||||
export const getStaticProps: GetStaticProps = async (context) => {
|
||||
const sdk = getReadySdk();
|
||||
const slug = context.params?.slug ? context.params.slug.toString() : "";
|
||||
const contentsFolder = await sdk.getContentsFolder({
|
||||
slug: slug,
|
||||
language_code: context.locale ?? "en",
|
||||
});
|
||||
if (!contentsFolder.contentsFolders?.data[0]?.attributes) {
|
||||
return { notFound: true };
|
||||
}
|
||||
|
||||
const folder = contentsFolder.contentsFolders.data[0].attributes;
|
||||
const appStaticProps = await getAppStaticProps(context);
|
||||
|
||||
const title = (() => {
|
||||
if (slug === "root") {
|
||||
return appStaticProps.langui.contents ?? "Contents";
|
||||
}
|
||||
if (context.locale && context.locales) {
|
||||
const selectedTranslation = staticSmartLanguage({
|
||||
items: folder.titles,
|
||||
languageExtractor: (item) => item.language?.data?.attributes?.code,
|
||||
preferredLanguages: getDefaultPreferredLanguages(
|
||||
context.locale,
|
||||
context.locales
|
||||
),
|
||||
});
|
||||
if (selectedTranslation) {
|
||||
return selectedTranslation.title;
|
||||
}
|
||||
}
|
||||
return prettySlug(folder.slug);
|
||||
})();
|
||||
|
||||
const props: Props = {
|
||||
...appStaticProps,
|
||||
openGraph: getOpenGraph(appStaticProps.langui, title),
|
||||
folder,
|
||||
};
|
||||
return {
|
||||
props: props,
|
||||
};
|
||||
};
|
||||
|
||||
// ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─
|
||||
|
||||
export const getStaticPaths: GetStaticPaths = async (context) => {
|
||||
const sdk = getReadySdk();
|
||||
const contents = await sdk.getContentsFoldersSlugs();
|
||||
const paths: GetStaticPathsResult["paths"] = [];
|
||||
filterHasAttributes(contents.contentsFolders?.data, [
|
||||
"attributes",
|
||||
] as const).map((item) => {
|
||||
context.locales?.map((local) => {
|
||||
paths.push({
|
||||
params: { slug: item.attributes.slug },
|
||||
locale: local,
|
||||
});
|
||||
});
|
||||
});
|
||||
return {
|
||||
paths,
|
||||
fallback: "blocking",
|
||||
};
|
||||
};
|
||||
|
||||
/*
|
||||
* ╭──────────────────────╮
|
||||
* ───────────────────────────────────╯ PRIVATE COMPONENTS ╰──────────────────────────────────────
|
||||
*/
|
||||
|
||||
interface PreviewFolderProps {
|
||||
href: string;
|
||||
title: string | null | undefined;
|
||||
}
|
||||
|
||||
export const PreviewFolder = ({
|
||||
href,
|
||||
title,
|
||||
}: PreviewFolderProps): JSX.Element => (
|
||||
<Link
|
||||
href={href}
|
||||
className="flex w-full cursor-pointer flex-row place-content-center place-items-center gap-4
|
||||
rounded-md bg-light p-6 px-8 transition-transform drop-shadow-shade-xl hover:scale-[1.02]
|
||||
mobile:px-6 mobile:py-6"
|
||||
>
|
||||
{title && (
|
||||
<p className="text-center font-headers text-lg font-bold leading-none">
|
||||
{title}
|
||||
</p>
|
||||
)}
|
||||
</Link>
|
||||
);
|
||||
|
||||
interface NoContentNorFolderMessageProps {
|
||||
langui: AppStaticProps["langui"];
|
||||
}
|
||||
|
||||
const NoContentNorFolderMessage = ({
|
||||
langui,
|
||||
}: NoContentNorFolderMessageProps) => (
|
||||
<div className="grid place-content-center">
|
||||
<div
|
||||
className="grid grid-flow-col place-items-center gap-9 rounded-2xl border-2 border-dotted
|
||||
border-dark p-8 text-dark opacity-40"
|
||||
>
|
||||
<p className="max-w-xs text-2xl">{langui.empty_folder_message}</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
|
@ -1,332 +1,16 @@
|
|||
import { GetStaticProps } from "next";
|
||||
import { useState, useMemo, useCallback } from "react";
|
||||
import { AppLayout, AppLayoutRequired } from "components/AppLayout";
|
||||
import { Select } from "components/Inputs/Select";
|
||||
import { Switch } from "components/Inputs/Switch";
|
||||
import { PanelHeader } from "components/PanelComponents/PanelHeader";
|
||||
import {
|
||||
ContentPanel,
|
||||
ContentPanelWidthSizes,
|
||||
} from "components/Panels/ContentPanel";
|
||||
import { SubPanel } from "components/Panels/SubPanel";
|
||||
import { AppStaticProps, getAppStaticProps } from "graphql/getAppStaticProps";
|
||||
import { getReadySdk } from "graphql/sdk";
|
||||
import { prettyInlineTitle, prettySlug } from "helpers/formatters";
|
||||
import { TextInput } from "components/Inputs/TextInput";
|
||||
import { WithLabel } from "components/Inputs/WithLabel";
|
||||
import { Button } from "components/Inputs/Button";
|
||||
import { useMediaHoverable } from "hooks/useMediaQuery";
|
||||
import { Icon } from "components/Ico";
|
||||
import { filterDefined, filterHasAttributes } from "helpers/others";
|
||||
import { GetContentsQuery } from "graphql/generated";
|
||||
import { SmartList } from "components/SmartList";
|
||||
import { SelectiveNonNullable } from "helpers/types/SelectiveNonNullable";
|
||||
import { useBoolean } from "hooks/useBoolean";
|
||||
import { TranslatedPreviewCard } from "components/Translated";
|
||||
import { getOpenGraph } from "helpers/openGraph";
|
||||
|
||||
/*
|
||||
* ╭─────────────╮
|
||||
* ────────────────────────────────────────╯ CONSTANTS ╰──────────────────────────────────────────
|
||||
*/
|
||||
|
||||
const DEFAULT_FILTERS_STATE = {
|
||||
groupingMethod: -1,
|
||||
keepInfoVisible: false,
|
||||
combineRelatedContent: true,
|
||||
searchName: "",
|
||||
};
|
||||
import ContentsFolder, {
|
||||
getStaticProps as folderGetStaticProps,
|
||||
} from "./folder/[slug]";
|
||||
|
||||
/*
|
||||
* ╭────────╮
|
||||
* ──────────────────────────────────────────╯ PAGE ╰─────────────────────────────────────────────
|
||||
*/
|
||||
|
||||
interface Props extends AppStaticProps, AppLayoutRequired {
|
||||
contents: NonNullable<GetContentsQuery["contents"]>["data"];
|
||||
}
|
||||
|
||||
const Contents = ({
|
||||
langui,
|
||||
contents,
|
||||
languages,
|
||||
...otherProps
|
||||
}: Props): JSX.Element => {
|
||||
const hoverable = useMediaHoverable();
|
||||
|
||||
const [groupingMethod, setGroupingMethod] = useState<number>(
|
||||
DEFAULT_FILTERS_STATE.groupingMethod
|
||||
);
|
||||
const {
|
||||
state: keepInfoVisible,
|
||||
toggleState: toggleKeepInfoVisible,
|
||||
setState: setKeepInfoVisible,
|
||||
} = useBoolean(DEFAULT_FILTERS_STATE.keepInfoVisible);
|
||||
|
||||
const {
|
||||
state: combineRelatedContent,
|
||||
toggleState: toggleCombineRelatedContent,
|
||||
setState: setCombineRelatedContent,
|
||||
} = useBoolean(DEFAULT_FILTERS_STATE.combineRelatedContent);
|
||||
const [searchName, setSearchName] = useState(
|
||||
DEFAULT_FILTERS_STATE.searchName
|
||||
);
|
||||
|
||||
const effectiveCombineRelatedContent = useMemo(
|
||||
() => (searchName.length > 1 ? false : combineRelatedContent),
|
||||
[combineRelatedContent, searchName.length]
|
||||
);
|
||||
|
||||
const groupingFunction = useCallback(
|
||||
(
|
||||
item: SelectiveNonNullable<
|
||||
NonNullable<GetContentsQuery["contents"]>["data"][number],
|
||||
"attributes" | "id"
|
||||
>
|
||||
): string[] => {
|
||||
switch (groupingMethod) {
|
||||
case 0: {
|
||||
const categories = filterHasAttributes(
|
||||
item.attributes.categories?.data,
|
||||
["attributes"] as const
|
||||
);
|
||||
if (categories.length > 0) {
|
||||
return categories.map((category) => category.attributes.name);
|
||||
}
|
||||
return [langui.no_category ?? "No category"];
|
||||
}
|
||||
case 1: {
|
||||
return [
|
||||
item.attributes.type?.data?.attributes?.titles?.[0]?.title ??
|
||||
item.attributes.type?.data?.attributes?.slug
|
||||
? prettySlug(item.attributes.type.data.attributes.slug)
|
||||
: langui.no_type ?? "No type",
|
||||
];
|
||||
}
|
||||
default: {
|
||||
return [""];
|
||||
}
|
||||
}
|
||||
},
|
||||
[groupingMethod, langui]
|
||||
);
|
||||
|
||||
const filteringFunction = useCallback(
|
||||
(
|
||||
item: SelectiveNonNullable<Props["contents"][number], "attributes" | "id">
|
||||
) => {
|
||||
if (
|
||||
effectiveCombineRelatedContent &&
|
||||
item.attributes.group?.data?.attributes?.combine === true &&
|
||||
item.attributes.group.data.attributes.contents?.data[0].id !== item.id
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
if (searchName.length > 1) {
|
||||
if (
|
||||
filterDefined(item.attributes.translations).find((translation) =>
|
||||
prettyInlineTitle(
|
||||
translation.pre_title,
|
||||
translation.title,
|
||||
translation.subtitle
|
||||
)
|
||||
.toLowerCase()
|
||||
.includes(searchName.toLowerCase())
|
||||
)
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
},
|
||||
[effectiveCombineRelatedContent, searchName]
|
||||
);
|
||||
|
||||
const groupCountingFunction = useCallback(
|
||||
(
|
||||
item: SelectiveNonNullable<Props["contents"][number], "attributes" | "id">
|
||||
) =>
|
||||
item.attributes.group?.data?.attributes?.combine &&
|
||||
effectiveCombineRelatedContent
|
||||
? item.attributes.group.data.attributes.contents?.data.length ?? 1
|
||||
: 1,
|
||||
[effectiveCombineRelatedContent]
|
||||
);
|
||||
|
||||
const subPanel = useMemo(
|
||||
() => (
|
||||
<SubPanel>
|
||||
<PanelHeader
|
||||
icon={Icon.Workspaces}
|
||||
title={langui.contents}
|
||||
description={langui.contents_description}
|
||||
/>
|
||||
|
||||
<TextInput
|
||||
className="mb-6 w-full"
|
||||
placeholder={langui.search_title ?? "Search..."}
|
||||
value={searchName}
|
||||
onChange={setSearchName}
|
||||
/>
|
||||
|
||||
<WithLabel
|
||||
label={langui.group_by}
|
||||
input={
|
||||
<Select
|
||||
className="w-full"
|
||||
options={[langui.category ?? "Category", langui.type ?? "Type"]}
|
||||
value={groupingMethod}
|
||||
onChange={setGroupingMethod}
|
||||
allowEmpty
|
||||
/>
|
||||
}
|
||||
/>
|
||||
|
||||
<WithLabel
|
||||
label={langui.combine_related_contents}
|
||||
disabled={searchName.length > 1}
|
||||
input={
|
||||
<Switch
|
||||
value={effectiveCombineRelatedContent}
|
||||
onClick={toggleCombineRelatedContent}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
|
||||
{hoverable && (
|
||||
<WithLabel
|
||||
label={langui.always_show_info}
|
||||
input={
|
||||
<Switch onClick={toggleKeepInfoVisible} value={keepInfoVisible} />
|
||||
}
|
||||
/>
|
||||
)}
|
||||
|
||||
<Button
|
||||
className="mt-8"
|
||||
text={langui.reset_all_filters}
|
||||
icon={Icon.Replay}
|
||||
onClick={() => {
|
||||
setSearchName(DEFAULT_FILTERS_STATE.searchName);
|
||||
setGroupingMethod(DEFAULT_FILTERS_STATE.groupingMethod);
|
||||
setKeepInfoVisible(DEFAULT_FILTERS_STATE.keepInfoVisible);
|
||||
setCombineRelatedContent(
|
||||
DEFAULT_FILTERS_STATE.combineRelatedContent
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</SubPanel>
|
||||
),
|
||||
[
|
||||
effectiveCombineRelatedContent,
|
||||
groupingMethod,
|
||||
hoverable,
|
||||
keepInfoVisible,
|
||||
langui.always_show_info,
|
||||
langui.category,
|
||||
langui.combine_related_contents,
|
||||
langui.contents,
|
||||
langui.contents_description,
|
||||
langui.group_by,
|
||||
langui.reset_all_filters,
|
||||
langui.search_title,
|
||||
langui.type,
|
||||
searchName,
|
||||
setCombineRelatedContent,
|
||||
setKeepInfoVisible,
|
||||
toggleCombineRelatedContent,
|
||||
toggleKeepInfoVisible,
|
||||
]
|
||||
);
|
||||
|
||||
const contentPanel = useMemo(
|
||||
() => (
|
||||
<ContentPanel width={ContentPanelWidthSizes.Full}>
|
||||
<SmartList
|
||||
items={filterHasAttributes(contents, ["attributes", "id"] as const)}
|
||||
getItemId={(item) => item.id}
|
||||
renderItem={({ item }) => (
|
||||
<TranslatedPreviewCard
|
||||
href={`/contents/${item.attributes.slug}`}
|
||||
translations={filterHasAttributes(item.attributes.translations, [
|
||||
"language.data.attributes.code",
|
||||
] as const).map((translation) => ({
|
||||
pre_title: translation.pre_title,
|
||||
title: translation.title,
|
||||
subtitle: translation.subtitle,
|
||||
language: translation.language.data.attributes.code,
|
||||
}))}
|
||||
fallback={{ title: prettySlug(item.attributes.slug) }}
|
||||
thumbnail={item.attributes.thumbnail?.data?.attributes}
|
||||
thumbnailAspectRatio="3/2"
|
||||
thumbnailForceAspectRatio
|
||||
stackNumber={
|
||||
effectiveCombineRelatedContent &&
|
||||
item.attributes.group?.data?.attributes?.combine === true
|
||||
? item.attributes.group.data.attributes.contents?.data.length
|
||||
: 0
|
||||
}
|
||||
topChips={
|
||||
item.attributes.type?.data?.attributes
|
||||
? [
|
||||
item.attributes.type.data.attributes.titles?.[0]
|
||||
? item.attributes.type.data.attributes.titles[0]?.title
|
||||
: prettySlug(item.attributes.type.data.attributes.slug),
|
||||
]
|
||||
: undefined
|
||||
}
|
||||
bottomChips={item.attributes.categories?.data.map(
|
||||
(category) => category.attributes?.short ?? ""
|
||||
)}
|
||||
keepInfoVisible={keepInfoVisible}
|
||||
/>
|
||||
)}
|
||||
className="grid-cols-2 desktop:grid-cols-[repeat(auto-fill,_minmax(15rem,1fr))]"
|
||||
groupingFunction={groupingFunction}
|
||||
groupCountingFunction={groupCountingFunction}
|
||||
filteringFunction={filteringFunction}
|
||||
searchingTerm={searchName}
|
||||
searchingBy={(item) =>
|
||||
`
|
||||
${item.attributes.slug}
|
||||
${filterDefined(item.attributes.translations)
|
||||
.map((translation) =>
|
||||
prettyInlineTitle(
|
||||
translation.pre_title,
|
||||
translation.title,
|
||||
translation.subtitle
|
||||
)
|
||||
)
|
||||
.join(" ")}`
|
||||
}
|
||||
langui={langui}
|
||||
/>
|
||||
</ContentPanel>
|
||||
),
|
||||
[
|
||||
contents,
|
||||
effectiveCombineRelatedContent,
|
||||
filteringFunction,
|
||||
groupCountingFunction,
|
||||
groupingFunction,
|
||||
keepInfoVisible,
|
||||
langui,
|
||||
searchName,
|
||||
]
|
||||
);
|
||||
|
||||
return (
|
||||
<AppLayout
|
||||
subPanel={subPanel}
|
||||
contentPanel={contentPanel}
|
||||
subPanelIcon={Icon.Search}
|
||||
languages={languages}
|
||||
langui={langui}
|
||||
{...otherProps}
|
||||
/>
|
||||
);
|
||||
};
|
||||
const Contents = (props: Parameters<typeof ContentsFolder>[0]): JSX.Element => (
|
||||
<ContentsFolder {...props} />
|
||||
);
|
||||
export default Contents;
|
||||
|
||||
/*
|
||||
|
@ -335,27 +19,6 @@ export default Contents;
|
|||
*/
|
||||
|
||||
export const getStaticProps: GetStaticProps = async (context) => {
|
||||
const sdk = getReadySdk();
|
||||
const contents = await sdk.getContents({
|
||||
language_code: context.locale ?? "en",
|
||||
});
|
||||
if (!contents.contents) return { notFound: true };
|
||||
contents.contents.data.sort((a, b) => {
|
||||
const titleA = a.attributes?.slug ?? "";
|
||||
const titleB = b.attributes?.slug ?? "";
|
||||
return titleA.localeCompare(titleB);
|
||||
});
|
||||
|
||||
const appStaticProps = await getAppStaticProps(context);
|
||||
const props: Props = {
|
||||
...appStaticProps,
|
||||
contents: contents.contents.data,
|
||||
openGraph: getOpenGraph(
|
||||
appStaticProps.langui,
|
||||
appStaticProps.langui.contents ?? "Contents"
|
||||
),
|
||||
};
|
||||
return {
|
||||
props: props,
|
||||
};
|
||||
context.params = { slug: "root" };
|
||||
return await folderGetStaticProps(context);
|
||||
};
|
||||
|
|
|
@ -451,6 +451,7 @@ const Library = ({
|
|||
)
|
||||
}
|
||||
filteringFunction={filteringFunction}
|
||||
paginationItemPerPage={25}
|
||||
langui={langui}
|
||||
/>
|
||||
</ContentPanel>
|
||||
|
|
|
@ -139,6 +139,7 @@ const News = ({ langui, posts, ...otherProps }: Props): JSX.Element => {
|
|||
?.map((translation) => translation?.title)
|
||||
.join(" ")}`
|
||||
}
|
||||
paginationItemPerPage={25}
|
||||
/>
|
||||
</ContentPanel>
|
||||
),
|
||||
|
|
|
@ -212,6 +212,7 @@ const Wiki = ({ langui, pages, ...otherProps }: Props): JSX.Element => {
|
|||
.join(" ")
|
||||
}
|
||||
groupingFunction={groupingFunction}
|
||||
paginationItemPerPage={25}
|
||||
/>
|
||||
</ContentPanel>
|
||||
),
|
||||
|
|
Loading…
Reference in New Issue