Skip to content

Commit

Permalink
OPHJOD-1139: Fetch content from CMS API and show them as cards on hom…
Browse files Browse the repository at this point in the history
…e page
  • Loading branch information
juhaniko committed Dec 17, 2024
1 parent 539ec14 commit d3e6534
Show file tree
Hide file tree
Showing 9 changed files with 212 additions and 28 deletions.
4 changes: 4 additions & 0 deletions .env
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
JODTOKEN=
CMSUSER=
CMSPASSWORD=
CMSURL=
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,8 @@ npm install
```

5. Take the steps in JOD Design System repository to get components of the design system work in hot reload mode.
6. Once the installation is complete, run the following command to start the development server:
6. Create a `.env.local` file and put the required environment variables there in order to get the connection to the CMS working. The required variable names are in `.env` file. See the following wiki page for further instructions: https://wiki.eduuni.fi/pages/viewpage.action?pageId=539867888
7. Once the installation is complete, run the following command to start the development server:

```shell
npm run dev
Expand Down
48 changes: 45 additions & 3 deletions src/routes/Home/Home.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
import { Title } from '@/components';
import { HeroCard, useMediaQueries } from '@jod/design-system';
import { LoaderData } from '@/routes/Home/loader';
import { findContentValueByLabel } from '@/utils/cms';
import { HeroCard, MediaCard, useMediaQueries } from '@jod/design-system';
import React from 'react';
import { useTranslation } from 'react-i18next';
import { Link } from 'react-router';
import { Link, useLoaderData } from 'react-router';

interface CardsProps {
className?: string;
Expand Down Expand Up @@ -53,9 +56,37 @@ const Cards = ({ className = '' }: CardsProps) => {
);
};

interface CardData {
id: string;
title: string;
description: string;
imageSrc: string;
imageAlt: string;
tags: string[];
}

