From 82de40572b06125f0566efbd53bd650ef468616e Mon Sep 17 00:00:00 2001 From: Adam Matthiesen Date: Wed, 17 Jan 2024 01:42:28 -0800 Subject: [PATCH] Working.... local only not ready for prod --- .gitignore | 24 ++ README.md | 22 +- astro.config.ts | 8 + index.ts | 54 ++- package.json | 48 ++- src/{ => api}/functions.ts | 2 +- index.d.ts => src/api/ghosttypes.ts | 0 src/api/index.ts | 5 + src/{ => api}/interface.js | 0 src/{ => api}/tryghost-content-api.js | 0 src/components/AuthorCard.astro | 80 ++++ src/components/AuthorList.astro | 75 ++++ src/components/BaseHead.astro | 86 ++++ src/components/FeatureImage.astro | 32 ++ src/components/Footer.astro | 112 +++++ src/components/Header.astro | 437 ++++++++++++++++++++ src/components/HeroContent.astro | 131 ++++++ src/components/MainLayout.astro | 23 ++ src/components/Page.astro | 569 +++++++++++++++++++++++++ src/components/Paginator.astro | 26 ++ src/components/Post.astro | 570 ++++++++++++++++++++++++++ src/components/PostFooter.astro | 125 ++++++ src/components/PostHero.astro | 110 +++++ src/components/PostPreview.astro | 301 ++++++++++++++ src/components/PostPreviewList.astro | 43 ++ src/components/TagCard.astro | 51 +++ src/env.d.ts | 1 + src/layouts/default.astro | 60 +++ src/routes/[slug].astro | 46 +++ src/routes/archives/[...page].astro | 51 +++ src/routes/author/[slug].astro | 121 ++++++ src/routes/authors.astro | 44 ++ src/routes/index.astro | 33 ++ src/routes/tag/[slug].astro | 102 +++++ src/routes/tags.astro | 48 +++ src/styles/app.scss | 390 ++++++++++++++++++ src/styles/reset.scss | 462 +++++++++++++++++++++ src/styles/variables.scss | 8 + src/utils/index.ts | 32 ++ 39 files changed, 4304 insertions(+), 28 deletions(-) create mode 100644 .gitignore create mode 100644 astro.config.ts rename src/{ => api}/functions.ts (99%) rename index.d.ts => src/api/ghosttypes.ts (100%) create mode 100644 src/api/index.ts rename src/{ => api}/interface.js (100%) rename src/{ => api}/tryghost-content-api.js (100%) create mode 100644 src/components/AuthorCard.astro create mode 100644 src/components/AuthorList.astro create mode 100644 src/components/BaseHead.astro create mode 100644 src/components/FeatureImage.astro create mode 100644 src/components/Footer.astro create mode 100644 src/components/Header.astro create mode 100644 src/components/HeroContent.astro create mode 100644 src/components/MainLayout.astro create mode 100644 src/components/Page.astro create mode 100644 src/components/Paginator.astro create mode 100644 src/components/Post.astro create mode 100644 src/components/PostFooter.astro create mode 100644 src/components/PostHero.astro create mode 100644 src/components/PostPreview.astro create mode 100644 src/components/PostPreviewList.astro create mode 100644 src/components/TagCard.astro create mode 100644 src/env.d.ts create mode 100644 src/layouts/default.astro create mode 100644 src/routes/[slug].astro create mode 100644 src/routes/archives/[...page].astro create mode 100644 src/routes/author/[slug].astro create mode 100644 src/routes/authors.astro create mode 100644 src/routes/index.astro create mode 100644 src/routes/tag/[slug].astro create mode 100644 src/routes/tags.astro create mode 100644 src/styles/app.scss create mode 100644 src/styles/reset.scss create mode 100644 src/styles/variables.scss create mode 100644 src/utils/index.ts diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000..92068a14 --- /dev/null +++ b/.gitignore @@ -0,0 +1,24 @@ +# build output +dist +.vercel +# generated types +.astro/ +# dependencies +node_modules/ +.snowpack/ + +# logs +npm-debug.log* +yarn-debug.log* +yarn-error.log* + +# environment variables +.env +.env.production + +# macOS-specific files +.DS_Store + +# editor +.idea +package-lock.json \ No newline at end of file diff --git a/README.md b/README.md index 4253ed39..ad74383d 100644 --- a/README.md +++ b/README.md @@ -1,12 +1,18 @@ # Welcome to Astro-GhostCMS +Astro minimum Version: **Astro v4.0** + This addon uses the `@tryghost/content-api` and creates astro friendly functions to interface between ghost and astro. *This package contains a independent copy of the tryghost content-api.js that is used to establish the connection so this package dose not depend on `@tryghost/content-api` package.* -## Astro Integration Mode *(Planned for V2)* +## Astro Integration Mode -This is coming soon. And will allow the user to utilize the prebuilt astro-ghostcms-basetheme to be integrated through this main project. This feature is not yet setup or integrated. If you want a easy quick and simple deploy please copy this Template Repo, [astro-ghostcms-basetheme](https://github.com/MatthiesenXYZ/astro-ghostcms-basetheme) This will get you setup and ready to deploy in minutes using this addon! +In this mode, the addon will not be just an API, but will be a full Route takeover, there is plans to add more themes in time, but for now there is only the base Casper theme based on Ghost's main Theme. + +``` +astro add @matthiesenxyz/astro-ghostcms +``` ## Manual Installation @@ -17,19 +23,13 @@ npm i @matthiesenxyz/astro-ghostcms Must create `.env` with the following: ```env -CONTENT_API_KEY= -CONTENT_API_URL= +CONTENT_API_KEY=a33da3965a3a9fb2c6b3f63b48 +CONTENT_API_URL=https://ghostdemo.matthiesen.xyz ``` **When you deploy your install dont forget to set the above ENVIRONMENT VARIABLES!** -Astro minimum Version: **Astro v4.0** - -Dependencies: -- **Axios v1.0** *Will be auto installed* -- **Typescript v5.3.3** *Will be auto installed* - -## Function Usage Examples: +## Manual Function Usage Examples: ### getGhostPosts() - Get list of posts diff --git a/astro.config.ts b/astro.config.ts new file mode 100644 index 00000000..9d96843a --- /dev/null +++ b/astro.config.ts @@ -0,0 +1,8 @@ +import { defineConfig } from "astro/config"; +import sitemap from "@astrojs/sitemap"; +import GhostCMS from './index'; + +// https://astro.build/config +export default defineConfig({ + integrations: [sitemap(), GhostCMS()], +}); diff --git a/index.ts b/index.ts index b19b164e..f78cb5d1 100644 --- a/index.ts +++ b/index.ts @@ -1,5 +1,51 @@ -// FUNCTION EXPORTS -export { getGhostPosts, getGhostRecentPosts, getGhostFeaturedPosts, getGhostPostbySlug, getGhostPostsbyTag, getGhostTags, getGhostTagbySlug, getGhostAuthors, getGhostPages, getGhostPage, getGhostSettings } from './src/functions'; +import type { AstroIntegration } from "astro" -// TYPE EXPORTS -export type { PostOrPage, ArrayOrValue, Author, Authors, BrowseFunction, CodeInjection, Excerpt, Facebook, FieldParam, FilterParam, FormatParam, GhostAPI, GhostContentAPIOptions, GhostData, GhostError, Identification, IncludeParam, LimitParam, Metadata, Nullable, OrderParam, PageParam, Pagination, Params, PostsOrPages, ReadFunction, Settings, SettingsResponse, SocialMedia, Tag, TagVisibility, Tags, Twitter } from './index.d'; \ No newline at end of file +export default function GhostCMS(): AstroIntegration { + return { + name: 'astro-ghostcms', + hooks: { + 'astro:config:setup': async ({ + injectRoute, + logger, + }) => { + + injectRoute({ + pattern: '/', + entrypoint: './src/routes/index.astro' + }) + + injectRoute({ + pattern: '/[slug]', + entrypoint: './src/routes/[slug].astro' + }) + + injectRoute({ + pattern: '/tags', + entrypoint: './src/routes/tags.astro' + }) + + injectRoute({ + pattern: '/authors', + entrypoint: './src/routes/authors.astro' + }) + + injectRoute({ + pattern: '/tag/[slug]', + entrypoint: './src/routes/tag/[slug].astro' + }) + + injectRoute({ + pattern: '/author/[slug]', + entrypoint: './src/routes/author/[slug].astro' + }) + + injectRoute({ + pattern: '/archives/[...page]', + entrypoint: './src/routes/archives/[...page].astro' + }) + + logger.info('Astro GhostCMS Plugin Loaded!') + } + } + } +} \ No newline at end of file diff --git a/package.json b/package.json index 98395732..d9c0ffe8 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "@matthiesenxyz/astro-ghostcms", "description": "Astro GhostCMS integration to allow easier importing of GhostCMS Content", - "version": "1.0.6", + "version": "2.0.0-dev.1", "author": "MatthiesenXYZ (https://matthiesen.xyz)", "type": "module", "license": "MIT", @@ -14,14 +14,21 @@ }, "homepage": "https://github.com/MatthiesenXYZ/astro-ghostcms", "exports": { - ".": "./index.ts" + "./api": "./src/api/index.ts", + "./index.astro": "./src/routes/index.astro", + "./[slug].astro": "./src/routes/[slug].astro", + "./tags.astro": "./src/routes/tags.astro", + "./authors.astro": "./src/routes/authors.astro", + "./tag/[slug].astro": "./src/routes/tag/[slug].astro", + "./author/[slug].astro": "./src/routes/author/[slug].astro", + "./archives/[...page].astro": "./src/routes/archives/[...page].astro", + "./DefaultLayout": "./src/layouts/default.astro" }, "main": "index.ts", - "types": "index.d.ts", + "types": "src/api/ghosttypes.ts", "files": [ "src", - "index.ts", - "index.d.ts" + "index.ts" ], "keywords": [ "astro-component", @@ -29,16 +36,33 @@ "ghost", "ghostcms" ], - "scripts": {}, - "devDependencies": { - "astro": "^4.1.1" - }, - "peerDependencies": { - "astro": "^4.0.0" + "scripts": { + "dev": "astro dev", + "build": "astro build", + "typecheck": "astro check && tsc --noEmit", + "preview": "astro preview", + "format": "prettier --write .", + "lint": "eslint --cache --cache-location ./node_modules/.cache/eslint ." }, "dependencies": { "@astrojs/check": "^0.3.4", "typescript": "^5.3.3", - "axios": "^1.0.0" + "axios": "^1.0.0", + "astro-font": "^0.0.72", + "@astrojs/renderer-svelte": "0.5.2", + "@astrojs/rss": "^4.0.2", + "@astrojs/sitemap": "^3.0.4", + "@snowpack/plugin-dotenv": "^2.2.0", + "@typescript-eslint/eslint-plugin": "^6.5.0", + "@typescript-eslint/parser": "^6.5.0", + "astro": "^4.1.2", + "eslint": "^8.48.0", + "eslint-config-prettier": "^9.0.0", + "eslint-plugin-astro": "^0.29.0", + "prettier": "^3.0.3", + "prettier-plugin-astro": "^0.12.0", + "sass": "^1.66.1", + "tiny-invariant": "^1.3.1", + "vite": "^4.4.9" } } diff --git a/src/functions.ts b/src/api/functions.ts similarity index 99% rename from src/functions.ts rename to src/api/functions.ts index f9500485..021b83ca 100644 --- a/src/functions.ts +++ b/src/api/functions.ts @@ -1,5 +1,5 @@ // IMPORT Ghost Types -import type { PostOrPage, PostsOrPages, Authors, Tag, Tags, ArrayOrValue, IncludeParam, LimitParam, Settings, Nullable } from '..'; +import type { PostOrPage, PostsOrPages, Authors, Tag, Tags, ArrayOrValue, IncludeParam, LimitParam, Settings, Nullable } from './ghosttypes'; // IMPORT Ghost API Client import api from './interface'; diff --git a/index.d.ts b/src/api/ghosttypes.ts similarity index 100% rename from index.d.ts rename to src/api/ghosttypes.ts diff --git a/src/api/index.ts b/src/api/index.ts new file mode 100644 index 00000000..1621bb3a --- /dev/null +++ b/src/api/index.ts @@ -0,0 +1,5 @@ +// FUNCTION EXPORTS +export { getGhostPosts, getGhostRecentPosts, getGhostFeaturedPosts, getGhostPostbySlug, getGhostPostsbyTag, getGhostTags, getGhostTagbySlug, getGhostAuthors, getGhostPages, getGhostPage, getGhostSettings } from './functions'; + +// TYPE EXPORTS +export type { PostOrPage, ArrayOrValue, Author, Authors, BrowseFunction, CodeInjection, Excerpt, Facebook, FieldParam, FilterParam, FormatParam, GhostAPI, GhostContentAPIOptions, GhostData, GhostError, Identification, IncludeParam, LimitParam, Metadata, Nullable, OrderParam, PageParam, Pagination, Params, PostsOrPages, ReadFunction, Settings, SettingsResponse, SocialMedia, Tag, TagVisibility, Tags, Twitter } from './ghosttypes'; \ No newline at end of file diff --git a/src/interface.js b/src/api/interface.js similarity index 100% rename from src/interface.js rename to src/api/interface.js diff --git a/src/tryghost-content-api.js b/src/api/tryghost-content-api.js similarity index 100% rename from src/tryghost-content-api.js rename to src/api/tryghost-content-api.js diff --git a/src/components/AuthorCard.astro b/src/components/AuthorCard.astro new file mode 100644 index 00000000..23e4ab86 --- /dev/null +++ b/src/components/AuthorCard.astro @@ -0,0 +1,80 @@ +--- +import { getGhostImgPath } from "../utils"; +import type { Settings, Author } from "../api"; +export type Props = { + author: Author; + wide?: boolean; + addClass?: string; + settings: Settings; + showCover?: boolean; +}; +const { + author, + wide = false, + settings, + showCover = true, +} = Astro.props as Props; +--- + +
+
+ {author.cover_image && showCover && ( + {author.name} + )} +
+ + {author.profile_image && ( + + {author.name} + + )} +
+ + + {author.bio &&
{author.bio}
} + +
+ {author.count && author.count.posts && ( +
+ {author.count.posts > 0 ? `${author.count.posts} posts` : "No posts"} +
+ )} +
+
+
+ + diff --git a/src/components/AuthorList.astro b/src/components/AuthorList.astro new file mode 100644 index 00000000..ef4125e7 --- /dev/null +++ b/src/components/AuthorList.astro @@ -0,0 +1,75 @@ +--- +import { getGhostImgPath } from "../utils"; +import type { Settings, PostOrPage } from "../api"; +export type Props = { + post: PostOrPage; + settings: Settings; +}; +const { post, settings } = Astro.props as Props; +--- + + + + diff --git a/src/components/BaseHead.astro b/src/components/BaseHead.astro new file mode 100644 index 00000000..e484bc02 --- /dev/null +++ b/src/components/BaseHead.astro @@ -0,0 +1,86 @@ +--- +import { AstroFont } from "astro-font"; +import { ViewTransitions } from 'astro:transitions'; +import type { Settings } from "../api"; + +export type Props = { + title: string; + description: string; + permalink?: string; + image?: string; + settings: Settings; +}; + +const { description, permalink, image, settings, title } = Astro.props as Props; +--- + + + + + + +{title} + + +{description && } + + + + + + + + + + + + + +{permalink && } +{description && } +{image && } + + + + +{permalink && } +{description && } +{image && } + + + + + + diff --git a/src/components/FeatureImage.astro b/src/components/FeatureImage.astro new file mode 100644 index 00000000..ddbddaa2 --- /dev/null +++ b/src/components/FeatureImage.astro @@ -0,0 +1,32 @@ +--- +import { getGhostImgPath } from "../utils"; +import type { Settings } from "../api"; +export type Props = { + image: string; + alt?: string; + caption?: string; + settings: Settings; + transitionName?: string; +}; +const { image, alt, caption = "", settings, transitionName } = Astro.props as Props; +--- + +
+ {alt} + {caption &&
{caption}
} +
+ + diff --git a/src/components/Footer.astro b/src/components/Footer.astro new file mode 100644 index 00000000..2f4d2939 --- /dev/null +++ b/src/components/Footer.astro @@ -0,0 +1,112 @@ +--- +import type { Settings } from "../api"; +export type Props = { + settings: Settings; +}; +const { settings } = Astro.props as Props; +--- + +
+
+ + + +
+
+ + diff --git a/src/components/Header.astro b/src/components/Header.astro new file mode 100644 index 00000000..996f8add --- /dev/null +++ b/src/components/Header.astro @@ -0,0 +1,437 @@ +--- +import type { Settings } from "../api"; +export type Props = { + settings: Settings; +}; +const { settings } = Astro.props as Props; +--- + +
+ +
+ + + + diff --git a/src/components/HeroContent.astro b/src/components/HeroContent.astro new file mode 100644 index 00000000..c52a8788 --- /dev/null +++ b/src/components/HeroContent.astro @@ -0,0 +1,131 @@ +--- +import { getGhostImgPath } from "../utils"; +import type { Settings } from "../api"; +export type Props = { + featureImg?: string; + mainTitle?: string; + settings: Settings; + description?: string; + addClass?: string; +}; +const { + featureImg = "https://static.ghost.org/v4.0.0/images/publication-cover.jpg", + mainTitle = "", + settings, + description = "", +} = Astro.props as Props; +--- + +
+ {featureImg && ( + {mainTitle} + )} + + +

