Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[DEPRIORITISED] Multilingual content support #155

Open
wants to merge 22 commits into
base: main
Choose a base branch
from
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
261 changes: 228 additions & 33 deletions admin_app/src/app/content/edit/page.tsx

Large diffs are not rendered by default.

259 changes: 196 additions & 63 deletions admin_app/src/app/content/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,19 @@
import type { Content } from "@/app/content/edit/page";
import ContentCard from "@/components/ContentCard";
import { Layout } from "@/components/Layout";
import { LANGUAGE_OPTIONS, sizes } from "@/utils";
import { appColors, sizes } from "@/utils";
import { apiCalls } from "@/utils/api";
import { useAuth } from "@/utils/auth";
import { Add } from "@mui/icons-material";
import { Button, CircularProgress, Grid } from "@mui/material";
import { Add, Sort } from "@mui/icons-material";
import {
Button,
CircularProgress,
FormControl,
Grid,
InputLabel,
MenuItem,
Select,
} from "@mui/material";
import Alert from "@mui/material/Alert";
import Snackbar from "@mui/material/Snackbar";
import Link from "next/link";
Expand All @@ -17,69 +25,155 @@ import { SearchBar } from "../../components/SearchBar";

const MAX_CARDS_PER_PAGE = 12;

interface ContentLanding extends Content {
languages: string[];
}
interface Language {
language_id: number;
language_name: string;
}
const CardsPage = () => {
const [displayLanguage, setDisplayLanguage] = React.useState<string>(
LANGUAGE_OPTIONS[0].label,
);
const [displayLanguage, setDisplayLanguage] = React.useState<Language>();
const [defaultLanguage, setDefaultLanguage] = React.useState<Language>();
const [searchTerm, setSearchTerm] = React.useState<string>("");
const { accessLevel } = useAuth();

const { token, accessLevel } = useAuth();
React.useEffect(() => {
if (!displayLanguage && token) {
const fetchDefaultLanguage = async () => {
try {
const defaultLanguage = await apiCalls.getDefaultLanguage(token!);
setDisplayLanguage(defaultLanguage);
setDefaultLanguage(defaultLanguage);
} catch (error) {
console.error("Failed to fetch default language:", error);
}
};
fetchDefaultLanguage();
}
}, [token]);
return (
<Layout.FlexBox alignItems="center" gap={sizes.baseGap}>
<Layout.FlexBox gap={sizes.baseGap}>
<Layout.Spacer multiplier={3} />
<Layout.FlexBox
flexDirection={"row"}
gap={sizes.smallGap}
sx={{
width: "70%",
maxWidth: "500px",
minWidth: "200px",
}}
alignItems="center"
justifyContent="center"
width={"100%"}
>
<SearchBar searchTerm={searchTerm} setSearchTerm={setSearchTerm} />
</Layout.FlexBox>
<CardsUtilityStrip editAccess={accessLevel === "fullaccess"} />
<CardsGrid displayLanguage={displayLanguage} searchTerm={searchTerm} />
<CardsUtilityStrip
token={token!}
displayLanguage={displayLanguage!}
onChangeDisplayLanguage={(language) => {
setDisplayLanguage(language);
}}
/>
<CardsGrid displayLanguage={displayLanguage!} searchTerm={searchTerm} />
<CardsBottomStrip
editAccess={accessLevel === "fullaccess"}
defaultLanguageId={defaultLanguage ? defaultLanguage.language_id : null} />
<Layout.Spacer multiplier={4} />
</Layout.FlexBox>
);
};