const Home = () => {
const { t } = useTranslation();
const { sm } = useMediaQueries();
const { data } = useLoaderData<LoaderData>();
const [newContent, setNewContent] = React.useState<CardData[]>([]);

React.useEffect(() => {
const items = data.items.map((item) => {
const imageContent = findContentValueByLabel(item, 'Kuva')?.image;
const ingress = findContentValueByLabel(item, 'Tiivistelmä')?.data ?? '';

return {
id: item.uuid ?? '',
title: item.title ?? '',
description: ingress ?? '',
imageSrc: imageContent?.contentUrl ?? '',
imageAlt: imageContent?.title ?? '',
tags: item.keywords ?? [],
};
});
setNewContent(items);
}, [data]);

return (
<main role="main" className="mx-auto w-full max-w-screen-xl" id="jod-main">
Expand All @@ -68,7 +99,18 @@ const Home = () => {
<div className="col-span-3 print:col-span-3 flex flex-col gap-8">
<div>
<h2 className="text-heading-2-mobile sm:text-heading-2 mb-5">Uudet sisällöt</h2>
<p className="bg-todo h-[328px] flex items-center justify-center rounded">TODO</p>
<div className="flex flex-row gap-6">
{newContent.map((c) => (
<MediaCard
key={c.id}
label={c.title}
description={c.description}
imageSrc={c.imageSrc}
imageAlt={c.imageAlt}
tags={c.tags}
/>
))}
</div>
</div>
<div>
<h2 className="text-heading-2-mobile sm:text-heading-2 mb-5">Suositut sisällöt</h2>
Expand Down
3 changes: 2 additions & 1 deletion src/routes/Home/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import Home from './Home';
import loader from './loader';

export { Home };
export { Home, loader as homeLoader };
19 changes: 19 additions & 0 deletions src/routes/Home/loader.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { StructuredContentPage } from '@/types/cms-content';
import { LoaderFunction } from 'react-router';

const loader = (async () => {
const queryParams = new URLSearchParams();
queryParams.set('sort', 'dateCreated:desc');
const response = await fetch(`cms/o/headless-delivery/v1.0/sites/20117/structured-contents?${queryParams}`, {
headers: {
Accept: 'application/json',
'Accept-Language': 'fi-FI',
},
});
const data: StructuredContentPage = await response.json();

return { data };
}) satisfies LoaderFunction;

export type LoaderData = Awaited<ReturnType<typeof loader>>;
export default loader;
3 changes: 2 additions & 1 deletion src/routes/index.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import i18n from '@/i18n/config';
import { RouteObject, redirect } from 'react-router';
import { Home } from './Home';
import { Home, homeLoader } from './Home';
import { NoMatch, Root } from './Root';

const rootRoute: RouteObject = {
Expand All @@ -11,6 +11,7 @@ const rootRoute: RouteObject = {
{
index: true,
element: <Home />,
loader: homeLoader,
},
],
};
Expand Down
76 changes: 76 additions & 0 deletions src/types/cms-content.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
// https://app.swaggerhub.com/apis/liferayinc/headless-delivery/v1.0#/TaxonomyCategoryBrief
export interface TaxonomyBrief {
taxonomyCategoryId: number;
taxonomyCategoryName: string;
}

// https://app.swaggerhub.com/apis/liferayinc/headless-delivery/v1.0#/ContentDocument
export interface ContentDocument {
contentType: string;
contentUrl: string;
description: string;
encodingFormat: string;
fileExtension: string;
id: number;
sizeInBytes: number;
title: string;
}

// https://app.swaggerhub.com/apis/liferayinc/headless-delivery/v1.0#/ContentFieldValue
export interface ContentFieldValue {
data?: string;
image?: ContentDocument;
geo?: Record<string, unknown>;
link?: string;
}

// https://app.swaggerhub.com/apis/liferayinc/headless-delivery/v1.0#/ContentField
export interface ContentField {
contentFieldValue: ContentFieldValue;
dataType: string;
inputControl: string;
label: string;
name: string;
nestedContentFields: unknown[];
repeatable: boolean;
}

// https://app.swaggerhub.com/apis/liferayinc/headless-delivery/v1.0#/StructuredContent
export interface StructuredContent {
actions?: Record<string, unknown>;
availableLanguages?: string[];
contentFields?: ContentField[];
contentStructureId: number;
creator?: Record<string, string | number>;
customFields?: unknown[];
dateCreated?: string;
dateModified?: string;
description?: string;
externalReferenceCode?: string;
friendlyUrlPath?: string;
id?: number;
key?: number;
keywords?: string[];
neverExpire?: boolean;
numberOfComments?: number;
priority?: number;
relatedContents?: unknown[];
renderedContents?: Record<string, unknown>[];
siteId?: number;
strucuturedContentFolderId?: number;
subscribed?: boolean;
taxonomyCategoryBriefs?: TaxonomyBrief[];
title: string;
uuid?: string;
}

// https://app.swaggerhub.com/apis/liferayinc/headless-delivery/v1.0#/StructuredContent/getSiteStructuredContentsPage
export interface StructuredContentPage {
actions: Record<string, unknown>;
facets: unknown[];
items: StructuredContent[];
lastPage: number;
page: number;
pageSize: number;
totalCount: number;
}
13 changes: 13 additions & 0 deletions src/utils/cms.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { StructuredContent } from '@/types/cms-content';

type ContentLabel = 'Tiivistelmä' | 'Kuvaus' | 'Kuva' | 'Tiedosto' | 'Linkki';

/**
* Finds the content value from Liferay strucured content by label
* @param item Structured content item
* @param {ContentLabel} label Content label
* @returns {ContentFieldValue | undefined} Content value
*/
export const findContentValueByLabel = (item: StructuredContent, label: ContentLabel) => {
return item.contentFields?.find((field) => field.label === label)?.contentFieldValue;
};
71 changes: 49 additions & 22 deletions vite.config.ts
Original file line number Diff line number Diff line change
@@ -1,30 +1,57 @@
/// <reference types="vitest" />
import react from '@vitejs/plugin-react-swc';
import { fileURLToPath } from 'node:url';
import { defineConfig } from 'vite';
import { defineConfig, loadEnv } from 'vite';

const cmsHeaders = (env: Record<string, string>) => ({
Cookie: `JODTOKEN=${env.JODTOKEN}`,
Accept: 'application/json',
Authorization: 'Basic ' + Buffer.from(`${env.CMSUSER}:${env.CMSPASSWORD}`).toString('base64'),
});

// https://vite.dev/config/
export default defineConfig({
base: '/ohjaaja/',
plugins: [react()],
test: {
environment: 'jsdom',
globals: true,
setupFiles: ['./vitest.setup.ts'],
coverage: {
provider: 'v8',
reporter: ['lcov'],
export default defineConfig(({ mode }) => {
// loadEnv is required to read .env files
const env = loadEnv(mode, process.cwd(), '');
const target = env.CMSURL;

return {
base: '/ohjaaja/',
plugins: [react()],
test: {
environment: 'jsdom',
globals: true,
setupFiles: ['./vitest.setup.ts'],
coverage: {
provider: 'v8',
reporter: ['lcov'],
},
},
resolve: {
alias: [
{
find: '@',
replacement: fileURLToPath(new URL('./src', import.meta.url)),
},
],
},
},
resolve: {
alias: [
{
find: '@',
replacement: fileURLToPath(new URL('./src', import.meta.url)),
server: {
port: 8080,
proxy: {
'/ohjaaja/cms': {
target,
changeOrigin: true,
xfwd: true,
rewrite: (path) => path.replace(/^\/ohjaaja\/cms/, '/cms'),
headers: cmsHeaders(env),
},
'/cms/documents': {
target,
changeOrigin: true,
xfwd: true,
headers: cmsHeaders(env),
},
},
],
},
server: {
port: 8080,
},
},
};
});

0 comments on commit d3e6534

Please sign in to comment.