+ {settings.logo ? ( + + ) : ( + settings.title + )} +

+
+

{description}

+
+ + diff --git a/src/components/MainLayout.astro b/src/components/MainLayout.astro new file mode 100644 index 00000000..d5ca8ab7 --- /dev/null +++ b/src/components/MainLayout.astro @@ -0,0 +1,23 @@ +--- +import Header from "./Header.astro"; +import Footer from "./Footer.astro"; +import type { Settings } from "../api"; +export type Props = { + settings: Settings; +}; +const { settings } = Astro.props as Props; +--- + +
+
+
+ +
+
+
+ + diff --git a/src/components/Page.astro b/src/components/Page.astro new file mode 100644 index 00000000..857f8d92 --- /dev/null +++ b/src/components/Page.astro @@ -0,0 +1,569 @@ +--- +import FeatureImage from "./FeatureImage.astro"; +import type { Settings, PostOrPage } from "../api"; +export type Props = { + page: PostOrPage; + settings: Settings; + pageClass?: string; +}; +const { page, settings, pageClass } = Astro.props as Props; +--- + +
+
+
+ {page.feature_image && ( + + )} +
+ +
+

{page.title}

+ +
+
+
+ + diff --git a/src/components/Paginator.astro b/src/components/Paginator.astro new file mode 100644 index 00000000..cf0e572f --- /dev/null +++ b/src/components/Paginator.astro @@ -0,0 +1,26 @@ +--- +import type { Page } from 'astro'; +const { page } = Astro.props as {page: Page}; +--- + +
+ {page.url.prev && ( + + ← Prev + + )} + {page.url.next && ( + + Next → + + )} +
+ + diff --git a/src/components/Post.astro b/src/components/Post.astro new file mode 100644 index 00000000..1554ef89 --- /dev/null +++ b/src/components/Post.astro @@ -0,0 +1,570 @@ +--- +import PostHero from "./PostHero.astro"; +import PostFooter from "./PostFooter.astro"; +import invariant from "tiny-invariant"; +import type {PostOrPage, PostsOrPages, Settings } from "../api"; +export type Props = { + post: PostOrPage; + settings: Settings; + postClass?: string; + posts: PostsOrPages; +}; +const { post, settings, postClass, posts } = Astro.props as Props; +invariant(settings, "Settings not found"); +--- + +
+
+ +
+ +
+
+ +
+ + diff --git a/src/components/PostFooter.astro b/src/components/PostFooter.astro new file mode 100644 index 00000000..7203f4be --- /dev/null +++ b/src/components/PostFooter.astro @@ -0,0 +1,125 @@ +--- +import PostPreview from "./PostPreview.astro"; +import type { Settings, PostOrPage, PostsOrPages } from "../api"; +export type Props = { + post: PostOrPage; + settings: Settings; + posts: PostsOrPages; +}; +const { post, settings, posts } = Astro.props as Props; +--- + + + + + + diff --git a/src/components/PostHero.astro b/src/components/PostHero.astro new file mode 100644 index 00000000..49bb2c47 --- /dev/null +++ b/src/components/PostHero.astro @@ -0,0 +1,110 @@ +--- +import FeatureImage from "./FeatureImage.astro"; +import AuthorList from "./AuthorList.astro"; +import { formatDate } from "../utils"; +import type { Settings, PostOrPage } from "../api"; +export type Props = { + post: PostOrPage; + settings: Settings; +}; +const { post, settings } = Astro.props as Props; +--- + +
+ {post.primary_tag && ( + + )} +