const CardsUtilityStrip = ({ editAccess }: { editAccess: boolean }) => {
const CardsUtilityStrip = ({
token,
displayLanguage,
onChangeDisplayLanguage,
}: {
token: string;
displayLanguage: Language;
onChangeDisplayLanguage: (language: Language) => void;
}) => {
const [languageOptions, setLanguageOptions] = React.useState<Language[]>([]);
const [loadingLanguages, setLoadingLanguages] = React.useState(true);
React.useEffect(() => {
const fetchLanguages = async () => {
setLoadingLanguages(true);
try {
const languages = await apiCalls.getLanguageList(token);
setLanguageOptions(languages);
} catch (error) {
console.error("Failed to fetch language list:", error);
} finally {
setLoadingLanguages(false);
}
};
fetchLanguages();
onChangeDisplayLanguage(displayLanguage);
}, [token]);
const selectedValue = displayLanguage
? displayLanguage.language_name
: "";
return (
<Layout.FlexBox
key={"utility-strip"}
flexDirection={"row"}
justifyContent={"flex-right"}
alignItems={"right"}
sx={{
display: "flex",
alignSelf: "flex-end",
px: sizes.baseGap,
}}
gap={sizes.baseGap}
alignItems={"flex-start"}
justifyContent={"flex-start"}
sx={{ px: sizes.baseGap }}
>
<Button
variant="contained"
disabled={!editAccess}
component={Link}
href="/content/edit"
<Layout.FlexBox
sx={{ width: "auto" }}
flexDirection={"row"}
alignItems={"center"}
>
<Add />
New
</Button>
</Layout.FlexBox>
<Sort sx={{ display: "flex" }} />
<Layout.Spacer horizontal multiplier={1} />
<FormControl sx={{ width: "100%" }}>
<InputLabel>Language</InputLabel>
<Select
value={loadingLanguages || !displayLanguage ? "" : selectedValue}
label="Language"
onChange={({ target }) => {
const selectedLanguage = languageOptions.find(
(lang) => lang.language_name === target.value,
);
if (selectedLanguage) {
onChangeDisplayLanguage(selectedLanguage);
}
}}
sx={{
backgroundColor: appColors.white,
fontSize: sizes.mediumGap,
}}
>
{loadingLanguages || !displayLanguage ? (
<MenuItem value=""
sx={{ fontSize: sizes.mediumGap }}
>
Loading...
</MenuItem>
) :
languageOptions.map((language) => (
<MenuItem
key={language.language_id}
value={language.language_name}
sx={{ fontSize: sizes.mediumGap }}
>
{language.language_name}
</MenuItem>
))
}
</Select>
</FormControl>
</Layout.FlexBox>
</Layout.FlexBox >
);
};

const CardsGrid = ({
displayLanguage,
searchTerm,
}: {
displayLanguage: string;
displayLanguage: Language;
searchTerm: string;
}) => {
const [page, setPage] = React.useState<number>(1);
const [max_pages, setMaxPages] = React.useState<number>(1);
const [cards, setCards] = React.useState<Content[]>([]);
const [cards, setCards] = React.useState<ContentLanding[]>([]);
const [isLoading, setIsLoading] = React.useState<boolean>(true);

const searchParams = useSearchParams();
Expand All @@ -92,10 +186,8 @@ const CardsGrid = ({
action: string | null,
content_id: number | null,
): string | null => {
if (action === "edit") {
return `Content #${content_id} updated`;
} else if (action === "add") {
return `Content #${content_id} created`;
if (action === "delete") {
return `Content #${content_id} deleted successfully`;
}
return null;
};
Expand All @@ -105,30 +197,37 @@ const CardsGrid = ({
);

const [refreshKey, setRefreshKey] = React.useState(0);
const onSuccessfulDelete = (content_id: number) => {
const onSuccessfulDelete = (content_id: number, language_id: number | null) => {
setIsLoading(true);
setSnackMessage(getSnackMessage("delete", content_id));
setRefreshKey((prevKey) => prevKey + 1);
setSnackMessage(`Content #${content_id} deleted successfully`);
};

};
const handleDeleteLanguageVersion = (content_id: number, language_id: number | null) => {
return apiCalls.deleteContent(content_id, language_id, token!);
};
React.useEffect(() => {
apiCalls
.getContentList(token!)
.then((data) => {
const filteredData = data.filter(
(card: Content) =>
card.content_title.includes(searchTerm) ||
card.content_text.includes(searchTerm),
);
setCards(filteredData);
setMaxPages(Math.ceil(filteredData.length / MAX_CARDS_PER_PAGE));
setIsLoading(false);
})
.catch((error) => {
console.error("Failed to fetch content:", error);
setIsLoading(false);
});
}, [refreshKey, searchTerm, token]);
if (displayLanguage) {
setIsLoading(true);
apiCalls
.getContentListLanding(
displayLanguage ? displayLanguage.language_name : "",
token!,
)
.then((data) => {
const filteredData = data.filter(
(card: ContentLanding) =>
card.content_title.includes(searchTerm) ||
card.content_text.includes(searchTerm),
);
setCards(filteredData);
setMaxPages(Math.ceil(filteredData.length / MAX_CARDS_PER_PAGE));
setIsLoading(false);
})
.catch((error) => console.error("Failed to fetch content:", error))
.finally(() => setIsLoading(false));
}
}, [refreshKey, searchTerm, displayLanguage, token]);

if (isLoading) {
return (
Expand Down Expand Up @@ -197,16 +296,26 @@ const CardsGrid = ({
title={item.content_title}
text={item.content_text}
content_id={item.content_id}
language_id={displayLanguage.language_id}
last_modified={item.updated_datetime_utc}
languages={item.languages}
getContentData={(content_id: number) => {
return apiCalls.getContent(content_id, null, token!);
}}
getLanguageList={() => {
return apiCalls.getLanguageList(token!);
}}
onSuccessfulDelete={onSuccessfulDelete}
onFailedDelete={(content_id: number) => {
setSnackMessage(
`Failed to delete content #${content_id}`,
);
}}
deleteContent={(content_id: number) => {
return apiCalls.deleteContent(content_id, token!);
return apiCalls.deleteContent(content_id, null, token!);
}}
deleteLanguageVersion={handleDeleteLanguageVersion}
setRefreshKey={setRefreshKey}
editAccess={accessLevel === "fullaccess"}
/>
</Grid>
Expand All @@ -216,9 +325,33 @@ const CardsGrid = ({
</Grid>
</Layout.FlexBox>
<PageNavigation page={page} setPage={setPage} max_pages={max_pages} />
<Layout.Spacer multiplier={1} />
</>
);
};

const CardsBottomStrip = ({ editAccess, defaultLanguageId }:
{
editAccess: boolean;
defaultLanguageId: number | null;
}) => {

return (
<Layout.FlexBox
flexDirection={"row"}
sx={{ px: sizes.baseGap }}
gap={sizes.baseGap}
>
<Button
variant="contained"
disabled={!editAccess}
component={Link}
href={`/content/edit?default_language_id=${defaultLanguageId}`}
>
<Add />
New
</Button>
</Layout.FlexBox>
);
};

export default CardsPage;
Loading
Loading