diff --git a/homepage/_pages/en/cards.md b/homepage/_pages/en/cards.md
new file mode 100644
index 00000000..105cffda
--- /dev/null
+++ b/homepage/_pages/en/cards.md
@@ -0,0 +1,28 @@
+---
+unique_slug: cards
+name: Cards
+layout_list:
+ - type: layout_headline
+ title: Card Introduction
+ - type: layout_cards
+ card_type: project
+ title: Project Card | Open Government
+ card_tags:
+ - open gov
+ - type: layout_cards
+ title: Project Card | Open Data
+ card_type: project
+ card_tags:
+ - open data
+ - type: layout_cards
+ title: Project Card | Open Source
+ card_type: project
+ card_tags:
+ - open source
+ - type: layout_cards
+ title: Job Card
+ card_type: job
+ - type: layout_cards
+ title: Event Card
+ card_type: event
+---
diff --git a/homepage/_pages/zh-tw/cards.md b/homepage/_pages/zh-tw/cards.md
new file mode 100644
index 00000000..3f6e3957
--- /dev/null
+++ b/homepage/_pages/zh-tw/cards.md
@@ -0,0 +1,28 @@
+---
+unique_slug: cards
+name: 卡片頁
+layout_list:
+ - type: layout_headline
+ title: 卡片介紹
+ - type: layout_cards
+ card_type: project
+ title: 專案卡 | 開放政府專案
+ card_tags:
+ - open gov
+ - type: layout_cards
+ title: 專案卡 | 開放資料專案
+ card_type: project
+ card_tags:
+ - open data
+ - type: layout_cards
+ title: 專案卡 | 開放原始碼專案
+ card_type: project
+ card_tags:
+ - open source
+ - type: layout_cards
+ title: 人力卡
+ card_type: job
+ - type: layout_cards
+ title: 事件卡
+ card_type: event
+---
diff --git a/homepage/src/components/cards.jsx b/homepage/src/components/cards.jsx
new file mode 100644
index 00000000..12122913
--- /dev/null
+++ b/homepage/src/components/cards.jsx
@@ -0,0 +1,35 @@
+import { ParseMarkdownAndHtml } from './parseMarkdownAndHtml';
+
+const Cards = ({ id, title, cards }) => {
+ return (
+
+
+
+
{title}
+
+
+ {cards.map((card) => (
+
+ ))}
+
+
+
+ );
+};
+
+const Card = ({ card }) => (
+
+
+
{card.data.title}
+
+
+
{card.data.description}
+
+ {card.content}
+
+
+
+
+);
+
+export default Cards;
diff --git a/homepage/src/layouts/contentMapper.jsx b/homepage/src/layouts/contentMapper.jsx
index a1723ac8..e703bbe6 100644
--- a/homepage/src/layouts/contentMapper.jsx
+++ b/homepage/src/layouts/contentMapper.jsx
@@ -4,76 +4,25 @@ import ThreeColumns from '../components/threeColumns';
import ImageAndText from '../components/imageAndText';
import Section from '../components/section';
import Headline from '../components/headline';
+import Cards from '../components/cards';
+import { componentTypes } from '../lib/componentMapper';
-const contentMapper = (layout) => {
- switch (layout.type) {
- case 'layout_banner':
- return (
-
- );
- case 'layout_section':
- if (layout.columns?.length === 2) {
- return (
-
- );
- } else if (layout.columns?.length === 3) {
- return (
-
- );
- } else {
- return (
-
- );
- }
- case 'layout_image_text':
- return (
-
- );
- case 'layout_headline':
- return (
-
- );
+const contentMapper = (component) => {
+ switch (component.type) {
+ case componentTypes.Banner:
+ return ;
+ case componentTypes.Headline:
+ return ;
+ case componentTypes.ImageAndText:
+ return ;
+ case componentTypes.OneColumn:
+ return ;
+ case componentTypes.TwoColumns:
+ return ;
+ case componentTypes.ThreeColumns:
+ return ;
+ case componentTypes.Cards:
+ return ;
default:
return null;
}
diff --git a/homepage/src/lib/componentMapper.js b/homepage/src/lib/componentMapper.js
new file mode 100644
index 00000000..2e233082
--- /dev/null
+++ b/homepage/src/lib/componentMapper.js
@@ -0,0 +1,121 @@
+import { titleToAnchorId } from './titleToAnchorId';
+
+const removeUndefined = (obj) => {
+ const newObj = {};
+ Object.keys(obj).forEach((key) => {
+ if (obj[key] !== undefined) {
+ newObj[key] = obj[key];
+ }
+ });
+ return newObj;
+};
+
+export const componentTypes = {
+ Banner: 'Banner',
+ Headline: 'Headline',
+ OneColumn: 'OneColumn',
+ TwoColumns: 'TwoColumns',
+ ThreeColumns: 'ThreeColumns',
+ ImageAndText: 'ImageAndText',
+ Cards: 'Cards',
+};
+
+export const componentMapper = (layout, cards = []) => {
+ let type = '';
+ let props = {};
+ switch (layout.type) {
+ case 'layout_banner': {
+ type = componentTypes.Banner;
+ props = {
+ id: titleToAnchorId(layout.title),
+ title: layout.title,
+ subtitle: layout.subtitle,
+ heroImage: layout.hero_image,
+ highlights: layout.highlights,
+ };
+ break;
+ }
+ case 'layout_headline': {
+ type = componentTypes.Headline;
+ props = {
+ id: titleToAnchorId(layout.title),
+ title: layout.title,
+ subtitle: layout.subtitle,
+ };
+ break;
+ }
+ case 'layout_image_text': {
+ type = componentTypes.ImageAndText;
+ props = {
+ id: titleToAnchorId(layout.title),
+ title: layout.title,
+ subtitle: layout.subtitle,
+ image: layout.image,
+ content: layout.text,
+ highlights: layout.highlights,
+ markdown: true,
+ };
+ break;
+ }
+ case 'layout_section': {
+ if (layout.columns?.length <= 1) {
+ type = componentTypes.OneColumn;
+ props = {
+ id: titleToAnchorId(layout.title),
+ title: layout.title,
+ subtitle: layout.columns?.[0]?.title,
+ image: layout.columns?.[0]?.image,
+ content: layout.columns?.[0]?.text,
+ markdown: true,
+ };
+ break;
+ } else if (layout.columns?.length === 2) {
+ type = componentTypes.TwoColumns;
+ props = {
+ id: titleToAnchorId(layout.title),
+ title: layout.title,
+ columns: layout.columns,
+ markdown: true,
+ };
+ break;
+ } else if (layout.columns?.length >= 3) {
+ type = componentTypes.ThreeColumns;
+ props = {
+ id: titleToAnchorId(layout.title),
+ title: layout.title,
+ columns: layout.columns,
+ markdown: true,
+ };
+ break;
+ }
+ }
+ case 'layout_cards': {
+ let filteredCards = cards;
+ // filter cards by card_type
+ if (layout.card_type) {
+ filteredCards = cards.filter(
+ (card) => layout.card_type === card.data.type,
+ );
+ }
+ // filter cards by card_tags
+ if (layout.card_tags && layout.card_tags.length > 0) {
+ filteredCards = filteredCards.filter((card) =>
+ layout.card_tags.every((tag) => card.data.tags?.includes(tag)),
+ );
+ }
+
+ type = componentTypes.Cards;
+ props = {
+ id: titleToAnchorId(layout.title),
+ title: layout.title,
+ cards: filteredCards,
+ };
+ break;
+ }
+ }
+
+ return {
+ type,
+ props: removeUndefined(props),
+ };
+};
diff --git a/homepage/src/lib/fetchCards.js b/homepage/src/lib/fetchCards.js
index aa44a7f8..d101ea94 100644
--- a/homepage/src/lib/fetchCards.js
+++ b/homepage/src/lib/fetchCards.js
@@ -2,9 +2,6 @@ import fs from 'fs';
import { join } from 'path';
import matter from 'gray-matter';
-import { remark } from 'remark';
-import html from 'remark-html';
-
const cardsDirectory = join(process.cwd(), '_cards');
/**
@@ -16,24 +13,18 @@ function getCardDirectoryPath(lang) {
return join(cardsDirectory, lang);
}
-export async function fetchCards(lang) {
+export function fetchCards(lang) {
const cardsDirectory = getCardDirectoryPath(lang);
const filesInCards = fs.readdirSync(cardsDirectory);
- const cards = filesInCards.map(async (filename) => {
+ const cards = filesInCards.map((filename) => {
const fullPath = join(cardsDirectory, filename);
const file = fs.readFileSync(fullPath, 'utf8');
const matterFile = matter(file);
const { data, content } = matterFile;
- const processedContent = await remark().use(html).process(content);
- const contentHtml = processedContent.toString();
-
- return {
- frontMatter: data,
- content: contentHtml,
- };
+ return { data, content };
});
- return Promise.all(cards);
+ return cards;
}
diff --git a/homepage/src/lib/fetchPage.js b/homepage/src/lib/fetchPage.js
index 68b737e6..28697f63 100644
--- a/homepage/src/lib/fetchPage.js
+++ b/homepage/src/lib/fetchPage.js
@@ -1,7 +1,6 @@
import fs from 'fs';
import { join } from 'path';
import matter from 'gray-matter';
-import { titleToAnchorId } from './titleToAnchorId';
const pagesDirectory = join(process.cwd(), '_pages');
@@ -16,10 +15,6 @@ export function fetchPage(lang, page) {
const fileContent = fs.readFileSync(filePath, 'utf8');
const { data } = matter(fileContent);
- data['layout_list']?.forEach((layout) => {
- layout.id = layout.id || titleToAnchorId(layout.title);
- });
-
return {
data,
};
diff --git a/homepage/src/lib/getNavigationList.js b/homepage/src/lib/getNavigationList.js
index ec67ce8b..1b01536b 100644
--- a/homepage/src/lib/getNavigationList.js
+++ b/homepage/src/lib/getNavigationList.js
@@ -29,10 +29,6 @@ export async function getPagesList(lang) {
export async function getNavigationList(lang) {
const pagesList = await getPagesList(lang);
- const cardsPage = {
- en: 'Cards',
- 'zh-tw': '卡片頁',
- };
// * dynamic page navigation
const navigationList = pagesList
@@ -41,8 +37,6 @@ export async function getNavigationList(lang) {
page.path = page.path.replace('index', '');
return { link: `/${page.path}`, text: page.name };
});
- // add hard coded cards page
- navigationList.push({ link: `/cards`, text: cardsPage[lang] });
return navigationList;
}
diff --git a/homepage/src/lib/markdownToHtml.js b/homepage/src/lib/markdownToHtml.js
new file mode 100644
index 00000000..5139f40d
--- /dev/null
+++ b/homepage/src/lib/markdownToHtml.js
@@ -0,0 +1,7 @@
+import { remark } from 'remark';
+import html from 'remark-html';
+
+export async function markdownToHtml(markdown) {
+ const result = await remark().use(html).process(markdown);
+ return result.toString();
+}
diff --git a/homepage/src/lib/titleToAnchorId.js b/homepage/src/lib/titleToAnchorId.js
index d55bdc23..2728c4f2 100644
--- a/homepage/src/lib/titleToAnchorId.js
+++ b/homepage/src/lib/titleToAnchorId.js
@@ -6,7 +6,7 @@ const cleanTitle = (title) => {
.trim();
};
-export const titleToAnchorId = (title) => {
+export const titleToAnchorId = (title = '') => {
let anchorId = title.toLowerCase(); // lowercase
anchorId = cleanTitle(anchorId); // remove special characters
anchorId = anchorId.normalize('NFD').replace(/[\u0300-\u036f]/g, ''); // remove accents
diff --git a/homepage/src/pages/admin/config.json b/homepage/src/pages/admin/config.json
index 21076ccd..277215b2 100644
--- a/homepage/src/pages/admin/config.json
+++ b/homepage/src/pages/admin/config.json
@@ -23,6 +23,8 @@
"description": "Pages in the website",
"folder": "homepage/_pages",
"slug": "{{unique_slug}}",
+ "identifier_field": "unique_slug",
+ "preview_path": "{{unique_slug}}",
"create": true,
"i18n": true,
"fields": [
@@ -43,6 +45,7 @@
"label": "Layout - Banner",
"name": "layout_banner",
"widget": "object",
+ "summary": "{{fields.title}}",
"fields": [
{
"label": "Background Hero Image",
@@ -68,6 +71,7 @@
"label": "Layout - Headline",
"name": "layout_headline",
"widget": "object",
+ "summary": "{{fields.title}}",
"fields": [
{ "label": "Title", "name": "title", "widget": "string" },
{
@@ -82,6 +86,7 @@
"label": "Layout - Image & Text",
"name": "layout_image_text",
"widget": "object",
+ "summary": "{{fields.title}}",
"fields": [
{ "label": "Image", "name": "image", "widget": "image" },
{ "label": "Title", "name": "title", "widget": "string" },
@@ -122,6 +127,7 @@
"label": "Layout - Section",
"name": "layout_section",
"widget": "list",
+ "summary": "{{fields.title}}",
"fields": [
{ "label": "Title", "name": "title", "widget": "string" },
{
@@ -151,6 +157,47 @@
]
}
]
+ },
+ {
+ "label": "Layout - Cards",
+ "name": "layout_cards",
+ "widget": "object",
+ "summary": "{{fields.title}}",
+ "fields": [
+ { "label": "Title", "name": "title", "widget": "string" },
+ {
+ "label": "Card type",
+ "name": "card_type",
+ "widget": "select",
+ "options": [
+ { "label": "專案", "value": "project" },
+ { "label": "人力", "value": "job" },
+ { "label": "事件", "value": "event" }
+ ],
+ "required": false
+ },
+ {
+ "label": "Card tags",
+ "name": "card_tags",
+ "widget": "select",
+ "multiple": true,
+ "options": [
+ { "label": "開放政府", "value": "open gov" },
+ { "label": "開放資料", "value": "open data" },
+ { "label": "開放原始碼", "value": "open source" },
+ { "label": "基礎", "value": "basic" },
+ { "label": "進階", "value": "advance" },
+ { "label": "工程師", "value": "engineer" },
+ { "label": "美術設計", "value": "designer" },
+ { "label": "文字工作者", "value": "writer" },
+ { "label": "行銷公關", "value": "marketing" },
+ { "label": "議題工作者", "value": "advocator" },
+ { "label": "公務員", "value": "civil servants" },
+ { "label": "法務人員", "value": "legal" }
+ ],
+ "required": false
+ }
+ ]
}
]
}
@@ -164,10 +211,26 @@
"folder": "homepage/_cards",
"create": true,
"i18n": true,
- "view_groups": [
- { "label": "Drafts", "field": "draft" },
- { "label": "Type", "field": "type" }
+ "view_groups": [{ "label": "Type", "field": "type" }],
+ "view_filters": [
+ { "label": "專案", "field": "type", "pattern": "project" },
+ { "label": "人力", "field": "type", "pattern": "job" },
+ { "label": "事件", "field": "type", "pattern": "event" },
+
+ { "label": "開放政府", "field": "tags", "pattern": "open gov" },
+ { "label": "開放資料", "field": "tags", "pattern": "open data" },
+ { "label": "開放原始碼", "field": "tags", "pattern": "open source" },
+ { "label": "基礎", "field": "tags", "pattern": "basic" },
+ { "label": "進階", "field": "tags", "pattern": "advance" },
+ { "label": "工程師", "field": "tags", "pattern": "engineer" },
+ { "label": "美術設計", "field": "tags", "pattern": "designer" },
+ { "label": "文字工作者", "field": "tags", "pattern": "writer" },
+ { "label": "行銷公關", "field": "tags", "pattern": "marketing" },
+ { "label": "議題工作者", "field": "tags", "pattern": "advocator" },
+ { "label": "公務員", "field": "tags", "pattern": "civil servants" },
+ { "label": "法務人員", "field": "tags", "pattern": "legal" }
],
+ "summary": "{{fields.type}} / {{fields.title}}",
"fields": [
{
"label": "Image",
@@ -288,9 +351,10 @@
"label": "Links",
"label_singular": "Link",
"widget": "list",
+ "summary": "{{fields.type} - {{fields.url}}",
"fields": [
{
- "label": "Type`",
+ "label": "Type",
"name": "type",
"widget": "select",
"options": [
@@ -337,6 +401,7 @@
"label_singular": "Link",
"widget": "list",
"i18n": true,
+ "summary": "{{fields.display_text}} - {{fields.url}}",
"fields": [
{
"label": "Display Text",
diff --git a/homepage/src/pages/admin/index.jsx b/homepage/src/pages/admin/index.jsx
index f0e95736..de4e855e 100644
--- a/homepage/src/pages/admin/index.jsx
+++ b/homepage/src/pages/admin/index.jsx
@@ -3,116 +3,15 @@ import Head from 'next/head';
import Script from 'next/script';
import config from './config.json';
-import Banner from '../../components/banner';
-import ImageAndText from '../../components/imageAndText';
-import Headline from '../../components/headline';
-import Section from '../../components/section';
-import TwoColumns from '../../components/twoColumns';
-import ThreeColumns from '../../components/threeColumns';
import FooterLinks from '../../layouts/footer/footerLinks';
+import { componentMapper } from '../../lib/componentMapper';
+import contentMapper from '../../layouts/contentMapper';
const PagePreview = ({ entry }) => {
const layoutList = entry.getIn(['data', 'layout_list']);
const sections = layoutList?.map((layout) => {
- const layoutType = layout.get('type')?.toString();
- if (layoutType === 'layout_banner') {
- return (
-
- );
- } else if (layoutType === 'layout_image_text') {
- const highlights = layout
- .get('highlights')
- ?.map((highlight) => {
- return {
- item: highlight.get('item')?.toString(),
- description: highlight.get('description')?.toString(),
- };
- })
- ?.toArray();
- return (
-
- );
- } else if (layoutType === 'layout_headline') {
- return (
-
- );
- } else if (layoutType === 'layout_section') {
- const columnSize = layout.get('columns')?.size ?? 0;
- if (columnSize === 0 || columnSize === 1) {
- return (
-
- );
- } else if (columnSize === 2) {
- const columns = layout
- .get('columns')
- ?.map((column) => {
- return {
- title: column.get('title')?.toString(),
- image: column.get('image')?.toString(),
- text: column.get('text')?.toString(),
- };
- })
- .toArray();
- return (
-
- );
- } else if (columnSize >= 3) {
- const columns = layout
- .get('columns')
- ?.map((column) => {
- return {
- title: column.get('title')?.toString(),
- image: column.get('image')?.toString(),
- text: column.get('text')?.toString(),
- };
- })
- .toArray();
- return (
-
- );
- }
- } else {
- return ;
- }
+ const component = componentMapper(layout.toJS(), []);
+ return contentMapper(component);
});
return {sections}
;
};
diff --git a/homepage/src/pages/cards.jsx b/homepage/src/pages/cards.jsx
index 04dc9aff..73b97c70 100644
--- a/homepage/src/pages/cards.jsx
+++ b/homepage/src/pages/cards.jsx
@@ -1,73 +1,42 @@
import Head from 'next/head';
-import Headline from '../components/headline';
-import CardsGrid from '../components/cardsGrid';
-
+import contentMapper from '../layouts/contentMapper';
+import { fetchPage } from '../lib/fetchPage';
import { fetchCards } from '../lib/fetchCards';
import { getNavigationList } from '../lib/getNavigationList';
+import { componentMapper } from '../lib/componentMapper';
+import { titleToAnchorId } from '../lib/titleToAnchorId';
/**
*
* @type {import('next').GetStaticProps}
*/
export const getStaticProps = async ({ locale }) => {
- const cards = await fetchCards(locale);
-
- // Get correct image path
- const updatedCards = cards.map((card) => {
- const updatedImage = card.frontMatter.image
- ? card.frontMatter.image.replace('/homepage/public', '')
- : '/images/uploads/初階專案卡封面-01.png';
+ const rawCards = fetchCards(locale);
+ const cardTasks = rawCards.map(async ({ data, content }) => {
+ let image = data.image;
+ const defaultImage = '/images/uploads/初階專案卡封面-01.png';
+ image = image ? image.replace('/homepage/public', '') : defaultImage;
// Workaround for image prefix path. Will be removed after image path is fixed in all cards.
- const image = !updatedImage.startsWith('/')
- ? `/${updatedImage}`
- : updatedImage;
+ image = !image.startsWith('/') ? `/${image}` : image;
+
+ data.image = image;
+ data.id = data.id || titleToAnchorId(data.title);
return {
- ...card,
- frontMatter: {
- ...card.frontMatter,
- image,
- },
+ data,
+ content,
};
});
+ const cards = await Promise.all(cardTasks);
const navigationList = await getNavigationList(locale);
- const title = {
- en: 'Card Introduction',
- 'zh-tw': '卡片介紹',
- };
-
- const projectCardTitle = {
- en: 'Project Card',
- 'zh-tw': '專案卡',
- };
-
- const jobCardTitle = {
- en: 'Job Card',
- 'zh-tw': '人力卡',
- };
-
- const eventCardTitle = {
- en: 'Event Card',
- 'zh-tw': '事件卡',
- };
+ const page = fetchPage(locale, 'cards');
- const projectCardSubtitle = {
- 'open gov': {
- en: 'Open Government',
- 'zh-tw': '開放政府專案',
- },
- 'open data': {
- en: 'Open Data',
- 'zh-tw': '開放資料專案',
- },
- 'open source': {
- en: 'Open Source',
- 'zh-tw': '開放原始碼專案',
- },
- };
+ const contentList = page.data['layout_list']?.map((layout) =>
+ componentMapper(layout, cards),
+ );
const headInfo = {
title: {
@@ -78,76 +47,25 @@ export const getStaticProps = async ({ locale }) => {
return {
props: {
- cards: updatedCards,
navigationList,
headInfo: {
title: headInfo.title[locale],
},
- title: title[locale],
- projectCardTitle: projectCardTitle[locale],
- jobCardTitle: jobCardTitle[locale],
- eventCardTitle: eventCardTitle[locale],
- projectCardSubtitle: {
- 'open gov': projectCardSubtitle['open gov'][locale],
- 'open data': projectCardSubtitle['open data'][locale],
- 'open source': projectCardSubtitle['open source'][locale],
+ page: {
+ contentList,
},
},
};
};
-const cards = ({
- cards,
- headInfo,
- title,
- projectCardTitle,
- jobCardTitle,
- eventCardTitle,
- projectCardSubtitle,
-}) => {
+const cards = ({ headInfo, page }) => {
return (
<>
{headInfo.title}
-
+
-
-
-
-
-
-
+ {page.contentList?.map(contentMapper)}
>
);
};
diff --git a/homepage/src/pages/index.jsx b/homepage/src/pages/index.jsx
index 450cc941..570b199a 100644
--- a/homepage/src/pages/index.jsx
+++ b/homepage/src/pages/index.jsx
@@ -3,6 +3,7 @@ import Script from 'next/script';
import { fetchPage } from '../lib/fetchPage';
import { getNavigationList } from '../lib/getNavigationList';
import contentMapper from '../layouts/contentMapper';
+import { componentMapper } from '../lib/componentMapper';
/**
*
@@ -23,6 +24,9 @@ export const getStaticProps = async ({ locale }) => {
};
const page = fetchPage(locale, 'index');
+ const contentList = page.data['layout_list']?.map((layout) =>
+ componentMapper(layout, []),
+ );
return {
props: {
@@ -31,7 +35,9 @@ export const getStaticProps = async ({ locale }) => {
title: headInfo.title[locale],
description: headInfo.description[locale],
},
- page,
+ page: {
+ contentList,
+ },
},
};
};
@@ -63,7 +69,7 @@ const Index = ({ headInfo, page }) => (
`,
}}
/>
- {page.data['layout_list']?.map(contentMapper)}
+ {page.contentList?.map(contentMapper)}
>
);
diff --git a/homepage/src/pages/resource.jsx b/homepage/src/pages/resource.jsx
index 24dbeee8..5e15f2e9 100644
--- a/homepage/src/pages/resource.jsx
+++ b/homepage/src/pages/resource.jsx
@@ -1,4 +1,5 @@
import contentMapper from '../layouts/contentMapper';
+import { componentMapper } from '../lib/componentMapper';
import { fetchPage } from '../lib/fetchPage';
import { getNavigationList } from '../lib/getNavigationList';
@@ -8,16 +9,21 @@ import { getNavigationList } from '../lib/getNavigationList';
*/
export const getStaticProps = async ({ locale }) => {
const page = fetchPage(locale, 'resource');
+ const contentList = page.data['layout_list']?.map((layout) =>
+ componentMapper(layout, []),
+ );
const navigationList = await getNavigationList(locale);
return {
props: {
- page,
+ page: {
+ contentList,
+ },
navigationList,
},
};
};
export default function Resource({ page }) {
- return <>{page.data['layout_list']?.map(contentMapper)}>;
+ return <>{page.contentList?.map(contentMapper)}>;
}