{post.title}

+ + {post.custom_excerpt &&

{post.custom_excerpt}

} + + {post.feature_image && ( + + )} +
+ + diff --git a/src/components/PostPreview.astro b/src/components/PostPreview.astro new file mode 100644 index 00000000..527b1c1a --- /dev/null +++ b/src/components/PostPreview.astro @@ -0,0 +1,301 @@ +--- +import { getGhostImgPath, formatDate } from "../utils"; +import AuthorList from "./AuthorList.astro"; +import type { Settings, PostOrPage, Tag } from "../api"; +export type Props = { + post: PostOrPage; + settings: Settings; + index?: number; + isHome?: boolean; +}; +const { post, settings, index, isHome = false } = Astro.props as Props; +--- + + + + diff --git a/src/components/PostPreviewList.astro b/src/components/PostPreviewList.astro new file mode 100644 index 00000000..126908a7 --- /dev/null +++ b/src/components/PostPreviewList.astro @@ -0,0 +1,43 @@ +--- +import PostPreview from "./PostPreview.astro"; +import type { Settings, PostOrPage } from "../api"; +export type Props = { + posts: PostOrPage[]; + settings: Settings; + isHome?: boolean; +}; +const { posts, settings, isHome = false } = Astro.props as Props; +--- + +
+ {posts.map((post: PostOrPage, index: number) => ( + + ))} +
+ + diff --git a/src/components/TagCard.astro b/src/components/TagCard.astro new file mode 100644 index 00000000..f200ad6f --- /dev/null +++ b/src/components/TagCard.astro @@ -0,0 +1,51 @@ +--- +import { getGhostImgPath } from "../utils"; +import type { Settings, Tag } from "../api"; +export type Props = { + tag: Tag; + addClass?: string; + settings: Settings; +}; +const { tag, addClass = "", settings } = Astro.props; +--- + + + { + tag.feature_image && ( +
+ {tag.name} +
+ ) + } +
+

