From 08bab4e6058022c5b9933b4af02bdadf23da427b Mon Sep 17 00:00:00 2001 From: Ilko Kacharov Date: Wed, 23 Nov 2022 21:53:11 +0200 Subject: [PATCH] 1158 Embed ghost blog articles in frontend app (#1161) * Added ghost api as dependency * Update date utils * Added blog pages fetched from remote ghost instance * Switch inline styles to emotion styles * Update sentry dependency * Update sitemaps * Capture blog post exceptions * Update testing-library to work with react 18 * Hide sentry sourcemaps https://docs.sentry.io/platforms/javascript/guides/nextjs/manual-setup/\#use-hidden-source-map * Setup env during builds in check-pr workflow * Use ghost key from repo secret * Copy env prior build * Added more dockerfile args * Update release.yml * Wrap env usage in workflows on check pr and release * Remove ghost envs from build args * Revert "Remove ghost envs from build args" This reverts commit df0456fe18c33960d551642cb56ac603b474b046. --- .env.local.example | 7 +- .github/workflows/check-pr.yml | 7 +- .github/workflows/playwright.yml | 8 +- .github/workflows/release.yml | 6 +- Dockerfile | 4 + manifests/base/config-web.yaml | 2 + manifests/base/deployment.yaml | 10 + next.config.js | 8 +- package.json | 10 +- public/locales/bg/blog.json | 10 + public/locales/bg/common.json | 3 +- public/locales/en/blog.json | 10 + public/locales/en/common.json | 3 +- public/sitemap-0.xml | 93 +++-- sentry.client.config.ts | 2 +- src/common/routes.ts | 12 +- src/common/util/date.ts | 22 +- src/common/util/ghost-client.ts | 9 + src/components/admin/InfoRequestGrid.tsx | 4 +- src/components/admin/SupportersGrid.tsx | 4 +- .../auth/profile/MyCampaignsTable.tsx | 16 +- .../profile/MyDonatedToCampaignsTable.tsx | 16 +- .../auth/profile/PersonalInfoTab.tsx | 2 +- src/components/blog/BlogIndexPage.tsx | 65 +++ src/components/blog/BlogPage.tsx | 58 +++ src/components/blog/BlogPostPage.tsx | 58 +++ src/components/blog/DateCreated.tsx | 19 + src/components/blog/FeaturedImage.tsx | 57 +++ src/components/blog/ReadingTime.tsx | 18 + src/components/blog/RenderContent.tsx | 32 ++ .../campaigns/grid/CampaignGrid.tsx | 16 +- .../layout/Footer/helpers/FooterData.tsx | 2 +- src/components/layout/Layout.tsx | 28 +- src/components/layout/nav/ProjectMenu.tsx | 5 +- src/components/modal/DetailsModal.tsx | 6 +- src/components/navigation/BackButton.tsx | 17 + src/components/person/PersonInfo.tsx | 4 +- src/pages/blog/[slug].tsx | 46 ++ src/pages/blog/index.tsx | 31 ++ src/pages/page/[slug].tsx | 46 ++ yarn.lock | 395 +++++++++++------- 41 files changed, 919 insertions(+), 252 deletions(-) create mode 100644 public/locales/bg/blog.json create mode 100644 public/locales/en/blog.json create mode 100644 src/common/util/ghost-client.ts create mode 100644 src/components/blog/BlogIndexPage.tsx create mode 100644 src/components/blog/BlogPage.tsx create mode 100644 src/components/blog/BlogPostPage.tsx create mode 100644 src/components/blog/DateCreated.tsx create mode 100644 src/components/blog/FeaturedImage.tsx create mode 100644 src/components/blog/ReadingTime.tsx create mode 100644 src/components/blog/RenderContent.tsx create mode 100644 src/components/navigation/BackButton.tsx create mode 100644 src/pages/blog/[slug].tsx create mode 100644 src/pages/blog/index.tsx create mode 100644 src/pages/page/[slug].tsx diff --git a/.env.local.example b/.env.local.example index 56fcb5482..632decde9 100644 --- a/.env.local.example +++ b/.env.local.example @@ -33,4 +33,9 @@ GOOGLE_SECRET= ## Paypal ## ############## # using 'sb' as sandbox. no need to put real sandbox key if you don't plan to test requests with backend webhooks -PAYPAL_CLIENT_ID=sb \ No newline at end of file +PAYPAL_CLIENT_ID=sb + +## Ghost ## +########### +GHOST_API_URL=https://blog.podkrepi.bg +GHOST_CONTENT_KEY=86ec17c4b9660acd66b6034682 diff --git a/.github/workflows/check-pr.yml b/.github/workflows/check-pr.yml index b53731941..f13766d3b 100644 --- a/.github/workflows/check-pr.yml +++ b/.github/workflows/check-pr.yml @@ -18,10 +18,15 @@ jobs: env: NODE_ENV: production SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }} + GHOST_API_URL: https://blog.podkrepi.bg + GHOST_CONTENT_KEY: ${{ secrets.GHOST_CONTENT_KEY }} with: push: false target: production - build-args: SENTRY_AUTH_TOKEN=$SENTRY_AUTH_TOKEN + build-args: | + SENTRY_AUTH_TOKEN=${{ env.SENTRY_AUTH_TOKEN }} + GHOST_API_URL=${{ env.GHOST_API_URL }} + GHOST_CONTENT_KEY=${{ env.GHOST_CONTENT_KEY }} tags: ghcr.io/podkrepi-bg/frontend:pr - name: Scan with Mondoo diff --git a/.github/workflows/playwright.yml b/.github/workflows/playwright.yml index 1c7029c16..06ccd074f 100644 --- a/.github/workflows/playwright.yml +++ b/.github/workflows/playwright.yml @@ -51,14 +51,14 @@ jobs: working-directory: ./frontend run: yarn - - name: Build frontend - working-directory: ./frontend - run: yarn run next build - - name: Setup env working-directory: ./frontend run: cp .env.local.example .env.local + - name: Build frontend + working-directory: ./frontend + run: yarn run next build + - name: Wait on backend uses: iFaxity/wait-on-action@v1 with: diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index e8b562b42..418cbf65d 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -68,11 +68,15 @@ jobs: env: NODE_ENV: production SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }} + GHOST_API_URL: https://blog.podkrepi.bg + GHOST_CONTENT_KEY: ${{ secrets.GHOST_CONTENT_KEY }} with: push: true target: production build-args: | - SENTRY_AUTH_TOKEN=$SENTRY_AUTH_TOKEN + SENTRY_AUTH_TOKEN=${{ env.SENTRY_AUTH_TOKEN }} + GHOST_API_URL=${{ env.GHOST_API_URL }} + GHOST_CONTENT_KEY=${{ env.GHOST_CONTENT_KEY }} VERSION=${{ env.VERSION }} tags: ghcr.io/podkrepi-bg/frontend:${{ env.VERSION }} diff --git a/Dockerfile b/Dockerfile index e5c0cfded..d5858d168 100644 --- a/Dockerfile +++ b/Dockerfile @@ -33,6 +33,10 @@ FROM base AS builder ARG VERSION=unversioned ARG SENTRY_AUTH_TOKEN ENV SENTRY_AUTH_TOKEN="$SENTRY_AUTH_TOKEN" +ARG GHOST_API_URL +ENV GHOST_API_URL="$GHOST_API_URL" +ARG GHOST_CONTENT_KEY +ENV GHOST_CONTENT_KEY="$GHOST_CONTENT_KEY" COPY --from=dependencies /app/node_modules /app/node_modules COPY . /app diff --git a/manifests/base/config-web.yaml b/manifests/base/config-web.yaml index e71277bd1..65cb7cb8b 100644 --- a/manifests/base/config-web.yaml +++ b/manifests/base/config-web.yaml @@ -6,3 +6,5 @@ data: app-url: https://podkrepi.bg api-url: https://podkrepi.bg/api/v1 image-host: podkrepi.bg + ghost-api-url: https://blog.podkrepi.bg + ghost-content-key: 86ec17c4b9660acd66b6034682 diff --git a/manifests/base/deployment.yaml b/manifests/base/deployment.yaml index dddc0b926..5f99e55e9 100644 --- a/manifests/base/deployment.yaml +++ b/manifests/base/deployment.yaml @@ -101,6 +101,16 @@ spec: secretKeyRef: name: secrets-web key: paypal-client-id + - name: GHOST_API_URL + valueFrom: + configMapKeyRef: + name: config-web + key: ghost-api-url + - name: GHOST_CONTENT_KEY + valueFrom: + configMapKeyRef: + name: config-web + key: ghost-content-key ports: - containerPort: 3040 resources: diff --git a/next.config.js b/next.config.js index 48d4a7baf..7e3355c8a 100644 --- a/next.config.js +++ b/next.config.js @@ -36,8 +36,14 @@ const moduleExports = { CAMPAIGN: process.env.FEATURE_CAMPAIGN ?? false, }, }, + sentry: { + hideSourceMaps: true, + }, images: { - domains: [process.env.IMAGE_HOST ?? 'localhost'], + domains: [ + process.env.IMAGE_HOST ?? 'localhost', + process.env.GHOST_API_URL?.replace('https://', '') || 'blog.podkrepi.bg', + ], }, async redirects() { return [ diff --git a/package.json b/package.json index ff0e95fb0..d684253ae 100644 --- a/package.json +++ b/package.json @@ -6,7 +6,7 @@ "scripts": { "predev": "shx test -e .env.local && exit 0 || shx echo 'You need to create .env.local file. Please check README.md!' && exit 1", "dev": "yarn && next dev -p 3040", - "build": "yarn format && next build", + "build": "yarn && next build", "start": "next start -p 3040", "test": "jest --env=jsdom", "test:e2e": "playwright test", @@ -38,9 +38,11 @@ "@next/bundle-analyzer": "^12.1.0", "@paypal/react-paypal-js": "^7.8.1", "@react-pdf/renderer": "^3.0.1", - "@sentry/nextjs": "6.17.8", + "@sentry/nextjs": "7.21.1", "@tanstack/react-query": "^4.16.1", + "@tryghost/content-api": "^1.11.4", "@types/react-slick": "^0.23.10", + "@types/tryghost__content-api": "^1.3.11", "@uppy/aws-s3": "2.0.5", "@uppy/core": "2.1.1", "@uppy/facebook": "2.0.4", @@ -83,8 +85,8 @@ }, "devDependencies": { "@playwright/test": "^1.24.2", - "@testing-library/jest-dom": "^5.16.1", - "@testing-library/react": "^12.1.2", + "@testing-library/jest-dom": "^5.16.5", + "@testing-library/react": "^13.4.0", "@types/cookie": "^0.4.1", "@types/lodash.truncate": "^4.4.7", "@types/lru-cache": "^5.1.1", diff --git a/public/locales/bg/blog.json b/public/locales/bg/blog.json new file mode 100644 index 000000000..515e0ba51 --- /dev/null +++ b/public/locales/bg/blog.json @@ -0,0 +1,10 @@ +{ + "title": "Блог", + "description": "Място за истории, идеи и добри каузи.", + "created-on": "Създаден на:", + "reading-time": { + "label": "Време за прочитане:", + "count_one": "{{count}} минутa", + "count_other": "{{count}} минути" + } +} diff --git a/public/locales/bg/common.json b/public/locales/bg/common.json index 28e16fcf9..90d8e9ee0 100644 --- a/public/locales/bg/common.json +++ b/public/locales/bg/common.json @@ -38,7 +38,8 @@ "logout": "Изход", "register": "Регистрация", "changePassword": "Смяна на парола", - "forgottenPassword": "Забравена парола?" + "forgottenPassword": "Забравена парола?", + "go-back": "Назад" }, "meta": { "title": "Подкрепи.бг" diff --git a/public/locales/en/blog.json b/public/locales/en/blog.json new file mode 100644 index 000000000..56cec7974 --- /dev/null +++ b/public/locales/en/blog.json @@ -0,0 +1,10 @@ +{ + "title": "Blog", + "description": "A place for stories, ideas and good causes.", + "created-on": "Created on:", + "reading-time": { + "label": "Reading time:", + "count_one": "{{count}} minute", + "count_other": "{{count}} minutes" + } +} diff --git a/public/locales/en/common.json b/public/locales/en/common.json index 9cdbf49ce..cb08fffa0 100644 --- a/public/locales/en/common.json +++ b/public/locales/en/common.json @@ -38,7 +38,8 @@ "logout": "Logout", "register": "Signup", "changePassword": "Change your password", - "forgottenPassword": "Forgotten password?" + "forgottenPassword": "Forgotten password?", + "go-back": "Go back" }, "meta": { "title": "Podkrepi.bg" diff --git a/public/sitemap-0.xml b/public/sitemap-0.xml index 5e163df5b..f4def4a9b 100644 --- a/public/sitemap-0.xml +++ b/public/sitemap-0.xml @@ -1,38 +1,61 @@ -https://podkrepi.bgdaily0.72022-07-29T19:20:59.384Z -https://podkrepi.bg/aboutdaily0.72022-07-29T19:20:59.384Z -https://podkrepi.bg/about-projectdaily0.72022-07-29T19:20:59.385Z -https://podkrepi.bg/campaignsdaily0.72022-07-29T19:20:59.385Z -https://podkrepi.bg/campaigns/createdaily0.72022-07-29T19:20:59.385Z -https://podkrepi.bg/change-passworddaily0.72022-07-29T19:20:59.385Z -https://podkrepi.bg/chatdaily0.72022-07-29T19:20:59.385Z -https://podkrepi.bg/contactdaily0.72022-07-29T19:20:59.385Z -https://podkrepi.bg/faqdaily0.72022-07-29T19:20:59.385Z -https://podkrepi.bg/forgotten-passworddaily0.72022-07-29T19:20:59.385Z -https://podkrepi.bg/logindaily0.72022-07-29T19:20:59.385Z -https://podkrepi.bg/logoutdaily0.72022-07-29T19:20:59.385Z -https://podkrepi.bg/privacy-policydaily0.72022-07-29T19:20:59.385Z -https://podkrepi.bg/profiledaily0.72022-07-29T19:20:59.385Z -https://podkrepi.bg/registerdaily0.72022-07-29T19:20:59.385Z -https://podkrepi.bg/supportdaily0.72022-07-29T19:20:59.385Z -https://podkrepi.bg/support_usdaily0.72022-07-29T19:20:59.385Z -https://podkrepi.bg/terms-of-servicedaily0.72022-07-29T19:20:59.385Z -https://podkrepi.bg/404daily0.72022-07-29T19:20:59.385Z -https://podkrepi.bg/en/404daily0.72022-07-29T19:20:59.385Z -https://podkrepi.bg/en/contactdaily0.72022-07-29T19:20:59.385Z -https://podkrepi.bg/en/privacy-policydaily0.72022-07-29T19:20:59.385Z -https://podkrepi.bg/en/supportdaily0.72022-07-29T19:20:59.385Z -https://podkrepi.bg/en/support_usdaily0.72022-07-29T19:20:59.385Z -https://podkrepi.bg/en/terms-of-servicedaily0.72022-07-29T19:20:59.385Z -https://podkrepi.bg/en/aboutdaily0.72022-07-29T19:20:59.385Z -https://podkrepi.bg/en/about-projectdaily0.72022-07-29T19:20:59.385Z -https://podkrepi.bg/faq/common-questionsdaily0.72022-07-29T19:20:59.385Z -https://podkrepi.bg/faq/campaignsdaily0.72022-07-29T19:20:59.385Z -https://podkrepi.bg/faq/donationsdaily0.72022-07-29T19:20:59.385Z -https://podkrepi.bg/faq/recurring-donationsdaily0.72022-07-29T19:20:59.385Z -https://podkrepi.bg/faq/potential-frauddaily0.72022-07-29T19:20:59.385Z -https://podkrepi.bg/faq/attracting-donatorsdaily0.72022-07-29T19:20:59.385Z -https://podkrepi.bg/faq/corporate-partnershipdaily0.72022-07-29T19:20:59.385Z -https://podkrepi.bg/en/faqdaily0.72022-07-29T19:20:59.385Z +https://podkrepi.bgdaily0.72022-11-23T15:46:04.698Z +https://podkrepi.bg/aboutdaily0.72022-11-23T15:46:04.698Z +https://podkrepi.bg/about-projectdaily0.72022-11-23T15:46:04.698Z +https://podkrepi.bg/blogdaily0.72022-11-23T15:46:04.698Z +https://podkrepi.bg/campaignsdaily0.72022-11-23T15:46:04.698Z +https://podkrepi.bg/campaigns/createdaily0.72022-11-23T15:46:04.698Z +https://podkrepi.bg/change-passworddaily0.72022-11-23T15:46:04.698Z +https://podkrepi.bg/chatdaily0.72022-11-23T15:46:04.698Z +https://podkrepi.bg/contactdaily0.72022-11-23T15:46:04.698Z +https://podkrepi.bg/faqdaily0.72022-11-23T15:46:04.698Z +https://podkrepi.bg/finance-reportdaily0.72022-11-23T15:46:04.698Z +https://podkrepi.bg/forgotten-passworddaily0.72022-11-23T15:46:04.698Z +https://podkrepi.bg/logindaily0.72022-11-23T15:46:04.698Z +https://podkrepi.bg/logoutdaily0.72022-11-23T15:46:04.698Z +https://podkrepi.bg/privacy-policydaily0.72022-11-23T15:46:04.698Z +https://podkrepi.bg/profiledaily0.72022-11-23T15:46:04.698Z +https://podkrepi.bg/registerdaily0.72022-11-23T15:46:04.698Z +https://podkrepi.bg/supportdaily0.72022-11-23T15:46:04.698Z +https://podkrepi.bg/support_usdaily0.72022-11-23T15:46:04.698Z +https://podkrepi.bg/terms-of-servicedaily0.72022-11-23T15:46:04.698Z +https://podkrepi.bg/404daily0.72022-11-23T15:46:04.698Z +https://podkrepi.bg/en/404daily0.72022-11-23T15:46:04.698Z +https://podkrepi.bg/en/aboutdaily0.72022-11-23T15:46:04.698Z +https://podkrepi.bg/en/blogdaily0.72022-11-23T15:46:04.698Z +https://podkrepi.bg/en/about-projectdaily0.72022-11-23T15:46:04.698Z +https://podkrepi.bg/blog/yanika-novo-nachalodaily0.72022-11-23T15:46:04.698Z +https://podkrepi.bg/blog/software-na-podkriepi-bgdaily0.72022-11-23T15:46:04.698Z +https://podkrepi.bg/blog/pokana-obshto-sbranie-04-2022daily0.72022-11-23T15:46:04.698Z +https://podkrepi.bg/blog/v-pomoshch-na-postradali-ot-voinata-v-ukrainadaily0.72022-11-23T15:46:04.698Z +https://podkrepi.bg/blog/meet-georgi-malchevdaily0.72022-11-23T15:46:04.698Z +https://podkrepi.bg/blog/meet-radoslav-bozhinovdaily0.72022-11-23T15:46:04.698Z +https://podkrepi.bg/blog/purviat-vatreshien-hakaton-na-podkrepi-bg-veche-e-faktdaily0.72022-11-23T15:46:04.698Z +https://podkrepi.bg/blog/meet-radostina-goroshevichdaily0.72022-11-23T15:46:04.698Z +https://podkrepi.bg/blog/meet-ani-kalpachkadaily0.72022-11-23T15:46:04.698Z +https://podkrepi.bg/blog/meet-ivan-milchevdaily0.72022-11-23T15:46:04.698Z +https://podkrepi.bg/blog/meet-radiana-kolevadaily0.72022-11-23T15:46:04.698Z +https://podkrepi.bg/blog/meet-diana-dobrevadaily0.72022-11-23T15:46:04.698Z +https://podkrepi.bg/blog/meet-ilko-kacharovdaily0.72022-11-23T15:46:04.698Z +https://podkrepi.bg/blog/meet-stanka-cherkezovadaily0.72022-11-23T15:46:04.698Z +https://podkrepi.bg/blog/meet-martin-kovachevdaily0.72022-11-23T15:46:04.698Z +https://podkrepi.bg/blog/meet-julian-kalderondaily0.72022-11-23T15:46:04.698Z +https://podkrepi.bg/blog/meet-ivan-goychevdaily0.72022-11-23T15:46:04.698Z +https://podkrepi.bg/blog/meet-ana-nikolovadaily0.72022-11-23T15:46:04.698Z +https://podkrepi.bg/en/contactdaily0.72022-11-23T15:46:04.698Z +https://podkrepi.bg/en/faqdaily0.72022-11-23T15:46:04.698Z +https://podkrepi.bg/en/finance-reportdaily0.72022-11-23T15:46:04.698Z +https://podkrepi.bg/en/privacy-policydaily0.72022-11-23T15:46:04.698Z +https://podkrepi.bg/en/supportdaily0.72022-11-23T15:46:04.698Z +https://podkrepi.bg/en/support_usdaily0.72022-11-23T15:46:04.698Z +https://podkrepi.bg/en/terms-of-servicedaily0.72022-11-23T15:46:04.698Z +https://podkrepi.bg/page/sample-pagedaily0.72022-11-23T15:46:04.698Z +https://podkrepi.bg/faq/common-questionsdaily0.72022-11-23T15:46:04.698Z +https://podkrepi.bg/faq/campaignsdaily0.72022-11-23T15:46:04.698Z +https://podkrepi.bg/faq/donationsdaily0.72022-11-23T15:46:04.698Z +https://podkrepi.bg/faq/recurring-donationsdaily0.72022-11-23T15:46:04.698Z +https://podkrepi.bg/faq/potential-frauddaily0.72022-11-23T15:46:04.698Z +https://podkrepi.bg/faq/attracting-donatorsdaily0.72022-11-23T15:46:04.698Z +https://podkrepi.bg/faq/corporate-partnershipdaily0.72022-11-23T15:46:04.698Z \ No newline at end of file diff --git a/sentry.client.config.ts b/sentry.client.config.ts index 49d2f5d84..29153dade 100644 --- a/sentry.client.config.ts +++ b/sentry.client.config.ts @@ -8,7 +8,7 @@ const SENTRY_DSN = process.env.SENTRY_DSN || process.env.NEXT_PUBLIC_SENTRY_DSN Sentry.init({ dsn: SENTRY_DSN || 'https://e25f62860a394934878c2e21306a6b66@o540074.ingest.sentry.io/5657969', - blacklistUrls: [/localhost/, /127.0.0.1/], + denyUrls: [/localhost/, /127.0.0.1/], enabled: process.env.NODE_ENV !== 'development', // Note: if you want to override the automatic release value, do not set a // `release` value here - use the environment variable `SENTRY_RELEASE`, so diff --git a/src/common/routes.ts b/src/common/routes.ts index d4f52eb5c..f48f7932f 100644 --- a/src/common/routes.ts +++ b/src/common/routes.ts @@ -5,7 +5,11 @@ const { } = getConfig() export const baseUrl = APP_URL -export const defaultOgImage = `${baseUrl}/img/og_image.jpeg` +export const defaultOgImage = { + src: `${baseUrl}/img/og_image.jpeg`, + width: '1640', + height: '624', +} export const staticUrls = { github: @@ -15,7 +19,6 @@ export const staticUrls = { howToContribute: 'https://docs.podkrepi.bg/general/communication/faq#kak-da-se-vkliucha-v-organizaciata', devDocs: 'https://docs.podkrepi.bg/development', - blog: 'https://blog.podkrepi.bg/', hostingProvider: 'https://superhosting.bg?rel=podkrepi.bg', eduspace: 'https://eduspace-bg.business.site/', dmsBg: 'https://dmsbg.com', @@ -42,6 +45,11 @@ export const routes = { support: '/support', support_us: '/support_us', reports: '/finance-report', + blog: { + index: '/blog', + postBySlug: (slug: string) => `/blog/${slug}`, + pageBySlug: (slug: string) => `/page/${slug}`, + }, campaigns: { index: '/campaigns', create: '/campaigns/create', diff --git a/src/common/util/date.ts b/src/common/util/date.ts index 4c0ce014b..dd2914dea 100644 --- a/src/common/util/date.ts +++ b/src/common/util/date.ts @@ -1,25 +1,23 @@ import { format, formatRelative, intervalToDuration, Locale } from 'date-fns' +import { bg, enUS } from 'date-fns/locale' export const formatDate = 'dd MMM yyyy' export const formatDatetime = 'dd MMM yyyy HH:mm:ss' -export const dateFormatter = (value: Date | string | number, locale?: Locale) => { - const date = new Date(value) - const exact = format(date, formatDatetime, { locale }) - const relative = formatRelative(date, new Date(), { locale }) - return `${exact} (${relative})` -} - -export const formatDateString = (dateString: string | Date, locale?: string | undefined) => { - if (locale) { - return Intl.DateTimeFormat(locale.split('-')).format(new Date(dateString)) +export const formatDateString = (dateString: string | Date, language?: string) => { + if (language) { + return Intl.DateTimeFormat(language).format(new Date(dateString)) } return new Date(dateString).toLocaleDateString() } -export const getRelativeDate = (value: Date | string, locale?: Locale) => { +const matchLocale = (language?: string): Locale => { + return language === 'en' ? enUS : bg +} + +export const getRelativeDate = (value: Date | string, language: string) => { const date = new Date(value) - return formatRelative(date, new Date(), { locale }) + return formatRelative(date, new Date(), { locale: matchLocale(language) }) } /** diff --git a/src/common/util/ghost-client.ts b/src/common/util/ghost-client.ts new file mode 100644 index 000000000..b5a6ea31e --- /dev/null +++ b/src/common/util/ghost-client.ts @@ -0,0 +1,9 @@ +import GhostContentAPI from '@tryghost/content-api' + +export const createGhostClient = () => { + return new GhostContentAPI({ + url: `${process.env.GHOST_API_URL}`, + key: `${process.env.GHOST_CONTENT_KEY}`, + version: 'v5.0', + }) +} diff --git a/src/components/admin/InfoRequestGrid.tsx b/src/components/admin/InfoRequestGrid.tsx index 025c11a35..955554c2f 100644 --- a/src/components/admin/InfoRequestGrid.tsx +++ b/src/components/admin/InfoRequestGrid.tsx @@ -2,7 +2,7 @@ import React from 'react' import { DataGrid, GridColumns } from '@mui/x-data-grid' import { DialogStore } from 'stores/DialogStore' -import { dateFormatter } from 'common/util/date' +import { formatDateString } from 'common/util/date' import { useInfoRequestList } from 'common/hooks/infoRequest' const columns: GridColumns = [ @@ -27,7 +27,7 @@ const columns: GridColumns = [ { field: 'createdAt', headerName: 'Date', - valueFormatter: (d) => typeof d.value === 'string' && dateFormatter(d.value), + valueFormatter: (d) => typeof d.value === 'string' && formatDateString(d.value), minWidth: 300, }, ] diff --git a/src/components/admin/SupportersGrid.tsx b/src/components/admin/SupportersGrid.tsx index b6798c4f5..5c36bb190 100644 --- a/src/components/admin/SupportersGrid.tsx +++ b/src/components/admin/SupportersGrid.tsx @@ -3,7 +3,7 @@ import { Check, Clear } from '@mui/icons-material' import { DataGrid, GridColDef, GridColumns, GridRenderCellParams } from '@mui/x-data-grid' import { DialogStore } from 'stores/DialogStore' -import { dateFormatter } from 'common/util/date' +import { formatDateString } from 'common/util/date' import { useSupportRequestList } from 'common/hooks/supportRequest' const renderCell = (params: GridRenderCellParams) => @@ -37,7 +37,7 @@ const columns: GridColumns = [ { field: 'createdAt', headerName: 'Date', - valueFormatter: (d) => typeof d.value === 'string' && dateFormatter(d.value), + valueFormatter: (d) => typeof d.value === 'string' && formatDateString(d.value), width: 200, }, { ...commonProps, field: 'associationMember', headerName: 'Association member' }, diff --git a/src/components/auth/profile/MyCampaignsTable.tsx b/src/components/auth/profile/MyCampaignsTable.tsx index a1b5017ff..e7c92af4b 100644 --- a/src/components/auth/profile/MyCampaignsTable.tsx +++ b/src/components/auth/profile/MyCampaignsTable.tsx @@ -1,4 +1,3 @@ -import { bg, enUS } from 'date-fns/locale' import { useTranslation } from 'next-i18next' import { useState, useMemo } from 'react' import { DataGrid, GridColDef, GridColumns, GridRenderCellParams } from '@mui/x-data-grid' @@ -32,7 +31,6 @@ const classes = { export default function MyCampaingsTable() { const { t, i18n } = useTranslation() - const locale = i18n.language == 'bg' ? bg : enUS const [viewId, setViewId] = useState() const { data: campaigns = [] } = useGetUserCampaigns() const selectedCampaign = useMemo( @@ -179,7 +177,9 @@ export default function MyCampaingsTable() { headerAlign: 'left', renderCell: (cellValues: GridRenderCellParams) => ( - + ), }, @@ -191,7 +191,7 @@ export default function MyCampaingsTable() { headerAlign: 'left', renderCell: (cellValues: GridRenderCellParams) => ( - + ), }, @@ -203,7 +203,9 @@ export default function MyCampaingsTable() { headerAlign: 'left', renderCell: (cellValues: GridRenderCellParams) => ( - + ), }, @@ -215,7 +217,9 @@ export default function MyCampaingsTable() { headerAlign: 'left', renderCell: (cellValues: GridRenderCellParams) => ( - + ), }, diff --git a/src/components/auth/profile/MyDonatedToCampaignsTable.tsx b/src/components/auth/profile/MyDonatedToCampaignsTable.tsx index 5319dccf2..c3b6a9e5d 100644 --- a/src/components/auth/profile/MyDonatedToCampaignsTable.tsx +++ b/src/components/auth/profile/MyDonatedToCampaignsTable.tsx @@ -1,4 +1,3 @@ -import { bg, enUS } from 'date-fns/locale' import { useTranslation } from 'next-i18next' import { DataGrid, GridColDef, GridColumns, GridRenderCellParams } from '@mui/x-data-grid' import { Tooltip, Button, Box } from '@mui/material' @@ -16,7 +15,6 @@ import { export default function MyDonatedToCampaignTable() { const { t, i18n } = useTranslation() - const locale = i18n.language == 'bg' ? bg : enUS const { data = [] } = useUserDonationsCampaigns() const commonProps: Partial = { align: 'left', @@ -122,7 +120,9 @@ export default function MyDonatedToCampaignTable() { headerAlign: 'left', renderCell: (cellValues: GridRenderCellParams) => ( - + ), }, @@ -134,7 +134,7 @@ export default function MyDonatedToCampaignTable() { headerAlign: 'left', renderCell: (cellValues: GridRenderCellParams) => ( - + ), }, @@ -146,7 +146,9 @@ export default function MyDonatedToCampaignTable() { headerAlign: 'left', renderCell: (cellValues: GridRenderCellParams) => ( - + ), }, @@ -158,7 +160,9 @@ export default function MyDonatedToCampaignTable() { headerAlign: 'left', renderCell: (cellValues: GridRenderCellParams) => ( - + ), }, diff --git a/src/components/auth/profile/PersonalInfoTab.tsx b/src/components/auth/profile/PersonalInfoTab.tsx index 260d0eecf..dccf7420b 100644 --- a/src/components/auth/profile/PersonalInfoTab.tsx +++ b/src/components/auth/profile/PersonalInfoTab.tsx @@ -188,7 +188,7 @@ export default function PersonalInfoTab() {

{t('profile:personalInfo.birthday')}

{person?.birthday - ? formatDateString(person?.birthday, i18n?.language) + ? formatDateString(person?.birthday, i18n.language) : t('profile:personalInfo.noBirthday')} diff --git a/src/components/blog/BlogIndexPage.tsx b/src/components/blog/BlogIndexPage.tsx new file mode 100644 index 000000000..cd00fa462 --- /dev/null +++ b/src/components/blog/BlogIndexPage.tsx @@ -0,0 +1,65 @@ +import React from 'react' +import NextLink from 'next/link' +import { useTranslation } from 'next-i18next' +import { PostsOrPages } from '@tryghost/content-api' +import { Container, Stack, Typography, Unstable_Grid2 as Grid2 } from '@mui/material' + +import { routes } from 'common/routes' +import Link from 'components/common/Link' +import Layout from 'components/layout/Layout' + +import ReadingTime from './ReadingTime' +import DateCreated from './DateCreated' +import FeaturedImage from './FeaturedImage' + +type Props = { + posts: PostsOrPages +} +export default function BlogIndexPage({ posts }: Props) { + const { t } = useTranslation() + + return ( + + + + {posts.map((post) => ( + + + + + + + + + + + + {post.title} + + + + + + + + + + + ))} + + + + ) +} diff --git a/src/components/blog/BlogPage.tsx b/src/components/blog/BlogPage.tsx new file mode 100644 index 000000000..6d3c068a5 --- /dev/null +++ b/src/components/blog/BlogPage.tsx @@ -0,0 +1,58 @@ +import React from 'react' +import { PostOrPage } from '@tryghost/content-api' +import { Container, Typography, Unstable_Grid2 as Grid2 } from '@mui/material' + +import { baseUrl, routes } from 'common/routes' +import Layout from 'components/layout/Layout' +import BackButton from 'components/navigation/BackButton' + +import ReadingTime from './ReadingTime' +import DateCreated from './DateCreated' +import FeaturedImage from './FeaturedImage' +import RenderContent from './RenderContent' + +type Props = { + page: PostOrPage +} +export default function BlogPage({ page }: Props) { + return ( + + + + + + + + + {page.title} + + + + + + + + + + + + + + + + + + + ) +} diff --git a/src/components/blog/BlogPostPage.tsx b/src/components/blog/BlogPostPage.tsx new file mode 100644 index 000000000..21180708e --- /dev/null +++ b/src/components/blog/BlogPostPage.tsx @@ -0,0 +1,58 @@ +import React from 'react' +import { PostOrPage } from '@tryghost/content-api' +import { Container, Typography, Unstable_Grid2 as Grid2 } from '@mui/material' + +import { baseUrl, routes } from 'common/routes' +import Layout from 'components/layout/Layout' +import BackButton from 'components/navigation/BackButton' + +import ReadingTime from './ReadingTime' +import DateCreated from './DateCreated' +import FeaturedImage from './FeaturedImage' +import RenderContent from './RenderContent' + +type Props = { + post: PostOrPage +} +export default function BlogPostPage({ post }: Props) { + return ( + + + + + + + + + {post.title} + + + + + + + + + + + + + + + + + + + ) +} diff --git a/src/components/blog/DateCreated.tsx b/src/components/blog/DateCreated.tsx new file mode 100644 index 000000000..4f5f1f8cb --- /dev/null +++ b/src/components/blog/DateCreated.tsx @@ -0,0 +1,19 @@ +import { Typography } from '@mui/material' +import { grey } from '@mui/material/colors' +import { useTranslation } from 'react-i18next' + +import { formatDateString } from 'common/util/date' + +type Props = { + createdAt?: string | Date + showLabel?: boolean +} +export default function DateCreated({ createdAt, showLabel = false }: Props) { + const { t, i18n } = useTranslation() + if (!createdAt) return null + return ( + + {showLabel && t('blog:created-on')} {formatDateString(createdAt, i18n.language)} + + ) +} diff --git a/src/components/blog/FeaturedImage.tsx b/src/components/blog/FeaturedImage.tsx new file mode 100644 index 000000000..da9110f2e --- /dev/null +++ b/src/components/blog/FeaturedImage.tsx @@ -0,0 +1,57 @@ +import { Box } from '@mui/material' +import Image, { ImageProps } from 'next/image' +import { PropsWithChildren } from 'react' + +const defaultImage = { + src: '/img/family.jpg', + objectPosition: '-820px center', +} + +type Props = { + height: string + src?: string | null + objectFit?: 'fill' | 'contain' | 'cover' | 'none' | 'scale-down' + objectPosition?: string + showPlaceholder?: boolean +} & Omit +export default function FeaturedImage({ + src, + height, + objectFit = 'contain', + objectPosition = 'center center', + showPlaceholder = false, + ...props +}: Props) { + if (!src) { + if (!showPlaceholder) return null + return ( + + + + ) + } + return ( + + + + ) +} + +const ImageWrapper = ({ height, children }: PropsWithChildren<{ height: string }>) => ( + + {children} + +) diff --git a/src/components/blog/ReadingTime.tsx b/src/components/blog/ReadingTime.tsx new file mode 100644 index 000000000..e273a9751 --- /dev/null +++ b/src/components/blog/ReadingTime.tsx @@ -0,0 +1,18 @@ +import { Typography } from '@mui/material' +import { grey } from '@mui/material/colors' +import { useTranslation } from 'react-i18next' + +type Props = { + readingTime?: number + showLabel?: boolean +} +export default function ReadingTime({ readingTime, showLabel = false }: Props) { + const { t } = useTranslation() + if (!readingTime) return null + return ( + + {showLabel && t('blog:reading-time.label')}{' '} + {t('blog:reading-time.count', { count: readingTime })} + + ) +} diff --git a/src/components/blog/RenderContent.tsx b/src/components/blog/RenderContent.tsx new file mode 100644 index 000000000..9e00969f6 --- /dev/null +++ b/src/components/blog/RenderContent.tsx @@ -0,0 +1,32 @@ +import { Box, Typography } from '@mui/material' + +type Props = { + html?: string | null +} +export default function RenderContent({ html }: Props) { + if (!html) return null + return ( + + theme.typography.pxToRem(18) }} + dangerouslySetInnerHTML={{ __html: html }} + /> + + ) +} diff --git a/src/components/campaigns/grid/CampaignGrid.tsx b/src/components/campaigns/grid/CampaignGrid.tsx index 815bc9c73..b84bb066f 100644 --- a/src/components/campaigns/grid/CampaignGrid.tsx +++ b/src/components/campaigns/grid/CampaignGrid.tsx @@ -1,4 +1,3 @@ -import { bg, enUS } from 'date-fns/locale' import { UseQueryResult } from '@tanstack/react-query' import { useTranslation } from 'next-i18next' import AddIcon from '@mui/icons-material/Add' @@ -61,7 +60,6 @@ export const DisplayCurrentAmount = ({ params }: CampaignCellProps) => { export default function CampaignGrid() { const { t, i18n } = useTranslation() - const locale = i18n.language == 'bg' ? bg : enUS const { data = [], refetch }: UseQueryResult = useCampaignAdminList() const [viewId, setViewId] = useState() const [deleteId, setDeleteId] = useState() @@ -207,7 +205,9 @@ export default function CampaignGrid() { headerAlign: 'left', renderCell: (cellValues: GridRenderCellParams) => ( - + ), }, @@ -219,7 +219,7 @@ export default function CampaignGrid() { headerAlign: 'left', renderCell: (cellValues: GridRenderCellParams) => ( - + ), }, @@ -231,7 +231,9 @@ export default function CampaignGrid() { headerAlign: 'left', renderCell: (cellValues: GridRenderCellParams) => ( - + ), }, @@ -243,7 +245,9 @@ export default function CampaignGrid() { headerAlign: 'left', renderCell: (cellValues: GridRenderCellParams) => ( - + ), }, diff --git a/src/components/layout/Footer/helpers/FooterData.tsx b/src/components/layout/Footer/helpers/FooterData.tsx index 76fd74115..298d75ae2 100644 --- a/src/components/layout/Footer/helpers/FooterData.tsx +++ b/src/components/layout/Footer/helpers/FooterData.tsx @@ -32,7 +32,7 @@ export const footerLinks: FooterSection[] = [ { title: 'components.footer.resources', links: [ - { external: true, label: 'nav.blog', href: staticUrls.blog }, + { external: true, label: 'nav.blog', href: routes.blog.index }, { label: 'components.footer.contact', href: routes.contact }, { label: 'components.footer.faq', href: routes.faq }, ], diff --git a/src/components/layout/Layout.tsx b/src/components/layout/Layout.tsx index e0a66ccc3..f9438151b 100644 --- a/src/components/layout/Layout.tsx +++ b/src/components/layout/Layout.tsx @@ -1,4 +1,5 @@ import Head from 'next/head' +import Script from 'next/script' import { useMemo, useState } from 'react' import { useTranslation } from 'next-i18next' import { Box, BoxProps, Container, ContainerProps, Typography } from '@mui/material' @@ -10,7 +11,6 @@ import DetailsModal from 'components/modal/DetailsModal' import AppNavBar from './AppNavBar' import MobileNav from './nav/MobileNav' -import Script from 'next/script' const createPageTitle = (suffix: string, title?: string) => { if (title) { @@ -29,6 +29,7 @@ type LayoutProps = React.PropsWithChildren< metaTitle?: string metaDescription?: string profilePage?: boolean + canonicalUrl?: string } > @@ -39,13 +40,14 @@ export default function Layout({ maxWidth = 'lg', disableOffset = false, hideFooter = false, + canonicalUrl, boxProps, metaTitle, metaDescription, profilePage = false, ...containerProps }: LayoutProps) { - const { t } = useTranslation() + const { t, i18n } = useTranslation() const [mobileOpen, setMobileOpen] = useState(false) const navMenuToggle = () => setMobileOpen(!mobileOpen) const pageTitle = useMemo( @@ -66,13 +68,27 @@ export default function Layout({ {pageTitle} + {canonicalUrl && ( + <> + + + + )} - + {/* TODO: think of how to make campaign level localization */} - - - + + {!ogImage && ( + + )} + {!ogImage && ( + + )}