{tag.name}

+ {tag.count && {tag.count.posts}+ Posts} +
+
+ + diff --git a/src/env.d.ts b/src/env.d.ts new file mode 100644 index 00000000..8c34fb45 --- /dev/null +++ b/src/env.d.ts @@ -0,0 +1 @@ +/// \ No newline at end of file diff --git a/src/layouts/default.astro b/src/layouts/default.astro new file mode 100644 index 00000000..93bd703d --- /dev/null +++ b/src/layouts/default.astro @@ -0,0 +1,60 @@ +--- +import type { Settings } from "../api"; +import BaseHead from "../components/BaseHead.astro"; +import MainLayout from "../components/MainLayout.astro"; +export type Props = { + content?: { + title: string|undefined; + description: string|undefined; + }; + bodyClass?: string; + settings: Settings; +}; + +const { content, settings, bodyClass = "" } = Astro.props as Props; +const ghostAccentColor = settings.accent_color; +console.log(settings.accent_color) +--- + + + + + + + + + + + + diff --git a/src/routes/[slug].astro b/src/routes/[slug].astro new file mode 100644 index 00000000..51fff808 --- /dev/null +++ b/src/routes/[slug].astro @@ -0,0 +1,46 @@ +--- +import type { InferGetStaticPropsType } from 'astro'; +import DefaultPageLayout from "../layouts/default.astro"; +import Page from "../components/Page.astro"; +import Post from "../components/Post.astro"; +import { getGhostSettings, getGhostPages, getGhostPosts } from "../api"; +import invariant from 'tiny-invariant'; + +export async function getStaticPaths() { + const [posts, pages, settings] = await Promise.all([getGhostPosts(), await getGhostPages(), await getGhostSettings()]); + const allPosts = [...posts, ...pages]; + return allPosts.map((post) => ({ + params: { slug: post.slug }, + props: { post, posts, settings }, + })); +} + +export type Props = InferGetStaticPropsType; + +const {post, posts, settings} = Astro.props as Props; +invariant(settings, "Settings are required"); +const postClass = post.tags?.map((tag) => "tag-" + tag.slug).join(" "); +const bodyClass = `post-template ${postClass}`; +--- + + + { + post.primary_author ? ( + + + ) : ( + + ) + } + + + diff --git a/src/routes/archives/[...page].astro b/src/routes/archives/[...page].astro new file mode 100644 index 00000000..270cc410 --- /dev/null +++ b/src/routes/archives/[...page].astro @@ -0,0 +1,51 @@ +--- +import type { GetStaticPathsOptions, Page } from 'astro'; +import invariant from "tiny-invariant"; +import DefaultPageLayout from "../../layouts/default.astro"; +import PostPreviewList from "../../components/PostPreviewList.astro"; +import HeroContent from "../../components/HeroContent.astro"; +import Paginator from "../../components/Paginator.astro"; +import { getGhostSettings, getGhostPosts } from "../../api"; +import type { PostOrPage } from '../../api'; + +export async function getStaticPaths({ paginate }:GetStaticPathsOptions) { + const posts = await getGhostPosts(); + return paginate(posts, { + pageSize: 5, + }); +} + +export type Props = { + page: Page +}; + +const settings = await getGhostSettings(); +invariant(settings, "Settings are required"); + +const title = settings.title; +const description = settings.description; +const { page } = Astro.props as Props; +--- + + + +

Archives

+
+ +
+
+ + +
+
+
diff --git a/src/routes/author/[slug].astro b/src/routes/author/[slug].astro new file mode 100644 index 00000000..34eb85e5 --- /dev/null +++ b/src/routes/author/[slug].astro @@ -0,0 +1,121 @@ +--- +import type { InferGetStaticParamsType, InferGetStaticPropsType } from 'astro'; +import DefaultPageLayout from "../../layouts/default.astro"; +import PostPreviewList from "../../components/PostPreviewList.astro"; +import { getGhostPosts, getGhostAuthors, getGhostSettings } from "../../api"; +import invariant from "tiny-invariant"; + +export async function getStaticPaths() { + const posts = await getGhostPosts(); + const authors = await getGhostAuthors(); + const settings = await getGhostSettings(); + + return authors.map((author) => { + const filteredPosts = posts.filter((post) => + post.authors?.map((author) => author.slug).includes(author.slug) + ); + return { + params: { slug: author.slug }, + props: { + posts: filteredPosts, + settings, + author, + }, + }; + }); +} + +export type Params = InferGetStaticParamsType; +export type Props = InferGetStaticPropsType; + +const { posts, settings, author } = Astro.props; +invariant(settings, "Settings are required"); +const title = `Posts by author: ${author.name}`; +const description = `All of the articles we've posted and linked so far under the author: ${author.name}`; +--- + + +
+
+
+
+ {author.profile_image ? ( + {author.name} + ) : ( + + + + + + + + )} +

{author.name}

+

+ {author.bio + ? author.bio + : author.count?.posts || 0 > 0 + ? `${author.count?.posts} Posts` + : ""} +

+ +
+ {author.location && ( +
📍 {author.location}
+ )} + + {author.website && ( + + + {author.website} + + + )} + {author.twitter && ( + + + {author.twitter} + + + )} + {author.facebook && ( + + + {author.facebook} + + + )} +
+
+
+ + +
+
+
diff --git a/src/routes/authors.astro b/src/routes/authors.astro new file mode 100644 index 00000000..5b7dd7da --- /dev/null +++ b/src/routes/authors.astro @@ -0,0 +1,44 @@ +--- +import DefaultPageLayout from "../layouts/default.astro"; +import AuthorCard from "../components/AuthorCard.astro"; +import { getGhostAuthors, getGhostSettings } from "../api"; +import invariant from "tiny-invariant"; + +let title = "All Authors"; +let description = "All the authors"; +const authors = await getGhostAuthors(); +const settings = await getGhostSettings(); +invariant(settings, 'Settings not found'); +--- + + +
+
+

+ {settings.title} +

+
+ Collection of Tags +
+
+ {authors.map((author) => ( +
+ +
+ ))} +
+
+
+
+ + diff --git a/src/routes/index.astro b/src/routes/index.astro new file mode 100644 index 00000000..a2c43d39 --- /dev/null +++ b/src/routes/index.astro @@ -0,0 +1,33 @@ +--- +import DefaultPageLayout from "../layouts/default.astro"; +import PostPreviewList from "../components/PostPreviewList.astro"; +import HeroContent from "../components/HeroContent.astro"; +import { getGhostPosts, getGhostSettings } from "../api"; +import invariant from "tiny-invariant"; +const posts = await getGhostPosts(); +const settings = await getGhostSettings(); +invariant(settings, "Settings not found"); + +const title = settings.title; +const description = settings.description; +--- + + + + +
+
+ +
+
+
diff --git a/src/routes/tag/[slug].astro b/src/routes/tag/[slug].astro new file mode 100644 index 00000000..e32c2a9d --- /dev/null +++ b/src/routes/tag/[slug].astro @@ -0,0 +1,102 @@ +--- +import type { InferGetStaticParamsType, InferGetStaticPropsType } from 'astro'; +import DefaultPageLayout from "../../layouts/default.astro"; +import PostPreview from "../../components/PostPreview.astro"; +import { getGhostPosts, getGhostTags, getGhostSettings } from "../../api"; +import { getGhostImgPath } from "../../utils"; +import invariant from "tiny-invariant"; + +export async function getStaticPaths() { + const posts = await getGhostPosts(); + const tags = await getGhostTags(); + const settings = await getGhostSettings(); + + return tags.map((tag) => { + const filteredPosts = posts.filter((post) => + post.tags?.map((tag) => tag.slug).includes(tag.slug) + ); + return { + params: { slug: tag.slug }, + props: { + posts: filteredPosts, + settings, + tag, + }, + }; + }); +} + +export type Params = InferGetStaticParamsType; +export type Props = InferGetStaticPropsType; + +const { posts, settings, tag } = Astro.props; +invariant(settings, 'Settings not found'); +const title = `Posts by Tag: ${tag.name}`; +const description = `all of the articles we've posted and linked so far under the tag: ${tag.name}`; +--- + + +
+
+
+
+ {tag.feature_image && ( +
+ {tag.name} +
+ )} +
+
+
+
Tagged
+

{tag.name}

+
+
+

+ {tag.description + ? tag.description + : `A collection of ${tag.count?.posts || 0 } Post${ + tag.count?.posts ?? 0 > 1 ? "s" : "" + }`} +

+
+
+
+
+ {posts.map((post, index) => ( + + ))} +
+
+
+
diff --git a/src/routes/tags.astro b/src/routes/tags.astro new file mode 100644 index 00000000..e6a8547d --- /dev/null +++ b/src/routes/tags.astro @@ -0,0 +1,48 @@ +--- +import DefaultPageLayout from "../layouts/default.astro"; +import TagCard from "../components/TagCard.astro"; +import { getGhostSettings, getGhostTags } from "../api"; +import invariant from 'tiny-invariant'; + + +let title = "All Tags"; +let description = "All the tags used so far..."; + +const tags = await getGhostTags(); +const settings = await getGhostSettings(); +invariant(settings, "Settings not found"); +--- + + +
+
+

+ {settings.title} +

+
+ Collection of Tags +
+
+ { + tags.map((tag) => ( +
+ +
+ )) + } +
+
+
+
+ + diff --git a/src/styles/app.scss b/src/styles/app.scss new file mode 100644 index 00000000..dcc688ee --- /dev/null +++ b/src/styles/app.scss @@ -0,0 +1,390 @@ +/* Reset +/* ---------------------------------------------------------- */ + +@import "./reset"; +@import "./variables"; + +/* 1. Global - Set up the things +/* ---------------------------------------------------------- */ +/* Import CSS reset and base styles */ +:root { + /* Colours */ + --color-green: #a4d037; + --color-yellow: #fecd35; + --color-red: #f05230; + --color-darkgrey: #15171a; + --color-midgrey: #738a94; + --color-lightgrey: #c5d2d9; + --color-wash: #e5eff5; + --color-darkmode: #151719; + + /* + An accent color is also set by Ghost itself in + Ghost Admin > Settings > Brand + + --ghost-accent-color: {value}; + + You can use this variale throughout your styles + */ + + /* Fonts */ + --font-sans-serif: -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", + "Oxygen", "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue", + sans-serif; + --font-serif: Georgia, Times, serif; + --font-mono: Menlo, Courier, monospace; +} + +/* 2. Layout - Page building blocks +/* ---------------------------------------------------------- */ + +.viewport { + display: flex; + flex-direction: column; + min-height: 100vh; +} + +.site-content { + flex-grow: 1; +} + +/* Full width page blocks */ +.outer { + position: relative; + padding: 0 4vmin; +} + +/* Centered content container blocks */ +.inner { + margin: 0 auto; + max-width: 1200px; + width: 100%; +} + +/* 9. Error Template +/* ---------------------------------------------------------- */ + +.error-content { + padding: 14vw 4vw 6vw; +} + +.error-message { + padding-bottom: 10vw; + text-align: center; +} + +.error-code { + margin: 0; + color: var(--ghost-accent-color); + font-size: 12vw; + line-height: 1em; + letter-spacing: -5px; +} + +.error-description { + margin: 0; + color: var(--color-midgrey); + font-size: 3.2rem; + line-height: 1.3em; + font-weight: 400; +} + +.error-link { + display: inline-block; + margin-top: 5px; +} + +@media (min-width: 940px) { + .error-content .post-card { + margin-bottom: 0; + padding-bottom: 0; + border-bottom: none; + } +} + +@media (max-width: 800px) { + .error-content { + padding-top: 24vw; + } + .error-code { + font-size: 11.2rem; + } + .error-message { + padding-bottom: 16vw; + } + .error-description { + margin: 5px 0 0 0; + font-size: 1.8rem; + } +} + +@media (max-width: 500px) { + .error-content { + padding-top: 28vw; + } + .error-message { + padding-bottom: 14vw; + } +} + +.author-template .posts { + position: relative; + height: 100%; + display: grid; + grid-template-columns: 200px 1fr 1fr; + grid-gap: 4vmin; +} + +.author-template .posts .post-feed { + grid-column: 2 / 4; + grid-template-columns: 1fr 1fr; +} +@media (max-width: 900px) { + .author-template .posts .post-feed { + grid-template-columns: 1fr; + } +} + +@media (max-width: 650px) { + .author-template .posts { + grid-template-columns: 1fr; + grid-gap: 0; + } + .author-template .posts .post-feed { + grid-column: 1 / auto; + } + .author-profile { + padding-right: 0; + } + .author-profile-content { + display: flex; + flex-direction: column; + align-items: center; + text-align: center; + } +} + +/* 11. Site Footer +/* ---------------------------------------------------------- */ + +/* 12. Dark Mode +/* ---------------------------------------------------------- */ + +/* If you prefer a dark color scheme, you can enable dark mode +by adding the following code to the Head section of "Code Injection" +settings inside: Ghost Admin > Settings > Advanced + + + +Or you can just edit default.hbs and add the .dark-mode class directly +to the html tag on the very first line of the file. + + */ + +html.dark-mode body { + color: rgba(255, 255, 255, 0.75); + background: var(--color-darkmode); +} + +html.dark-mode img { + opacity: 0.9; +} + +html.dark-mode .post-card, +html.dark-mode .post-card:hover { + border-bottom-color: color-mod(var(--color-darkmode) l(+8%)); +} + +html.dark-mode .post-card-byline-content a { + color: rgba(255, 255, 255, 0.75); +} + +html.dark-mode .post-card-byline-content a:hover { + color: #fff; +} + +html.dark-mode .post-card-image { + background: var(--color-darkmode); +} + +html.dark-mode .post-card-title { + color: rgba(255, 255, 255, 0.85); +} + +html.dark-mode .post-card-excerpt { + color: color-mod(var(--color-midgrey) l(+10%)); +} + +html.dark-mode .post-full-content { + background: var(--color-darkmode); +} + +html.dark-mode .article-title { + color: rgba(255, 255, 255, 0.9); +} + +html.dark-mode .article-excerpt { + color: color-mod(var(--color-midgrey) l(+10%)); +} + +html.dark-mode .post-full-image { + background-color: color-mod(var(--color-darkmode) l(+8%)); +} + +html.dark-mode .article-byline { + border-top-color: color-mod(var(--color-darkmode) l(+15%)); +} + +html.dark-mode .article-byline-meta h4 a { + color: rgba(255, 255, 255, 0.75); +} + +html.dark-mode .article-byline-meta h4 a:hover { + color: #fff; +} + +html.dark-mode .no-image .author-social-link a { + color: rgba(255, 255, 255, 0.75); +} + +html.dark-mode .gh-content h1, +html.dark-mode .gh-content h2, +html.dark-mode .gh-content h3, +html.dark-mode .gh-content h4, +html.dark-mode .gh-content h5, +html.dark-mode .gh-content h6 { + color: rgba(255, 255, 255, 0.9); +} + +html.dark-mode .gh-content pre { + background: color-mod(var(--color-darkgrey) l(-8%)); +} + +html.dark-mode .gh-content :not(pre) > code { + background: color-mod(var(--color-darkgrey) l(+6%)); + border-color: color-mod(var(--color-darkmode) l(+8%)); + color: var(--color-wash); +} + +html.dark-mode .post-full-content a { + color: #fff; + box-shadow: inset 0 -1px 0 #fff; +} + +html.dark-mode .post-full-content strong { + color: #fff; +} + +html.dark-mode .post-full-content em { + color: #fff; +} + +html.dark-mode .post-full-content code { + color: #fff; + background: #000; +} + +html.dark-mode hr { + border-top-color: color-mod(var(--color-darkmode) l(+8%)); +} + +html.dark-mode .post-full-content hr:after { + background: color-mod(var(--color-darkmode) l(+8%)); + box-shadow: var(--color-darkmode) 0 0 0 5px; +} + +html.dark-mode .gh-content figcaption { + color: rgba(255, 255, 255, 0.6); +} + +html.dark-mode .post-full-content table td:first-child { + background-image: linear-gradient( + to right, + var(--color-darkmode) 50%, + color-mod(var(--color-darkmode) a(0%)) 100% + ); +} + +html.dark-mode .post-full-content table td:last-child { + background-image: linear-gradient( + to left, + var(--color-darkmode) 50%, + color-mod(var(--color-darkmode) a(0%)) 100% + ); +} + +html.dark-mode .post-full-content table th { + color: rgba(255, 255, 255, 0.85); + background-color: color-mod(var(--color-darkmode) l(+8%)); +} + +html.dark-mode .post-full-content table th, +html.dark-mode .post-full-content table td { + border: color-mod(var(--color-darkmode) l(+8%)) 1px solid; +} + +html.dark-mode .post-full-content .kg-bookmark-container, +html.dark-mode .post-full-content .kg-bookmark-container:hover { + color: rgba(255, 255, 255, 0.75); + box-shadow: 0 0 1px rgba(255, 255, 255, 0.9); +} + +html.dark-mode .post-full-content input { + color: color-mod(var(--color-midgrey) l(-30%)); +} + +html.dark-mode .kg-bookmark-title { + color: #fff; +} + +html.dark-mode .kg-bookmark-description { + color: rgba(255, 255, 255, 0.75); +} + +html.dark-mode .kg-bookmark-metadata { + color: rgba(255, 255, 255, 0.75); +} + +html.dark-mode .site-archive-header .no-image { + color: rgba(255, 255, 255, 0.9); + background: var(--color-darkmode); +} + +html.dark-mode .subscribe-form { + border: none; + background: linear-gradient( + color-mod(var(--color-darkmode) l(-6%)), + color-mod(var(--color-darkmode) l(-3%)) + ); +} + +html.dark-mode .subscribe-form-title { + color: rgba(255, 255, 255, 0.9); +} + +html.dark-mode .subscribe-form p { + color: rgba(255, 255, 255, 0.7); +} + +html.dark-mode .subscribe-email { + border-color: color-mod(var(--color-darkmode) l(+6%)); + color: rgba(255, 255, 255, 0.9); + background: color-mod(var(--color-darkmode) l(+3%)); +} + +html.dark-mode .subscribe-email:focus { + border-color: color-mod(var(--color-darkmode) l(+25%)); +} + +html.dark-mode .subscribe-form button { + opacity: 0.9; +} + +html.dark-mode .subscribe-form .invalid .message-error, +html.dark-mode .subscribe-form .error .message-error { + color: color-mod(var(--color-red) l(+5%) s(-5%)); +} + +html.dark-mode .subscribe-form .success .message-success { + color: color-mod(var(--color-green) l(+5%) s(-5%)); +} diff --git a/src/styles/reset.scss b/src/styles/reset.scss new file mode 100644 index 00000000..d911f735 --- /dev/null +++ b/src/styles/reset.scss @@ -0,0 +1,462 @@ +html, +body, +div, +span, +applet, +object, +iframe, +h1, +h2, +h3, +h4, +h5, +h6, +p, +blockquote, +pre, +a, +abbr, +acronym, +address, +big, +cite, +code, +del, +dfn, +em, +img, +ins, +kbd, +q, +s, +samp, +small, +strike, +strong, +sub, +sup, +tt, +var, +dl, +dt, +dd, +ol, +ul, +li, +fieldset, +form, +label, +legend, +table, +caption, +tbody, +tfoot, +thead, +tr, +th, +td, +article, +aside, +canvas, +details, +embed, +figure, +figcaption, +footer, +header, +hgroup, +menu, +nav, +output, +ruby, +section, +summary, +time, +mark, +audio, +video { + margin: 0; + padding: 0; + border: 0; + font: inherit; + font-size: 100%; + vertical-align: baseline; +} +body { + line-height: 1; +} +ol, +ul { + list-style: none; +} +blockquote, +q { + quotes: none; +} +blockquote:before, +blockquote:after, +q:before, +q:after { + content: ""; + content: none; +} +table { + border-spacing: 0; + border-collapse: collapse; +} +img { + display: block; + max-width: 100%; + height: auto; +} +html { + box-sizing: border-box; + -ms-text-size-adjust: 100%; + -webkit-text-size-adjust: 100%; +} +*, +*:before, +*:after { + box-sizing: inherit; +} +a { + background-color: transparent; +} +a:active, +a:hover { + outline: 0; +} +b, +strong { + font-weight: bold; +} +i, +em, +dfn { + font-style: italic; +} +h1 { + margin: 0.67em 0; + font-size: 2em; +} +small { + font-size: 80%; +} +sub, +sup { + position: relative; + font-size: 75%; + line-height: 0; + vertical-align: baseline; +} +sup { + top: -0.5em; +} +sub { + bottom: -0.25em; +} +img { + border: 0; +} +svg:not(:root) { + overflow: hidden; +} +mark { + background-color: #fdffb6; +} +code, +kbd, +pre, +samp { + font-family: monospace, monospace; + font-size: 1em; +} +button, +input, +optgroup, +select, +textarea { + margin: 0; /* 3 */ + color: inherit; /* 1 */ + font: inherit; /* 2 */ +} +button { + overflow: visible; + border: none; +} +button, +select { + text-transform: none; +} +button, +html input[type="button"], +/* 1 */ +input[type="reset"], +input[type="submit"] { + cursor: pointer; /* 3 */ + + -webkit-appearance: button; /* 2 */ +} +button[disabled], +html input[disabled] { + cursor: default; +} +button::-moz-focus-inner, +input::-moz-focus-inner { + padding: 0; + border: 0; +} +input { + line-height: normal; +} +input:focus { + outline: none; +} +input[type="checkbox"], +input[type="radio"] { + box-sizing: border-box; /* 1 */ + padding: 0; /* 2 */ +} +input[type="number"]::-webkit-inner-spin-button, +input[type="number"]::-webkit-outer-spin-button { + height: auto; +} +input[type="search"] { + box-sizing: content-box; /* 2 */ + + -webkit-appearance: textfield; /* 1 */ +} +input[type="search"]::-webkit-search-cancel-button, +input[type="search"]::-webkit-search-decoration { + -webkit-appearance: none; +} +legend { + padding: 0; /* 2 */ + border: 0; /* 1 */ +} +textarea { + overflow: auto; +} +table { + border-spacing: 0; + border-collapse: collapse; +} +td, +th { + padding: 0; +} + +/* ========================================================================== + Base styles: opinionated defaults + ========================================================================== */ + +html { + font-size: 62.5%; + + -webkit-tap-highlight-color: rgba(0, 0, 0, 0); +} +body { + color: #35373a; + font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen, + Ubuntu, Cantarell, "Open Sans", "Helvetica Neue", sans-serif; + font-size: 1.6rem; + line-height: 1.6em; + font-weight: 400; + font-style: normal; + letter-spacing: 0; + text-rendering: optimizeLegibility; + background: #fff; + + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; + -moz-font-feature-settings: "liga" on; +} + +::selection { + text-shadow: none; + background: #daf2fd; +} + +hr { + position: relative; + display: block; + width: 100%; + margin: 2.5em 0 3.5em; + padding: 0; + height: 1px; + border: 0; + border-top: 1px solid #f0f0f0; +} + +audio, +canvas, +iframe, +img, +svg, +video { + vertical-align: middle; +} + +fieldset { + margin: 0; + padding: 0; + border: 0; +} + +textarea { + resize: vertical; +} + +::not(.gh-content) p, +::not(.gh-content) ul, +::not(.gh-content) ol, +::not(.gh-content) dl, +::not(.gh-content) blockquote { + margin: 0 0 1.5em 0; +} + +ol, +ul { + padding-left: 1.3em; + padding-right: 1.5em; +} + +ol ol, +ul ul, +ul ol, +ol ul { + margin: 0.5em 0 1em; +} + +ul { + list-style: disc; +} + +ol { + list-style: decimal; +} + +ul, +ol { + max-width: 100%; +} + +li { + padding-left: 0.3em; + line-height: 1.6em; +} + +li + li { + margin-top: 0.5em; +} + +dt { + float: left; + margin: 0 20px 0 0; + width: 120px; + color: #daf2fd; + font-weight: 500; + text-align: right; +} + +dd { + margin: 0 0 5px 0; + text-align: left; +} + +blockquote { + margin: 1.5em 0; + padding: 0 1.6em 0 1.6em; + border-left: #daf2fd; +} + +blockquote p { + margin: 0.8em 0; + font-size: 1.2em; + font-weight: 300; +} + +blockquote small { + display: inline-block; + margin: 0.8em 0 0.8em 1.5em; + font-size: 0.9em; + opacity: 0.8; +} +/* Quotation marks */ +blockquote small:before { + content: "\2014 \00A0"; +} + +blockquote cite { + font-weight: bold; +} +blockquote cite a { + font-weight: normal; +} + +a { + color: #15171a; + text-decoration: none; +} + +h1, +h2, +h3, +h4, +h5, +h6 { + margin-top: 0; + line-height: 1.15; + font-weight: 600; + text-rendering: optimizeLegibility; + letter-spacing: -0.01em; +} + +h1 { + margin: 0 0 0.5em 0; + font-size: 4.8rem; + font-weight: 700; + letter-spacing: -0.015em; +} +@media (max-width: 600px) { + h1 { + font-size: 2.8rem; + } +} + +h2 { + margin: 1.5em 0 0.5em 0; + font-size: 2.8rem; + font-weight: 700; +} +@media (max-width: 600px) { + h2 { + font-size: 2.3rem; + } +} + +h3 { + margin: 1.5em 0 0.5em 0; + font-size: 2.4rem; + font-weight: 600; +} +@media (max-width: 600px) { + h3 { + font-size: 1.7rem; + } +} + +h4 { + margin: 1.5em 0 0.5em 0; + font-size: 2.2rem; +} + +h5 { + margin: 1.5em 0 0.5em 0; + font-size: 2rem; +} + +h6 { + margin: 1.5em 0 0.5em 0; + font-size: 1.8rem; +} diff --git a/src/styles/variables.scss b/src/styles/variables.scss new file mode 100644 index 00000000..b0774680 --- /dev/null +++ b/src/styles/variables.scss @@ -0,0 +1,8 @@ +$color-green: #a4d037; +$color-yellow: #fecd35; +$color-red: #f05230; +$color-darkgrey: #15171a; +$color-midgrey: #738a94; +$color-lightgrey: #c5d2d9; +$color-wash: #e5eff5; +$color-darkmode: #151719; diff --git a/src/utils/index.ts b/src/utils/index.ts new file mode 100644 index 00000000..145b2801 --- /dev/null +++ b/src/utils/index.ts @@ -0,0 +1,32 @@ +export const getGhostImgPath = ( + baseUrl: string, + imgUrl: string, + width = 0 +): string => { + if (!imgUrl) return ""; + if (!imgUrl.startsWith(baseUrl)) { + return imgUrl; + } + const relativePath = imgUrl.substring(`${baseUrl}/content/images`.length); + const cleanedBaseUrl = baseUrl.replace(/\/~/, ""); + if (width && width > 0) { + return `${cleanedBaseUrl}/content/images/size/w${width}/${relativePath}`; + } + return `${cleanedBaseUrl}/content/images/${width}${relativePath}`; +}; + +export const truncate = (input: string, size: number): string => + input.length > size ? `${input.substring(0, size)}...` : input; + +export const formatDate = (dateInput: string): string => { + const dateObject = new Date(dateInput); + return dateObject.toDateString(); +}; + +export const uniqWith = ( + arr: Array, + fn: (element: T, step: T) => number +): Array => + arr.filter( + (element, index) => arr.findIndex((step) => fn(element, step)) === index + );