feat: feed feature, typescript, remove google, fix errs & warns
publish / Build-and-publish (push) Successful in 13m32s
publish / Build-and-publish (push) Successful in 13m32s
This commit is contained in:
@@ -13,7 +13,7 @@ jobs:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 20
|
||||
node-version-file: '.nvmrc'
|
||||
cache: 'npm'
|
||||
|
||||
- name: Install dependencies
|
||||
|
||||
@@ -11,10 +11,10 @@ jobs:
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Use Node.js 20
|
||||
- name: Use Node.js from .nvmrc
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 20
|
||||
node-version-file: .nvmrc
|
||||
cache: npm
|
||||
|
||||
- name: Install dependencies
|
||||
|
||||
@@ -30,3 +30,14 @@
|
||||
|
||||
- <img class="alignright" src="/wp-content/uploads/2021/04/2IrsV7v.png" alt="Title" width="352" height="442" />
|
||||
- <img class="alignleft" src="/wp-content/uploads/2021/04/2IrsV7v.png" alt="Title" width="352" height="442" />
|
||||
|
||||
## 📰 Blog & Feed content
|
||||
|
||||
There are two markdown-driven content sections on this site:
|
||||
|
||||
- **Blog** (`content/blog/`) — long-form articles, product releases and case studies. Published at `/blog/`.
|
||||
- **Feed** (`content/feed/`) — short announcements and social-media style updates. Published at `/feed/`.
|
||||
|
||||
Both sections are combined into a single RSS feed at [`/rss.xml`](https://datacontroller.io/rss.xml).
|
||||
|
||||
See [`docs/adding-feed-posts.md`](docs/adding-feed-posts.md) for a step-by-step guide on adding a new Feed post.
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
---
|
||||
title: "Welcome to the Data Controller Feed"
|
||||
description: A new home for quick announcements, social media updates and other bite-sized news from the Data Controller team.
|
||||
date: '2026-07-17 09:00:00'
|
||||
author: 'Data Controller'
|
||||
authorLink: https://www.linkedin.com/showcase/data_controller
|
||||
tags:
|
||||
- Announcements
|
||||
---
|
||||
|
||||
# Welcome to the Feed
|
||||
|
||||
We're launching a new **Feed** section on the Data Controller website! This is where you'll find:
|
||||
|
||||
- Quick announcements
|
||||
- Social media style updates
|
||||
- Smaller news items that don't warrant a full blog post
|
||||
|
||||
It sits alongside our [Blog](/blog/) and both are combined into a single [RSS feed](/rss.xml) so you can subscribe once and get everything in one place.
|
||||
|
||||
Stay tuned for more updates!
|
||||
@@ -0,0 +1,96 @@
|
||||
# Adding a Feed Post
|
||||
|
||||
The **Feed** section (`/feed/`) is for short announcements and social-media
|
||||
style updates — things that don't warrant a full [`content/blog/`](../content/blog)
|
||||
article, but are still worth publishing and syndicating via RSS.
|
||||
|
||||
Feed posts work exactly like blog posts (one markdown file per post, in its
|
||||
own directory), but live under `content/feed/` instead of `content/blog/`,
|
||||
and are published under `/feed/` instead of `/blog/`.
|
||||
|
||||
Both sections share a single combined RSS feed at
|
||||
[`/rss.xml`](https://datacontroller.io/rss.xml) (see `gatsby-plugin-feed` in
|
||||
[`gatsby-config.js`](../gatsby-config.js)), so a new Feed post automatically
|
||||
appears in the RSS feed the next time the site is built — no extra
|
||||
configuration required.
|
||||
|
||||
A visible "RSS Feed" link (pointing to `/rss.xml`) is rendered in two places
|
||||
so visitors can discover and subscribe to it:
|
||||
|
||||
- The site-wide [`Footer`](../src/components/footer/index.tsx), under
|
||||
"Other Resources".
|
||||
- The "Subscribe" section of the shared
|
||||
[`SideBar`](../src/templates/sidebar.tsx), shown on Blog and Feed list/post
|
||||
pages.
|
||||
|
||||
## 1. Create a new post directory
|
||||
|
||||
Create a new folder under `content/feed/`, named after the post slug (this
|
||||
becomes the URL):
|
||||
|
||||
```
|
||||
content/feed/my-new-announcement/index.md
|
||||
```
|
||||
|
||||
## 2. Write the frontmatter + content
|
||||
|
||||
```md
|
||||
---
|
||||
title: 'My New Announcement'
|
||||
description: A one or two sentence summary used for SEO and RSS.
|
||||
date: '2024-06-01 09:00:00'
|
||||
author: 'Your Name'
|
||||
authorLink: https://www.linkedin.com/in/yourprofile/
|
||||
tags:
|
||||
- Announcements
|
||||
---
|
||||
|
||||
Your announcement content goes here, written in regular Markdown.
|
||||
```
|
||||
|
||||
Notes on frontmatter fields:
|
||||
|
||||
| Field | Required | Notes |
|
||||
| ------------- | -------- | ---------------------------------------------------------------------- |
|
||||
| `title` | Yes | Post title, used on the page, in the sidebar and in the RSS item. |
|
||||
| `description` | Yes | Short summary, used for SEO meta tags and the RSS item description. |
|
||||
| `date` | Yes | Format `'YYYY-MM-DD HH:MM:SS'`. Controls sort order and the archive. |
|
||||
| `author` | Yes | Displayed under the post title. |
|
||||
| `authorLink` | No | If set, the author name links out (e.g. to a LinkedIn profile). |
|
||||
| `tags` | Yes | One or more tags. Used for the sidebar category list and RSS `<category>`. |
|
||||
| `previewImg` | No | Optional relative path to an image in the same folder (e.g. `'./cover.png'`). Feed posts commonly omit this since they're short-form. |
|
||||
|
||||
## 3. Add any images (optional)
|
||||
|
||||
If you use `previewImg` or reference images in the body, place the image
|
||||
files in the same post directory and reference them with a relative path,
|
||||
the same as for blog posts.
|
||||
|
||||
## 4. Build / preview
|
||||
|
||||
```shell
|
||||
npm run develop
|
||||
```
|
||||
|
||||
Your new post will be available at `/my-new-announcement/`, and will appear:
|
||||
|
||||
- On the [`/feed/`](https://datacontroller.io/feed/) listing page (with
|
||||
pagination, sidebar archives and tag categories under `/feed/...`).
|
||||
- In the site search index.
|
||||
- In the combined RSS feed at `/rss.xml`, alongside blog posts.
|
||||
|
||||
## How it differs from a Blog post
|
||||
|
||||
| Aspect | Blog | Feed |
|
||||
| ------------------ | ------------------------------ | ----------------------------------- |
|
||||
| Content directory | `content/blog/` | `content/feed/` |
|
||||
| Listing page | `/blog/` | `/feed/` |
|
||||
| Year archive | `/{year}/` | `/feed/{year}/` |
|
||||
| Category page | `/category/{tag}/` | `/feed/category/{tag}/` |
|
||||
| Template | `src/templates/blog-post.tsx`, `src/templates/blog-list.tsx` | `src/templates/feed-post.tsx`, `src/templates/feed-list.tsx` |
|
||||
| RSS | Included in `/rss.xml` | Included in `/rss.xml` |
|
||||
|
||||
Feed archive/category routes are prefixed with `/feed/` specifically to avoid
|
||||
colliding with the equivalent Blog routes, since both content types are
|
||||
rendered by the shared [`src/templates/sidebar.tsx`](../src/templates/sidebar.tsx)
|
||||
component (via its `basePath` prop).
|
||||
@@ -1,4 +1,5 @@
|
||||
import "bootstrap/dist/css/bootstrap.min.css";
|
||||
import "./src/styles/montserrat.css";
|
||||
|
||||
// Import all js dependencies.
|
||||
import "jquery/dist/jquery.min.js";
|
||||
|
||||
Vendored
+15
@@ -0,0 +1,15 @@
|
||||
export interface SiteMetadata {
|
||||
title: string
|
||||
description: string
|
||||
siteUrl: string
|
||||
author: {
|
||||
name: string
|
||||
summary: string
|
||||
}
|
||||
social: {
|
||||
linkedin: string
|
||||
}
|
||||
}
|
||||
|
||||
export const siteMetadata: SiteMetadata
|
||||
export const pathPrefix: string
|
||||
+66
-9
@@ -23,7 +23,6 @@ module.exports = {
|
||||
name: `Data Controller | Flexible and Secure SAS® Data Modification`,
|
||||
short_name: `Data Controller`,
|
||||
description: `Data Controller for SAS® is dedicated to helping users, admins and developers manage their data. A zero code approach with Data Lineage, Catalog, Dictionary, Validation, Workflow, Alerts and more.`,
|
||||
homepage_url: 'https://datacontroller.io/',
|
||||
start_url: '/',
|
||||
background_color: '#fff',
|
||||
theme_color: '#314351',
|
||||
@@ -51,6 +50,14 @@ module.exports = {
|
||||
},
|
||||
__key: 'blog'
|
||||
},
|
||||
{
|
||||
resolve: `gatsby-source-filesystem`,
|
||||
options: {
|
||||
path: `./content/feed`,
|
||||
name: `feed`
|
||||
},
|
||||
__key: 'feed'
|
||||
},
|
||||
{
|
||||
resolve: `gatsby-source-filesystem`,
|
||||
options: {
|
||||
@@ -103,13 +110,6 @@ module.exports = {
|
||||
},
|
||||
__key: 'pages'
|
||||
},
|
||||
{
|
||||
resolve: `gatsby-plugin-google-fonts`,
|
||||
options: {
|
||||
fonts: [`Montserrat\:300,400,500`],
|
||||
display: 'swap'
|
||||
}
|
||||
},
|
||||
{
|
||||
resolve: 'gatsby-plugin-local-search',
|
||||
options: {
|
||||
@@ -131,7 +131,7 @@ module.exports = {
|
||||
// required.
|
||||
query: `
|
||||
{
|
||||
remark: allMarkdownRemark (filter: {fileAbsolutePath: {regex: "/content/blog/"}}) {
|
||||
remark: allMarkdownRemark (filter: {fileAbsolutePath: {regex: "/content\\/(blog|feed)\\//"}}) {
|
||||
posts: edges {
|
||||
post: node {
|
||||
id
|
||||
@@ -184,6 +184,63 @@ module.exports = {
|
||||
html: data.post.html
|
||||
}))
|
||||
}
|
||||
},
|
||||
{
|
||||
resolve: 'gatsby-plugin-feed',
|
||||
options: {
|
||||
query: `
|
||||
{
|
||||
site {
|
||||
siteMetadata {
|
||||
title
|
||||
description
|
||||
siteUrl
|
||||
}
|
||||
}
|
||||
}
|
||||
`,
|
||||
feeds: [
|
||||
{
|
||||
serialize: ({ query: { site, allMarkdownRemark } }) =>
|
||||
allMarkdownRemark.nodes.map((node) => ({
|
||||
title: node.frontmatter.title,
|
||||
description: node.frontmatter.description || '',
|
||||
date: node.frontmatter.date,
|
||||
url: site.siteMetadata.siteUrl + node.fields.slug,
|
||||
guid: site.siteMetadata.siteUrl + node.fields.slug,
|
||||
author: node.frontmatter.author,
|
||||
categories: node.frontmatter.tags || [],
|
||||
custom_elements: [{ 'content:encoded': node.html }]
|
||||
})),
|
||||
query: `
|
||||
{
|
||||
allMarkdownRemark(
|
||||
sort: { frontmatter: { date: DESC } }
|
||||
filter: {
|
||||
fileAbsolutePath: { regex: "/content\\/(blog|feed)\\//" }
|
||||
}
|
||||
) {
|
||||
nodes {
|
||||
html
|
||||
fields {
|
||||
slug
|
||||
}
|
||||
frontmatter {
|
||||
title
|
||||
description
|
||||
date
|
||||
author
|
||||
tags
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`,
|
||||
output: '/rss.xml',
|
||||
title: 'Data Controller — Blog & Feed'
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
+172
-1
@@ -6,6 +6,10 @@ const recentPosts = []
|
||||
const archives = {}
|
||||
const tagsFrequent = []
|
||||
|
||||
const feedRecentPosts = []
|
||||
const feedArchives = {}
|
||||
const feedTagsFrequent = []
|
||||
|
||||
exports.createPages = async ({ graphql, actions, reporter }) => {
|
||||
const { createPage } = actions
|
||||
|
||||
@@ -14,6 +18,10 @@ exports.createPages = async ({ graphql, actions, reporter }) => {
|
||||
const blogListTemplate = path.resolve(`./src/templates/blog-list.tsx`)
|
||||
const blogSearchTemplate = path.resolve(`./src/templates/blog-search.tsx`)
|
||||
|
||||
// Define a template for feed post
|
||||
const feedPostTemplate = path.resolve(`./src/templates/feed-post.tsx`)
|
||||
const feedListTemplate = path.resolve(`./src/templates/feed-list.tsx`)
|
||||
|
||||
// Get all markdown blog posts sorted by date
|
||||
const result = await graphql(
|
||||
`
|
||||
@@ -34,7 +42,10 @@ exports.createPages = async ({ graphql, actions, reporter }) => {
|
||||
}
|
||||
}
|
||||
}
|
||||
tagsGroup: allMarkdownRemark(limit: 1000) {
|
||||
tagsGroup: allMarkdownRemark(
|
||||
limit: 1000
|
||||
filter: { fileAbsolutePath: { regex: "/content/blog/" } }
|
||||
) {
|
||||
group(field: { frontmatter: { tags: SELECT } }) {
|
||||
name: fieldValue
|
||||
totalCount
|
||||
@@ -176,6 +187,166 @@ exports.createPages = async ({ graphql, actions, reporter }) => {
|
||||
tags: tagsFrequent
|
||||
}
|
||||
})
|
||||
|
||||
// ---------------------------------------------------------------------
|
||||
// Feed section (announcements / social updates) - mirrors the blog logic
|
||||
// above but is scoped to /content/feed/ and lives under the /feed/ path
|
||||
// ---------------------------------------------------------------------
|
||||
|
||||
const feedResult = await graphql(
|
||||
`
|
||||
{
|
||||
allMarkdownRemark(
|
||||
sort: { frontmatter: { date: DESC } }
|
||||
limit: 1000
|
||||
filter: { fileAbsolutePath: { regex: "/content/feed/" } }
|
||||
) {
|
||||
nodes {
|
||||
id
|
||||
fields {
|
||||
slug
|
||||
}
|
||||
frontmatter {
|
||||
title
|
||||
date(formatString: "YYYY")
|
||||
}
|
||||
}
|
||||
}
|
||||
tagsGroup: allMarkdownRemark(
|
||||
limit: 1000
|
||||
filter: { fileAbsolutePath: { regex: "/content/feed/" } }
|
||||
) {
|
||||
group(field: { frontmatter: { tags: SELECT } }) {
|
||||
name: fieldValue
|
||||
totalCount
|
||||
}
|
||||
}
|
||||
}
|
||||
`
|
||||
)
|
||||
|
||||
if (feedResult.errors) {
|
||||
reporter.panicOnBuild(
|
||||
`There was an error loading your feed posts`,
|
||||
feedResult.errors
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
const feedPosts = feedResult.data.allMarkdownRemark.nodes
|
||||
feedRecentPosts.push(
|
||||
...feedPosts.slice(0, 10).map((p) => ({
|
||||
slug: p.fields.slug,
|
||||
title: p.frontmatter.title
|
||||
}))
|
||||
)
|
||||
|
||||
const feedTags = feedResult.data.tagsGroup.group
|
||||
feedTagsFrequent.push(
|
||||
...feedTags.sort((a, b) => b.totalCount - a.totalCount).slice(0, 10)
|
||||
)
|
||||
|
||||
feedPosts.forEach((d) => {
|
||||
if (feedArchives[d.frontmatter.date] == null)
|
||||
feedArchives[d.frontmatter.date] = 0
|
||||
feedArchives[d.frontmatter.date]++
|
||||
})
|
||||
|
||||
// Create individual feed post pages
|
||||
if (feedPosts.length > 0) {
|
||||
feedPosts.forEach((post, index) => {
|
||||
const previousPostId = index === 0 ? null : feedPosts[index - 1].id
|
||||
const nextPostId =
|
||||
index === feedPosts.length - 1 ? null : feedPosts[index + 1].id
|
||||
|
||||
createPage({
|
||||
path: post.fields.slug,
|
||||
component: feedPostTemplate,
|
||||
context: {
|
||||
id: post.id,
|
||||
archives: feedArchives,
|
||||
recentPosts: feedRecentPosts,
|
||||
tags: feedTagsFrequent,
|
||||
previousPostId,
|
||||
nextPostId
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
// Create feed-list pages
|
||||
const feedNumPages = Math.ceil(feedPosts.length / postsPerPage)
|
||||
Array.from({ length: feedNumPages }).forEach((_, i) => {
|
||||
createPage({
|
||||
path: i === 0 ? `/feed` : `/feed/page/${i + 1}`,
|
||||
component: feedListTemplate,
|
||||
context: {
|
||||
page: 'index',
|
||||
archives: feedArchives,
|
||||
recentPosts: feedRecentPosts,
|
||||
tags: feedTagsFrequent,
|
||||
filter: { fileAbsolutePath: { regex: '/content/feed/' } },
|
||||
limit: postsPerPage,
|
||||
skip: i * postsPerPage,
|
||||
numPages: feedNumPages,
|
||||
currentPage: i + 1
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
for (year in feedArchives) {
|
||||
const count = feedArchives[year]
|
||||
const numPagesOfYear = Math.ceil(count / postsPerPage)
|
||||
Array.from({ length: numPagesOfYear }).forEach((_, i) => {
|
||||
createPage({
|
||||
path:
|
||||
i === 0 ? `/feed/${year}/` : `/feed/${year}/page/${i + 1}`,
|
||||
component: feedListTemplate,
|
||||
context: {
|
||||
page: 'year',
|
||||
archives: feedArchives,
|
||||
recentPosts: feedRecentPosts,
|
||||
tags: feedTagsFrequent,
|
||||
filter: {
|
||||
frontmatter: { date: { gte: year, lt: year + 1 } },
|
||||
fileAbsolutePath: { regex: '/content/feed/' }
|
||||
},
|
||||
limit: postsPerPage,
|
||||
skip: i * postsPerPage,
|
||||
numPages: numPagesOfYear,
|
||||
currentPage: i + 1,
|
||||
year: year
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
feedTags.forEach((tag) => {
|
||||
const count = tag.totalCount
|
||||
const numPagesOfTag = Math.ceil(count / postsPerPage)
|
||||
Array.from({ length: numPagesOfTag }).forEach((__, i) => {
|
||||
const tagPath = `/feed/category/${_.kebabCase(tag.name)}/`
|
||||
createPage({
|
||||
path: i === 0 ? tagPath : `${tagPath}page/${i + 1}`,
|
||||
component: feedListTemplate,
|
||||
context: {
|
||||
page: 'category',
|
||||
archives: feedArchives,
|
||||
recentPosts: feedRecentPosts,
|
||||
tags: feedTagsFrequent,
|
||||
filter: {
|
||||
frontmatter: { tags: { in: [tag.name] } },
|
||||
fileAbsolutePath: { regex: '/content/feed/' }
|
||||
},
|
||||
limit: postsPerPage,
|
||||
skip: i * postsPerPage,
|
||||
numPages: numPagesOfTag,
|
||||
currentPage: i + 1,
|
||||
tag: tag.name
|
||||
}
|
||||
})
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
exports.onCreatePage = ({ page, actions }) => {
|
||||
|
||||
Generated
+627
-217
File diff suppressed because it is too large
Load Diff
+28
-4
@@ -14,16 +14,18 @@
|
||||
"serve": "gatsby serve",
|
||||
"clean": "gatsby clean",
|
||||
"lint": "npx prettier --check \"src/**/*.+(ts|tsx|js|jsx|json|css|scss)\"",
|
||||
"lint:fix": "npx prettier --write \"src/**/*.+(ts|tsx|js|jsx|json|css|scss)\" --ignore-path .gitignore"
|
||||
"lint:fix": "npx prettier --write \"src/**/*.+(ts|tsx|js|jsx|json|css|scss)\" --ignore-path .gitignore",
|
||||
"typecheck": "tsc --noEmit"
|
||||
},
|
||||
"dependencies": {
|
||||
"@browniebroke/gatsby-image-gallery": "^8.2.0",
|
||||
"@mdx-js/mdx": "^3.1.1",
|
||||
"@mdx-js/react": "^3.1.1",
|
||||
"babel-plugin-styled-components": "^2.1.4",
|
||||
"baseline-browser-mapping": "^2.10.43",
|
||||
"caniuse-lite": "^1.0.30001806",
|
||||
"gatsby": "^5.15.0",
|
||||
"gatsby-plugin-google-analytics": "^5.13.1",
|
||||
"gatsby-plugin-google-fonts": "^1.0.1",
|
||||
"gatsby-plugin-feed": "^5.16.0",
|
||||
"gatsby-plugin-image": "^3.15.0",
|
||||
"gatsby-plugin-local-search": "^2.0.1",
|
||||
"gatsby-plugin-manifest": "^5.15.0",
|
||||
@@ -50,8 +52,30 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@popperjs/core": "^2.9.2",
|
||||
"@types/lodash": "^4.17.24",
|
||||
"@types/node": "^20.19.43",
|
||||
"@types/react": "^18.3.31",
|
||||
"@types/react-dom": "^18.3.7",
|
||||
"@types/react-helmet": "^6.1.11",
|
||||
"bootstrap": "^5.0.0-beta3",
|
||||
"cloudron-surfer": "^6.4.1",
|
||||
"jquery": "^3.6.0"
|
||||
"jquery": "^3.6.0",
|
||||
"typescript": "^7.0.2"
|
||||
},
|
||||
"allowScripts": {
|
||||
"lmdb@2.5.2": true,
|
||||
"lmdb@2.5.3": true,
|
||||
"msgpackr@1.10.2": true,
|
||||
"@parcel/watcher@2.5.1": true
|
||||
},
|
||||
"overrides": {
|
||||
"msgpackr": "^1.12.1",
|
||||
"lmdb": "^2.9.4",
|
||||
"ordered-binary": "^1.6.1",
|
||||
"whatwg-url": "^17.1.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=24.0.0",
|
||||
"npm": ">=10.0.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,11 @@
|
||||
import React from 'react'
|
||||
import { FaRss } from 'react-icons/fa'
|
||||
import { Section } from '../shared'
|
||||
import { SolidButton } from '../shared/styledComponents'
|
||||
import { StyledHeading, StyledDesc, InputStyled, StyledAnchor } from './style'
|
||||
import { siteMetadata } from '../../../gatsby-config'
|
||||
|
||||
const rssFeedUrl = `${siteMetadata.siteUrl}rss.xml`
|
||||
|
||||
const anchorStyles = {
|
||||
color: '#888'
|
||||
@@ -13,42 +17,39 @@ const Footer = () => (
|
||||
<div className="col-md-3 me-md-5">
|
||||
<StyledHeading>Data Controller</StyledHeading>
|
||||
<StyledDesc>
|
||||
Data Controller is the product of a UK company with a singular focus on{' '}
|
||||
<StyledAnchor
|
||||
href="https://sasapps.io"
|
||||
target="_blank"
|
||||
rel="noopener"
|
||||
>
|
||||
SAS Web Apps
|
||||
</StyledAnchor>.
|
||||
Data Controller is the product of a UK company with a singular focus
|
||||
on <StyledAnchor href="https://sasapps.io">SAS Web Apps</StyledAnchor>
|
||||
.
|
||||
</StyledDesc>
|
||||
</div>
|
||||
<div className="col-md-3">
|
||||
<StyledHeading>Source Code</StyledHeading>
|
||||
<StyledDesc>
|
||||
All our source code can be found on our self-hosted{' '}
|
||||
<StyledAnchor
|
||||
href="https://git.datacontroller.io"
|
||||
target="_blank"
|
||||
rel="noopener"
|
||||
>
|
||||
Data Controller source is on our self-hosted{' '}
|
||||
<StyledAnchor href="https://git.datacontroller.io">
|
||||
Gitea Repository
|
||||
</StyledAnchor>
|
||||
.
|
||||
; the underlying SASjs framework is on{' '}
|
||||
<StyledAnchor href="https://github.com/sasjs">GitHub</StyledAnchor>.
|
||||
</StyledDesc>
|
||||
</div>
|
||||
<div className="col-md-3">
|
||||
<StyledHeading>Other Resources</StyledHeading>
|
||||
<StyledDesc>
|
||||
Leverage our underlying tech stack on{' '}
|
||||
<StyledAnchor
|
||||
href="https://github.com/sasjs"
|
||||
target="_blank"
|
||||
rel="noopener"
|
||||
>
|
||||
Github
|
||||
Connect on{' '}
|
||||
<StyledAnchor href={siteMetadata.social.linkedin}>
|
||||
LinkedIn
|
||||
</StyledAnchor>
|
||||
, read the{' '}
|
||||
<StyledAnchor href="https://docs.datacontroller.io">
|
||||
docs
|
||||
</StyledAnchor>
|
||||
, or subscribe to the{' '}
|
||||
<StyledAnchor href={rssFeedUrl}>
|
||||
<FaRss style={{ marginRight: '6px', verticalAlign: 'middle' }} />
|
||||
RSS feed
|
||||
</StyledAnchor>{' '}
|
||||
and build your own SAS Powered Web Apps.
|
||||
for updates.
|
||||
</StyledDesc>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import React from 'react'
|
||||
import * as React from 'react'
|
||||
import styled from 'styled-components'
|
||||
|
||||
import type { StyledAnchorProps } from '../../types'
|
||||
|
||||
export const StyledHeading = styled.h6`
|
||||
margin-bottom: 0.8rem;
|
||||
text-transform: uppercase;
|
||||
@@ -33,7 +35,10 @@ const Anchor = styled.a`
|
||||
}
|
||||
`
|
||||
|
||||
export const StyledAnchor = ({ children, href }) => (
|
||||
export const StyledAnchor: React.FC<StyledAnchorProps> = ({
|
||||
children,
|
||||
href
|
||||
}) => (
|
||||
<Anchor href={href} target="_blank" rel="noopener">
|
||||
{children}
|
||||
</Anchor>
|
||||
@@ -1,21 +1,15 @@
|
||||
import React from 'react'
|
||||
import { PageProps, Link } from 'gatsby'
|
||||
|
||||
import styled from 'styled-components'
|
||||
import { Link } from 'gatsby'
|
||||
|
||||
import { Hero, HeroHeading, HeroDesc } from './style'
|
||||
import { BottomSectionArrow, OutlineButton } from '../shared/styledComponents'
|
||||
import { Container } from '../shared'
|
||||
|
||||
import { pathPrefix } from '../../../gatsby-config.js'
|
||||
import { pathPrefix } from '../../../gatsby-config'
|
||||
|
||||
type DataProps = {
|
||||
location: Location
|
||||
heading: string
|
||||
desc: string
|
||||
}
|
||||
import type { HeroSectionProps } from '../../types'
|
||||
|
||||
const HeroSection: React.FC<PageProps<DataProps>> = ({
|
||||
const HeroSection: React.FC<HeroSectionProps> = ({
|
||||
location,
|
||||
heading,
|
||||
desc
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import styled from 'styled-components'
|
||||
import background from '../../images/home_hero_bg.png'
|
||||
|
||||
export const Hero = styled.main`
|
||||
export const Hero = styled.main<{ bg?: boolean }>`
|
||||
position: relative;
|
||||
padding: 50px 0;
|
||||
color: white;
|
||||
@@ -13,7 +13,8 @@ export const Hero = styled.main`
|
||||
`
|
||||
|
||||
export const HeroHeading = styled.h1`
|
||||
font-family: 'Montserrat', 'HelveticaNeue', 'Helvetica Neue', Helvetica, Arial,
|
||||
font-family:
|
||||
'Montserrat', 'HelveticaNeue', 'Helvetica Neue', Helvetica, Arial,
|
||||
sans-serif;
|
||||
text-transform: uppercase;
|
||||
`
|
||||
@@ -1,16 +1,12 @@
|
||||
import React, { useEffect } from 'react'
|
||||
import { PageProps } from 'gatsby'
|
||||
|
||||
import Navibar from './navibar'
|
||||
import HeroSection from './herosection'
|
||||
import Footer from './footer'
|
||||
|
||||
type DataProps = {
|
||||
children?: React.ReactNode
|
||||
heroSection: boolean
|
||||
}
|
||||
import type { LayoutProps } from '../types'
|
||||
|
||||
const Layout: React.FC<PageProps<DataProps>> = ({
|
||||
const Layout: React.FC<LayoutProps> = ({
|
||||
location,
|
||||
children,
|
||||
heroSection = true,
|
||||
|
||||
@@ -1,49 +1,34 @@
|
||||
import React from 'react'
|
||||
import { Link, PageProps } from 'gatsby'
|
||||
import { Link } from 'gatsby'
|
||||
|
||||
import dcLogo from '../../images/dclogo.png'
|
||||
|
||||
import { Container } from '../shared'
|
||||
import { logoStyles, CustomNavBar, ulStyles, Li, StyledLink } from './style'
|
||||
|
||||
import { pathPrefix } from '../../../gatsby-config.js'
|
||||
import { pathPrefix } from '../../../gatsby-config'
|
||||
|
||||
const naviLinks = [
|
||||
{
|
||||
name: 'Home',
|
||||
url: '/',
|
||||
active: 'no'
|
||||
},
|
||||
{
|
||||
name: 'About',
|
||||
url: '/about/',
|
||||
active: 'no'
|
||||
},
|
||||
{
|
||||
name: 'Blog',
|
||||
url: '/blog/',
|
||||
active: 'no'
|
||||
},
|
||||
{
|
||||
name: 'FAQ',
|
||||
url: '/faq/',
|
||||
active: 'no'
|
||||
},
|
||||
import type { NavibarProps } from '../../types'
|
||||
|
||||
interface NaviLink {
|
||||
name: string
|
||||
url: string
|
||||
active: 'yes' | 'no'
|
||||
}
|
||||
|
||||
const naviLinks: NaviLink[] = [
|
||||
{ name: 'Home', url: '/', active: 'no' },
|
||||
{ name: 'About', url: '/about/', active: 'no' },
|
||||
{ name: 'Blog', url: '/blog/', active: 'no' },
|
||||
{ name: 'Feed', url: '/feed/', active: 'no' },
|
||||
{ name: 'FAQ', url: '/faq/', active: 'no' },
|
||||
{
|
||||
name: 'Documentation',
|
||||
url: 'https://docs.datacontroller.io/',
|
||||
active: 'no'
|
||||
},
|
||||
{
|
||||
name: 'Pricing',
|
||||
url: '/pricing/',
|
||||
active: 'no'
|
||||
},
|
||||
{
|
||||
name: 'Book Demo',
|
||||
url: '/contact/',
|
||||
active: 'no'
|
||||
},
|
||||
{ name: 'Pricing', url: '/pricing/', active: 'no' },
|
||||
{ name: 'Book Demo', url: '/contact/', active: 'no' },
|
||||
{
|
||||
name: 'Source Code',
|
||||
url: 'https://git.datacontroller.io/dc/dc',
|
||||
@@ -51,11 +36,7 @@ const naviLinks = [
|
||||
}
|
||||
]
|
||||
|
||||
type DataProps = {
|
||||
location: Location
|
||||
}
|
||||
|
||||
const Navibar: React.FC<PageProps<DataProps>> = ({ location }) => {
|
||||
const Navibar: React.FC<NavibarProps> = ({ location }) => {
|
||||
naviLinks.forEach((link) => (link.active = 'no'))
|
||||
const currentLink = naviLinks.find(
|
||||
(link) => pathPrefix + link.url === location?.pathname
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
import React from 'react'
|
||||
import * as React from 'react'
|
||||
import styled, { css } from 'styled-components'
|
||||
import { Link } from 'gatsby'
|
||||
|
||||
// styles
|
||||
export const logoStyles = {
|
||||
height: '55px'
|
||||
}
|
||||
@@ -295,7 +294,6 @@ const LinkUnderlineStyles = css`
|
||||
transition: opacity 0.3s ease;
|
||||
`
|
||||
|
||||
// styled components
|
||||
export const Li = styled.li`
|
||||
position: relative;
|
||||
@media (min-width: 992px) {
|
||||
@@ -315,7 +313,14 @@ export const Li = styled.li`
|
||||
}
|
||||
}
|
||||
`
|
||||
export const StyledLink = styled((props) => <Link {...props} />)`
|
||||
|
||||
interface StyledLinkProps {
|
||||
active?: string
|
||||
}
|
||||
|
||||
export const StyledLink = styled(({ active, ...rest }: any) => (
|
||||
<Link {...rest} />
|
||||
))<StyledLinkProps>`
|
||||
padding-right: 0.8rem !important;
|
||||
padding-left: 0.8rem !important;
|
||||
color: white !important;
|
||||
+21
-22
@@ -1,9 +1,16 @@
|
||||
import * as React from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import { Helmet } from 'react-helmet'
|
||||
import { useStaticQuery, graphql } from 'gatsby'
|
||||
|
||||
const Seo = ({ description, lang, meta, title, previewImg = undefined }) => {
|
||||
import type { SeoProps } from '../types'
|
||||
|
||||
const Seo: React.FC<SeoProps> = ({
|
||||
description = '',
|
||||
lang = 'en',
|
||||
meta = [],
|
||||
title = '',
|
||||
previewImg
|
||||
}) => {
|
||||
const { site } = useStaticQuery(graphql`
|
||||
query {
|
||||
site {
|
||||
@@ -37,44 +44,36 @@ const Seo = ({ description, lang, meta, title, previewImg = undefined }) => {
|
||||
lang
|
||||
}}
|
||||
title={pageTitle}
|
||||
meta={[
|
||||
link={[
|
||||
{
|
||||
rel: 'alternate',
|
||||
type: 'application/rss+xml',
|
||||
title: `${defaultTitle} RSS Feed`,
|
||||
href: `${siteUrl}rss.xml`
|
||||
}
|
||||
]}
|
||||
meta={(
|
||||
[
|
||||
{ name: 'author', property: 'author', content: author },
|
||||
{
|
||||
name: 'description',
|
||||
property: 'og:description',
|
||||
content: metaDescription
|
||||
},
|
||||
// { name: 'facebook:site', content: '', },
|
||||
{ name: 'image', property: 'og:image', content: image },
|
||||
{
|
||||
name: `linkedin:site`,
|
||||
content: site.siteMetadata?.social?.linkedin || ``
|
||||
},
|
||||
{ name: `twitter:card`, content: `summary` },
|
||||
// { name: `twitter:creator`, content: site.siteMetadata?.social?.twitter || `` },
|
||||
{ name: `twitter:description`, content: metaDescription },
|
||||
// { name: 'twitter:site', content: `${site?.twitter}`, },
|
||||
{ name: `twitter:title`, content: title },
|
||||
// { name: 'youtube:site', content: `${site?.youtube}`, },
|
||||
{ property: `og:title`, content: title },
|
||||
{ property: `og:type`, content: `website` }
|
||||
].concat(meta)}
|
||||
] as Array<{ name?: string; property?: string; content?: string }>
|
||||
).concat(meta)}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
Seo.defaultProps = {
|
||||
lang: `en`,
|
||||
meta: [],
|
||||
description: ``,
|
||||
title: ``
|
||||
}
|
||||
|
||||
Seo.propTypes = {
|
||||
description: PropTypes.string,
|
||||
lang: PropTypes.string,
|
||||
meta: PropTypes.arrayOf(PropTypes.object),
|
||||
title: PropTypes.string
|
||||
}
|
||||
|
||||
export default Seo
|
||||
|
||||
@@ -1,10 +1,7 @@
|
||||
import React from 'react'
|
||||
import * as React from 'react'
|
||||
import styled from 'styled-components'
|
||||
import { PageProps } from 'gatsby'
|
||||
|
||||
type DataProps = {
|
||||
children?: React.ReactNode
|
||||
}
|
||||
import type { ContainerProps } from '../../types'
|
||||
|
||||
const StyledDiv = styled.div`
|
||||
@media (min-width: 576px) {
|
||||
@@ -13,6 +10,6 @@ const StyledDiv = styled.div`
|
||||
}
|
||||
`
|
||||
|
||||
export const Container: React.FC<PageProps<DataProps>> = ({ children }) => {
|
||||
export const Container: React.FC<ContainerProps> = ({ children }) => {
|
||||
return <StyledDiv className="container">{children}</StyledDiv>
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@ import styled from 'styled-components'
|
||||
|
||||
import { FaEnvelope } from 'react-icons/fa'
|
||||
|
||||
export const StyledLink = styled((props) => <Link {...props} />)`
|
||||
export const StyledLink = styled(Link)`
|
||||
color: rgb(255, 255, 255);
|
||||
background-color: rgb(144, 196, 69);
|
||||
border-radius: 0px;
|
||||
|
||||
@@ -1,25 +1,19 @@
|
||||
import React from 'react'
|
||||
import * as React from 'react'
|
||||
import styled from 'styled-components'
|
||||
|
||||
import { PageProps } from 'gatsby'
|
||||
import { BottomSectionArrow } from './styledComponents'
|
||||
import { Container } from './'
|
||||
|
||||
type DataProps = {
|
||||
children?: React.ReactNode
|
||||
color?: string
|
||||
bgColor?: string
|
||||
bottomArrow?: boolean
|
||||
}
|
||||
import type { SectionProps } from '../../types'
|
||||
|
||||
const StyledSection = styled.div`
|
||||
const StyledSection = styled.div<{ color?: string; bgColor?: string }>`
|
||||
position: relative;
|
||||
padding: 50px 0;
|
||||
color: ${(props) => props.color || 'white'};
|
||||
background-color: ${(props) => props.bgColor || '#314351'};
|
||||
`
|
||||
|
||||
export const Section: React.FC<PageProps<DataProps>> = ({
|
||||
export const Section: React.FC<SectionProps> = ({
|
||||
children,
|
||||
bgColor,
|
||||
color,
|
||||
|
||||
+10
-10
@@ -1,6 +1,8 @@
|
||||
import React from 'react'
|
||||
import * as React from 'react'
|
||||
import styled from 'styled-components'
|
||||
|
||||
import type { SolidButtonProps, OutlineButtonProps } from '../../types'
|
||||
|
||||
const BottomArrow = styled.div`
|
||||
width: 50px;
|
||||
height: 50px;
|
||||
@@ -9,9 +11,6 @@ const BottomArrow = styled.div`
|
||||
background: inherit;
|
||||
transform: translateX(-50%) rotate(45deg);
|
||||
left: 50%;
|
||||
// right: 0;
|
||||
// margin-left: auto;
|
||||
// margin-right: auto;
|
||||
z-index: 10;
|
||||
`
|
||||
const BottomArrowWrapper = styled.div`
|
||||
@@ -29,16 +28,17 @@ export const BottomSectionArrow = () => (
|
||||
</BottomArrowWrapper>
|
||||
)
|
||||
|
||||
export const SectionHeading = styled.h2`
|
||||
export const SectionHeading = styled.h2<{ center?: string }>`
|
||||
text-align: ${(props) => (props.center === 'no' ? 'left' : 'center')};
|
||||
letter-spacing: 1px;
|
||||
font-weight: 400;
|
||||
font-family: 'Montserrat', 'HelveticaNeue', 'Helvetica Neue', Helvetica, Arial,
|
||||
font-family:
|
||||
'Montserrat', 'HelveticaNeue', 'Helvetica Neue', Helvetica, Arial,
|
||||
sans-serif;
|
||||
text-transform: uppercase;
|
||||
`
|
||||
|
||||
export const SectionDesc = styled.p`
|
||||
export const SectionDesc = styled.p<{ center?: string; opacity?: string }>`
|
||||
text-align: ${(props) => (props.center === 'no' ? 'left' : 'center')};
|
||||
opacity: ${(props) => props.opacity ?? 0.6};
|
||||
a {
|
||||
@@ -59,12 +59,12 @@ const StyledSolidButton = styled.button`
|
||||
}
|
||||
`
|
||||
|
||||
export const SolidButton = ({
|
||||
export const SolidButton: React.FC<SolidButtonProps> = ({
|
||||
children,
|
||||
theme = 'light',
|
||||
type = 'submit',
|
||||
disabled = false,
|
||||
onClick = undefined
|
||||
onClick
|
||||
}) => (
|
||||
<StyledSolidButton
|
||||
type={type}
|
||||
@@ -83,7 +83,7 @@ const StyledOutlineButton = styled.button`
|
||||
border-width: 2px;
|
||||
`
|
||||
|
||||
export const OutlineButton = ({ children }) => (
|
||||
export const OutlineButton: React.FC<OutlineButtonProps> = ({ children }) => (
|
||||
<StyledOutlineButton type="button" className="btn btn-outline-light">
|
||||
{children}
|
||||
</StyledOutlineButton>
|
||||
@@ -34,7 +34,7 @@ export class NostrController extends EventEmitter {
|
||||
/**
|
||||
* Function will publish provided event to the provided relays
|
||||
*/
|
||||
publishEvent = async (event: Event, relays: string[]) => {
|
||||
publishEvent = async (event: Event, relays: string[]): Promise<string[]> => {
|
||||
const simplePool = new SimplePool()
|
||||
const promises = simplePool.publish(relays, event)
|
||||
|
||||
@@ -47,7 +47,7 @@ export class NostrController extends EventEmitter {
|
||||
})
|
||||
|
||||
if (publishedRelays.length === 0) {
|
||||
const failedPublishes: any[] = []
|
||||
const failedPublishes: Array<{ relay: string; error: string }> = []
|
||||
const fallbackRejectionReason =
|
||||
'Attempt to publish an event has been rejected with unknown reason.'
|
||||
|
||||
@@ -92,7 +92,7 @@ export class NostrController extends EventEmitter {
|
||||
return Promise.resolve(signedEvent)
|
||||
}
|
||||
|
||||
nip04Encrypt = async (receiver: string, content: string) => {
|
||||
nip04Encrypt = async (receiver: string, content: string): Promise<string> => {
|
||||
if (!this.generatedKeys) {
|
||||
throw new Error(`Private & public key pair is not found.`)
|
||||
}
|
||||
@@ -113,7 +113,7 @@ export class NostrController extends EventEmitter {
|
||||
* @param isSigner Boolean indicating whether the recipient is a signer or viewer.
|
||||
* @param setAuthUrl Function to set the authentication URL in the component state.
|
||||
*/
|
||||
sendDM = async (pubkey: string, message: string) => {
|
||||
sendDM = async (pubkey: string, message: string): Promise<string[]> => {
|
||||
// Set up timeout promise to handle encryption timeout
|
||||
const timeoutPromise = new Promise<never>((_, reject) => {
|
||||
setTimeout(() => {
|
||||
@@ -159,7 +159,7 @@ export class NostrController extends EventEmitter {
|
||||
* @param hexKey hex private or public key
|
||||
* @returns whether or not is key valid
|
||||
*/
|
||||
validateHex = (hexKey: string) => {
|
||||
validateHex = (hexKey: string): RegExpMatchArray | null => {
|
||||
return hexKey.match(/^[a-f0-9]{64}$/)
|
||||
}
|
||||
|
||||
@@ -193,7 +193,7 @@ export class NostrController extends EventEmitter {
|
||||
return { private: nip19.nsecEncode(nsec), public: getPublicKey(nsec) }
|
||||
}
|
||||
|
||||
verifySignedEvent = (event: SignedEvent) => {
|
||||
verifySignedEvent = (event: SignedEvent): void => {
|
||||
const isGood = verifyEvent(event)
|
||||
|
||||
if (!isGood) {
|
||||
|
||||
+8
-3
@@ -1,5 +1,4 @@
|
||||
import React from 'react'
|
||||
// import { Link } from 'gatsby'
|
||||
import * as React from 'react'
|
||||
|
||||
import Layout from '../components/layout'
|
||||
import Seo from '../components/seo'
|
||||
@@ -7,7 +6,13 @@ import Seo from '../components/seo'
|
||||
import { Section } from '../components/shared'
|
||||
import { SideBar } from '../templates/sidebar'
|
||||
|
||||
const NotFound = ({ data, location, pageContext }) => (
|
||||
import type { PageProps } from 'gatsby'
|
||||
import type { PageContext } from '../types'
|
||||
|
||||
const NotFound: React.FC<PageProps<unknown, PageContext>> = ({
|
||||
location,
|
||||
pageContext
|
||||
}) => (
|
||||
<Layout location={location} heroSection={false}>
|
||||
<Seo title="Page Not Found" />
|
||||
<Section color="black" bgColor="white" bottomArrow={false}>
|
||||
|
||||
+3
-13
@@ -1,5 +1,5 @@
|
||||
import { PageProps, Link, graphql } from 'gatsby'
|
||||
import React from 'react'
|
||||
import { PageProps, Link } from 'gatsby'
|
||||
import * as React from 'react'
|
||||
|
||||
import Layout from '../components/layout'
|
||||
import Seo from '../components/seo'
|
||||
@@ -10,17 +10,7 @@ import {
|
||||
SectionDesc
|
||||
} from '../components/shared/styledComponents'
|
||||
|
||||
type DataProps = {
|
||||
site: {
|
||||
meta: {
|
||||
title: string
|
||||
description: string
|
||||
social: { linkedin: string }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const About: React.FC<PageProps<DataProps>> = ({ data, location }) => {
|
||||
const About: React.FC<PageProps<unknown>> = ({ location }) => {
|
||||
return (
|
||||
<Layout
|
||||
location={location}
|
||||
|
||||
+11
-21
@@ -1,5 +1,5 @@
|
||||
import { PageProps, Link, graphql } from 'gatsby'
|
||||
import React, { useState } from 'react'
|
||||
import { PageProps } from 'gatsby'
|
||||
import * as React from 'react'
|
||||
|
||||
import Layout from '../components/layout'
|
||||
import Seo from '../components/seo'
|
||||
@@ -22,25 +22,15 @@ import contactBg from '../images/contact_bg.jpg'
|
||||
import '../styledComponents/contact.css'
|
||||
import { NostrController } from '../controllers'
|
||||
|
||||
type DataProps = {
|
||||
site: {
|
||||
meta: {
|
||||
title: string
|
||||
description: string
|
||||
social: { linkedin: string }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const Contact: React.FC<PageProps<DataProps>> = ({ data, location }) => {
|
||||
const Contact: React.FC<PageProps<unknown>> = ({ location }) => {
|
||||
const nostrController = NostrController.getInstance()
|
||||
|
||||
const [name, setName] = useState<string>()
|
||||
const [email, setEmail] = useState<string>()
|
||||
const [emailError, setEmailError] = useState<boolean>(false)
|
||||
const [subject, setSubject] = useState<string>()
|
||||
const [message, setMessage] = useState<string>()
|
||||
const [notification, setNotification] = useState<string>()
|
||||
const [name, setName] = React.useState<string>()
|
||||
const [email, setEmail] = React.useState<string>()
|
||||
const [emailError, setEmailError] = React.useState<boolean>(false)
|
||||
const [subject, setSubject] = React.useState<string>()
|
||||
const [message, setMessage] = React.useState<string>()
|
||||
const [notification, setNotification] = React.useState<string>()
|
||||
|
||||
const handleEmailChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const value = e.target.value
|
||||
@@ -98,7 +88,7 @@ Email: ${email}
|
||||
Subject: ${subject}
|
||||
Message: ${message}`
|
||||
)
|
||||
.catch((err) => {
|
||||
.catch((err: unknown) => {
|
||||
setNotification(
|
||||
`Something went wrong. Please check the console for more information. Please try one more time.`
|
||||
)
|
||||
@@ -176,7 +166,7 @@ Message: ${message}`
|
||||
className="form-control contactFormStyles"
|
||||
id="mesage"
|
||||
name="message"
|
||||
rows="5"
|
||||
rows={5}
|
||||
rules="required|max:200"
|
||||
onChange={(evt) => {
|
||||
setMessage(evt.target.value)
|
||||
|
||||
+3
-13
@@ -1,5 +1,5 @@
|
||||
import { PageProps, Link, graphql } from 'gatsby'
|
||||
import React from 'react'
|
||||
import { PageProps, Link } from 'gatsby'
|
||||
import * as React from 'react'
|
||||
|
||||
import Layout from '../components/layout'
|
||||
import Seo from '../components/seo'
|
||||
@@ -13,17 +13,7 @@ import {
|
||||
|
||||
import { FAQ } from '../styledComponents/faq'
|
||||
|
||||
type DataProps = {
|
||||
site: {
|
||||
meta: {
|
||||
title: string
|
||||
description: string
|
||||
social: { linkedin: string }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const Faq: React.FC<PageProps<DataProps>> = ({ data, location }) => {
|
||||
const Faq: React.FC<PageProps<unknown>> = ({ location }) => {
|
||||
return (
|
||||
<Layout
|
||||
location={location}
|
||||
|
||||
+15
-23
@@ -1,5 +1,5 @@
|
||||
import { Link, graphql } from 'gatsby'
|
||||
import React from 'react'
|
||||
import * as React from 'react'
|
||||
import { Link, graphql, PageProps } from 'gatsby'
|
||||
import Gallery from '@browniebroke/gatsby-image-gallery'
|
||||
import { IGatsbyImageData } from 'gatsby-plugin-image'
|
||||
|
||||
@@ -34,31 +34,31 @@ import dcDesign from '../images/data-controller-design.png'
|
||||
|
||||
interface ImageSharpEdge {
|
||||
node: {
|
||||
name: string
|
||||
childImageSharp: {
|
||||
thumb: IGatsbyImageData
|
||||
full: IGatsbyImageData
|
||||
meta: {
|
||||
originalName: string
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
interface PageProps {
|
||||
data: {
|
||||
interface IndexPageData {
|
||||
images: {
|
||||
edges: ImageSharpEdge[]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const Home: React.FC<PageProps> = ({ data, location }) => {
|
||||
const CustomWrapper: React.FC<{
|
||||
children?: React.ReactNode
|
||||
onClick?: () => void
|
||||
}> = ({ children, onClick }) => (
|
||||
<ThumbnailWrapper onClick={onClick}>{children}</ThumbnailWrapper>
|
||||
)
|
||||
|
||||
const Home: React.FC<PageProps<IndexPageData>> = ({ data, location }) => {
|
||||
const images = data.images.edges.map(({ node }) => ({
|
||||
...node.childImageSharp,
|
||||
// Use original name as caption.
|
||||
// The `originalName` is queried in a nested field,
|
||||
// but the `Gallery` component expects `caption` at the top level.
|
||||
caption: node.childImageSharp.meta.originalName
|
||||
caption: node.name
|
||||
}))
|
||||
|
||||
return (
|
||||
@@ -231,13 +231,7 @@ const Home: React.FC<PageProps> = ({ data, location }) => {
|
||||
</Section>
|
||||
<Section color="black" bgColor="white">
|
||||
<SectionHeading>See How it Looks</SectionHeading>
|
||||
<Gallery
|
||||
images={images}
|
||||
gutter={0}
|
||||
customWrapper={({ children, onClick }) => (
|
||||
<ThumbnailWrapper onClick={onClick}>{children}</ThumbnailWrapper>
|
||||
)}
|
||||
/>
|
||||
<Gallery images={images} gutter="0" customWrapper={CustomWrapper} />
|
||||
</Section>
|
||||
<ScheduleDemo />
|
||||
</Layout>
|
||||
@@ -254,6 +248,7 @@ export const pageQuery = graphql`
|
||||
) {
|
||||
edges {
|
||||
node {
|
||||
name
|
||||
childImageSharp {
|
||||
thumb: gatsbyImageData(
|
||||
width: 300
|
||||
@@ -261,9 +256,6 @@ export const pageQuery = graphql`
|
||||
placeholder: BLURRED
|
||||
)
|
||||
full: gatsbyImageData(layout: FULL_WIDTH)
|
||||
meta: fixed {
|
||||
originalName
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import React from 'react'
|
||||
import { graphql } from 'gatsby'
|
||||
import * as React from 'react'
|
||||
import { graphql, PageProps } from 'gatsby'
|
||||
import styled from 'styled-components'
|
||||
|
||||
import Layout from '../components/layout'
|
||||
@@ -7,6 +7,8 @@ import Seo from '../components/seo'
|
||||
|
||||
import { Section } from '../components/shared'
|
||||
|
||||
import type { PostNode, PageContext } from '../types'
|
||||
|
||||
const StyledMarkdown = styled.div`
|
||||
color: rgb(102, 102, 102);
|
||||
h2,
|
||||
@@ -18,13 +20,20 @@ const StyledMarkdown = styled.div`
|
||||
h2 {
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 1px;
|
||||
font-family: 'Montserrat', 'HelveticaNeue', 'Helvetica Neue', Helvetica,
|
||||
Arial, sans-serif;
|
||||
font-family:
|
||||
'Montserrat', 'HelveticaNeue', 'Helvetica Neue', Helvetica, Arial,
|
||||
sans-serif;
|
||||
}
|
||||
`
|
||||
|
||||
export default function Template({ data, location }) {
|
||||
const { frontmatter, html } = data?.markdownRemark
|
||||
interface MarkdownPageData {
|
||||
markdownRemark: PostNode
|
||||
}
|
||||
|
||||
const MarkdownPageTemplate: React.FC<
|
||||
PageProps<MarkdownPageData, PageContext>
|
||||
> = ({ data, location }) => {
|
||||
const { frontmatter, html } = data.markdownRemark
|
||||
return (
|
||||
<Layout
|
||||
location={location}
|
||||
@@ -39,6 +48,8 @@ export default function Template({ data, location }) {
|
||||
)
|
||||
}
|
||||
|
||||
export default MarkdownPageTemplate
|
||||
|
||||
export const pageQuery = graphql`
|
||||
query ($id: String!) {
|
||||
markdownRemark(id: { eq: $id }) {
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import styled from 'styled-components'
|
||||
|
||||
export const StyledHeading = styled.h5`
|
||||
font-family: 'Montserrat', 'HelveticaNeue', 'Helvetica Neue', Helvetica, Arial,
|
||||
font-family:
|
||||
'Montserrat', 'HelveticaNeue', 'Helvetica Neue', Helvetica, Arial,
|
||||
sans-serif;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 1px;
|
||||
@@ -1,17 +0,0 @@
|
||||
import styled from 'styled-components'
|
||||
|
||||
export const StyledHeading = styled.h3`
|
||||
font-family: 'Montserrat', 'HelveticaNeue', 'Helvetica Neue', Helvetica, Arial,
|
||||
sans-serif;
|
||||
`
|
||||
|
||||
export const StyledLabel = styled.label`
|
||||
margin-bottom: 0;
|
||||
`
|
||||
|
||||
export const ContactBackground = styled.img.attrs((props) => ({
|
||||
alt: props.info || '',
|
||||
title: props.info || ''
|
||||
}))`
|
||||
max-width: 100%;
|
||||
`
|
||||
@@ -0,0 +1,20 @@
|
||||
import styled from 'styled-components'
|
||||
|
||||
export const StyledHeading = styled.h3`
|
||||
font-family:
|
||||
'Montserrat', 'HelveticaNeue', 'Helvetica Neue', Helvetica, Arial,
|
||||
sans-serif;
|
||||
`
|
||||
|
||||
export const StyledLabel = styled.label`
|
||||
margin-bottom: 0;
|
||||
`
|
||||
|
||||
export const ContactBackground = styled.img.attrs<{ info?: string }>(
|
||||
(props) => ({
|
||||
alt: props.info || '',
|
||||
title: props.info || ''
|
||||
})
|
||||
)`
|
||||
max-width: 100%;
|
||||
`
|
||||
@@ -1,15 +1,18 @@
|
||||
import React from 'react'
|
||||
import * as React from 'react'
|
||||
import styled from 'styled-components'
|
||||
|
||||
import { FcFaq } from 'react-icons/fc'
|
||||
|
||||
import type { FaqProps } from '../types'
|
||||
|
||||
const StyledFaq = styled.div`
|
||||
margin-bottom: 20px;
|
||||
`
|
||||
|
||||
const StyledQuestion = styled.h5`
|
||||
color: #222;
|
||||
font-family: 'Montserrat', 'HelveticaNeue', 'Helvetica Neue', Helvetica, Arial,
|
||||
font-family:
|
||||
'Montserrat', 'HelveticaNeue', 'Helvetica Neue', Helvetica, Arial,
|
||||
sans-serif;
|
||||
text-transform: uppercase;
|
||||
`
|
||||
@@ -33,7 +36,7 @@ const FaqDetails = styled.div`
|
||||
margin-bottom: 20px;
|
||||
`
|
||||
|
||||
export const FAQ = ({ question, answer }) => (
|
||||
export const FAQ: React.FC<FaqProps> = ({ question, answer }) => (
|
||||
<StyledFaq>
|
||||
<IconHolderStyled>
|
||||
<FcFaq size={40} />
|
||||
@@ -1,6 +1,9 @@
|
||||
import React from 'react'
|
||||
import * as React from 'react'
|
||||
import styled from 'styled-components'
|
||||
import { ImArrowRight } from 'react-icons/im'
|
||||
import type { IconType } from 'react-icons'
|
||||
|
||||
import type { ArtProps, ReasonProps, FeatureProps } from '../types'
|
||||
|
||||
export const CenteredText = styled.p`
|
||||
text-align: center;
|
||||
@@ -9,20 +12,19 @@ export const CenteredText = styled.p`
|
||||
margin: 0 auto;
|
||||
font-size: 1.2rem;
|
||||
line-height: 1;
|
||||
font-family: 'Montserrat', 'HelveticaNeue', 'Helvetica Neue', Helvetica, Arial,
|
||||
font-family:
|
||||
'Montserrat', 'HelveticaNeue', 'Helvetica Neue', Helvetica, Arial,
|
||||
sans-serif;
|
||||
`
|
||||
export const Art = styled.img.attrs((props) => ({
|
||||
export const Art = styled.img.attrs<ArtProps>((props) => ({
|
||||
alt: props.info || '',
|
||||
title: props.info || ''
|
||||
}))`
|
||||
max-width: 100%;
|
||||
display: block;
|
||||
margin: 0 auto;
|
||||
padding: 20px;
|
||||
height: auto;
|
||||
`
|
||||
|
||||
const ArrowHolder = styled.div`
|
||||
const ArrowHolder = styled.div<{ bgColor?: string }>`
|
||||
display: inline-block;
|
||||
border-radius: 50px;
|
||||
padding: 15px 17px;
|
||||
@@ -30,7 +32,7 @@ const ArrowHolder = styled.div`
|
||||
background-color: ${(props) =>
|
||||
props.bgColor === 'red' ? '#ed1c1c' : '#90c445'};
|
||||
`
|
||||
const RightArrow = ({ bgColor = '' }) => (
|
||||
const RightArrow: React.FC<{ bgColor?: string }> = ({ bgColor = '' }) => (
|
||||
<ArrowHolder bgColor={bgColor}>
|
||||
<ImArrowRight size={24} />
|
||||
</ArrowHolder>
|
||||
@@ -46,7 +48,7 @@ const ReasonDesc = styled.p`
|
||||
text-transform: uppercase;
|
||||
`
|
||||
|
||||
export const Reason = ({ text, bgColor = '' }) => (
|
||||
export const Reason: React.FC<ReasonProps> = ({ text, bgColor = '' }) => (
|
||||
<>
|
||||
<RightArrow bgColor={bgColor} />
|
||||
<ReasonDesc>{text}</ReasonDesc>
|
||||
@@ -62,7 +64,7 @@ const IconHolderStyled = styled.div`
|
||||
vertical-align: top;
|
||||
`
|
||||
|
||||
const IconHolder = ({ Icon }) => {
|
||||
const IconHolder = ({ Icon }: { Icon: IconType }) => {
|
||||
return (
|
||||
<IconHolderStyled>
|
||||
<Icon size={24} />
|
||||
@@ -77,7 +79,8 @@ const FeatureDetails = styled.div`
|
||||
margin-bottom: 20px;
|
||||
`
|
||||
const FeatureTitle = styled.h5`
|
||||
font-family: 'Montserrat', 'HelveticaNeue', 'Helvetica Neue', Helvetica, Arial,
|
||||
font-family:
|
||||
'Montserrat', 'HelveticaNeue', 'Helvetica Neue', Helvetica, Arial,
|
||||
sans-serif;
|
||||
text-transform: uppercase;
|
||||
`
|
||||
@@ -86,7 +89,12 @@ const FeatureDesc = styled.p`
|
||||
font-size: 0.9rem;
|
||||
`
|
||||
|
||||
export const Feature = ({ title, desc, Icon, className }) => (
|
||||
export const Feature: React.FC<FeatureProps> = ({
|
||||
title,
|
||||
desc,
|
||||
Icon,
|
||||
className
|
||||
}) => (
|
||||
<div className={className ?? 'col-md-4'}>
|
||||
<IconHolder Icon={Icon} />
|
||||
<FeatureDetails>
|
||||
@@ -0,0 +1,35 @@
|
||||
@font-face {
|
||||
font-family: 'Montserrat';
|
||||
font-style: normal;
|
||||
font-weight: 300;
|
||||
font-display: swap;
|
||||
src: url('/fonts/Montserrat-300.woff2') format('woff2');
|
||||
unicode-range:
|
||||
U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC,
|
||||
U+0304, U+0308, U+0329, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193,
|
||||
U+2212, U+2215, U+FEFF, U+FFFD;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'Montserrat';
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
font-display: swap;
|
||||
src: url('/fonts/Montserrat-400.woff2') format('woff2');
|
||||
unicode-range:
|
||||
U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC,
|
||||
U+0304, U+0308, U+0329, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193,
|
||||
U+2212, U+2215, U+FEFF, U+FFFD;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'Montserrat';
|
||||
font-style: normal;
|
||||
font-weight: 500;
|
||||
font-display: swap;
|
||||
src: url('/fonts/Montserrat-500.woff2') format('woff2');
|
||||
unicode-range:
|
||||
U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC,
|
||||
U+0304, U+0308, U+0329, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193,
|
||||
U+2212, U+2215, U+FEFF, U+FFFD;
|
||||
}
|
||||
+25
-15
@@ -1,5 +1,5 @@
|
||||
import { Link, graphql } from 'gatsby'
|
||||
import React from 'react'
|
||||
import { Link, graphql, PageProps } from 'gatsby'
|
||||
import * as React from 'react'
|
||||
import kebabCase from 'lodash/kebabCase'
|
||||
import styled from 'styled-components'
|
||||
|
||||
@@ -16,27 +16,39 @@ import {
|
||||
import Post from './postpreview'
|
||||
import { SideBar } from './sidebar'
|
||||
|
||||
const BlogListTemplate = ({ data, location, pageContext }) => {
|
||||
import type { PostNode, PageContext } from '../types'
|
||||
|
||||
interface BlogListData {
|
||||
remark: {
|
||||
posts: Array<{ post: PostNode }>
|
||||
}
|
||||
}
|
||||
|
||||
const BlogListTemplate: React.FC<PageProps<BlogListData, PageContext>> = ({
|
||||
data,
|
||||
location,
|
||||
pageContext
|
||||
}) => {
|
||||
const posts = data.remark.posts
|
||||
|
||||
const iniPath =
|
||||
pageContext.page == 'index'
|
||||
const iniPath: string =
|
||||
pageContext.page === 'index'
|
||||
? `/blog/`
|
||||
: pageContext.page == 'year'
|
||||
: pageContext.page === 'year'
|
||||
? `/${pageContext.year}/`
|
||||
: pageContext.page == 'category'
|
||||
? `/category/${kebabCase(pageContext.tag)}/`
|
||||
: null
|
||||
: pageContext.page === 'category'
|
||||
? `/category/${kebabCase(pageContext.tag ?? '')}/`
|
||||
: `/blog/`
|
||||
|
||||
const pageInfo = `Page ${pageContext.currentPage} of ${pageContext.numPages}`
|
||||
Array.from({ length: 5 }, (v, k) => k + 1)
|
||||
const paginationJSX = Array.from(
|
||||
{ length: pageContext.numPages },
|
||||
{ length: pageContext.numPages ?? 0 },
|
||||
(_, i) => i + 1
|
||||
).map((pageIndex) => {
|
||||
const link = pageIndex === 1 ? iniPath : `${iniPath}page/${pageIndex}`
|
||||
return (
|
||||
<Link
|
||||
key={pageIndex}
|
||||
to={link}
|
||||
className={`btn btn-outline-dark btn-sm ${
|
||||
pageIndex === pageContext.currentPage ? 'disabled' : ''
|
||||
@@ -62,8 +74,8 @@ const BlogListTemplate = ({ data, location, pageContext }) => {
|
||||
<div className="row">
|
||||
<div className="col-md-7">
|
||||
<div className="row">
|
||||
{posts.map((data, i) => (
|
||||
<Post key={i} post={data.post} />
|
||||
{posts.map(({ post }, i) => (
|
||||
<Post key={i} post={post} />
|
||||
))}
|
||||
</div>
|
||||
<span className="float-start">{paginationJSX}</span>
|
||||
@@ -114,5 +126,3 @@ export const pageQuery = graphql`
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
// tags
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import React from 'react'
|
||||
import { Link, graphql } from 'gatsby'
|
||||
import * as React from 'react'
|
||||
import { graphql, PageProps } from 'gatsby'
|
||||
|
||||
import Layout from '../components/layout'
|
||||
import Seo from '../components/seo'
|
||||
@@ -8,15 +8,19 @@ import { Section } from '../components/shared'
|
||||
import Post from './post'
|
||||
import { SideBar } from './sidebar'
|
||||
|
||||
const BlogPostTemplate = ({ data, location, pageContext }) => {
|
||||
import type { PostNode, PageContext } from '../types'
|
||||
|
||||
const BlogPostTemplate: React.FC<
|
||||
PageProps<{ post: PostNode }, PageContext>
|
||||
> = ({ data, location, pageContext }) => {
|
||||
const { post } = data
|
||||
const { previewImg } = post.frontmatter
|
||||
|
||||
return (
|
||||
<Layout location={location} heroSection={false}>
|
||||
<Seo
|
||||
title={post?.frontmatter?.title}
|
||||
description={post?.frontmatter?.description}
|
||||
title={post.frontmatter.title}
|
||||
description={post.frontmatter.description}
|
||||
previewImg={
|
||||
previewImg?.childImageSharp?.gatsbyImageData?.images?.fallback?.src
|
||||
}
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
import { Link, graphql, useStaticQuery } from 'gatsby'
|
||||
import React from 'react'
|
||||
import kebabCase from 'lodash/kebabCase'
|
||||
import { Link, graphql, useStaticQuery, PageProps } from 'gatsby'
|
||||
import * as React from 'react'
|
||||
import { useFlexSearch } from 'react-use-flexsearch'
|
||||
import styled from 'styled-components'
|
||||
|
||||
import Layout from '../components/layout'
|
||||
import Seo from '../components/seo'
|
||||
@@ -17,7 +15,19 @@ import {
|
||||
import Post from './postpreview'
|
||||
import { SideBar } from './sidebar'
|
||||
|
||||
const BlogSearchTemplate = ({ location, pageContext }) => {
|
||||
import type { PageContext, PostNode, SearchPost } from '../types'
|
||||
|
||||
interface SearchData {
|
||||
localSearchBlog: {
|
||||
index: string
|
||||
store: Record<string, unknown>
|
||||
}
|
||||
}
|
||||
|
||||
const BlogSearchTemplate: React.FC<PageProps<SearchData, PageContext>> = ({
|
||||
location,
|
||||
pageContext
|
||||
}) => {
|
||||
const queryData = useStaticQuery(graphql`
|
||||
query {
|
||||
localSearchBlog {
|
||||
@@ -34,12 +44,12 @@ const BlogSearchTemplate = ({ location, pageContext }) => {
|
||||
query,
|
||||
queryData.localSearchBlog.index,
|
||||
queryData.localSearchBlog.store
|
||||
)
|
||||
const postsJSX = []
|
||||
) as SearchPost[]
|
||||
const postsJSX: React.ReactElement[] = []
|
||||
|
||||
if (query) {
|
||||
posts.forEach((post, i) => {
|
||||
const _post = {
|
||||
const _post: PostNode = {
|
||||
id: post.id,
|
||||
html: post.html,
|
||||
fields: {
|
||||
@@ -48,7 +58,7 @@ const BlogSearchTemplate = ({ location, pageContext }) => {
|
||||
frontmatter: {
|
||||
title: post.title,
|
||||
date: post.date,
|
||||
previewImg: post.previewImg
|
||||
previewImg: post.previewImg as PostNode['frontmatter']['previewImg']
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,131 @@
|
||||
import { Link, graphql, PageProps } from 'gatsby'
|
||||
import * as React from 'react'
|
||||
import kebabCase from 'lodash/kebabCase'
|
||||
|
||||
import Layout from '../components/layout'
|
||||
import Seo from '../components/seo'
|
||||
|
||||
import { Section } from '../components/shared'
|
||||
import {
|
||||
SectionHeading,
|
||||
SectionDesc,
|
||||
SolidButton
|
||||
} from '../components/shared/styledComponents'
|
||||
|
||||
import Post from './postpreview'
|
||||
import { SideBar } from './sidebar'
|
||||
|
||||
import type { PostNode, PageContext } from '../types'
|
||||
|
||||
interface FeedListData {
|
||||
remark: {
|
||||
posts: Array<{ post: PostNode }>
|
||||
}
|
||||
}
|
||||
|
||||
const FeedListTemplate: React.FC<PageProps<FeedListData, PageContext>> = ({
|
||||
data,
|
||||
location,
|
||||
pageContext
|
||||
}) => {
|
||||
const posts = data.remark.posts
|
||||
|
||||
const iniPath: string =
|
||||
pageContext.page === 'index'
|
||||
? `/feed/`
|
||||
: pageContext.page === 'year'
|
||||
? `/feed/${pageContext.year}/`
|
||||
: pageContext.page === 'category'
|
||||
? `/feed/category/${kebabCase(pageContext.tag ?? '')}/`
|
||||
: `/feed/`
|
||||
|
||||
const pageInfo = `Page ${pageContext.currentPage} of ${pageContext.numPages}`
|
||||
const paginationJSX = Array.from(
|
||||
{ length: pageContext.numPages ?? 0 },
|
||||
(_, i) => i + 1
|
||||
).map((pageIndex) => {
|
||||
const link = pageIndex === 1 ? iniPath : `${iniPath}page/${pageIndex}`
|
||||
return (
|
||||
<Link
|
||||
key={pageIndex}
|
||||
to={link}
|
||||
className={`btn btn-outline-dark btn-sm ${
|
||||
pageIndex === pageContext.currentPage ? 'disabled' : ''
|
||||
}`}
|
||||
style={{
|
||||
borderRadius: '50%',
|
||||
padding: '.375rem .75rem',
|
||||
margin: '0.25rem'
|
||||
}}
|
||||
>
|
||||
{pageIndex}
|
||||
</Link>
|
||||
)
|
||||
})
|
||||
return (
|
||||
<Layout
|
||||
location={location}
|
||||
heading="Data Controller’s Feed"
|
||||
desc="Announcements and social media style updates from the Data Controller team."
|
||||
>
|
||||
<Seo title="Feed" />
|
||||
<Section color="black" bgColor="white" bottomArrow={false}>
|
||||
<div className="row">
|
||||
<div className="col-md-7">
|
||||
<div className="row">
|
||||
{posts.map(({ post }, i) => (
|
||||
<Post key={i} post={post} />
|
||||
))}
|
||||
</div>
|
||||
<span className="float-start">{paginationJSX}</span>
|
||||
<span className="float-end">{pageInfo}</span>
|
||||
</div>
|
||||
<div className="col-md-5">
|
||||
<SideBar
|
||||
pageContext={pageContext}
|
||||
location={location}
|
||||
basePath="/feed"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</Section>
|
||||
</Layout>
|
||||
)
|
||||
}
|
||||
|
||||
export default FeedListTemplate
|
||||
|
||||
export const pageQuery = graphql`
|
||||
query FeedListQuery(
|
||||
$filter: MarkdownRemarkFilterInput!
|
||||
$skip: Int!
|
||||
$limit: Int!
|
||||
) {
|
||||
remark: allMarkdownRemark(
|
||||
filter: $filter
|
||||
limit: $limit
|
||||
skip: $skip
|
||||
sort: { frontmatter: { date: DESC } }
|
||||
) {
|
||||
posts: edges {
|
||||
post: node {
|
||||
html
|
||||
fields {
|
||||
slug
|
||||
}
|
||||
frontmatter {
|
||||
title
|
||||
date(formatString: "MMMM DD, YYYY")
|
||||
author
|
||||
authorLink
|
||||
previewImg {
|
||||
childImageSharp {
|
||||
gatsbyImageData(layout: CONSTRAINED)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`
|
||||
@@ -0,0 +1,71 @@
|
||||
import * as React from 'react'
|
||||
import { graphql, PageProps } from 'gatsby'
|
||||
|
||||
import Layout from '../components/layout'
|
||||
import Seo from '../components/seo'
|
||||
|
||||
import { Section } from '../components/shared'
|
||||
import Post from './post'
|
||||
import { SideBar } from './sidebar'
|
||||
|
||||
import type { PostNode, PageContext } from '../types'
|
||||
|
||||
const FeedPostTemplate: React.FC<
|
||||
PageProps<{ post: PostNode }, PageContext>
|
||||
> = ({ data, location, pageContext }) => {
|
||||
const { post } = data
|
||||
const { previewImg } = post.frontmatter
|
||||
|
||||
return (
|
||||
<Layout location={location} heroSection={false}>
|
||||
<Seo
|
||||
title={post.frontmatter.title}
|
||||
description={post.frontmatter.description}
|
||||
previewImg={
|
||||
previewImg?.childImageSharp?.gatsbyImageData?.images?.fallback?.src
|
||||
}
|
||||
/>
|
||||
<Section color="black" bgColor="white" bottomArrow={false}>
|
||||
<div className="row">
|
||||
<div className="col-md-7">
|
||||
<Post post={post} location={location} />
|
||||
</div>
|
||||
<div className="col-md-5">
|
||||
<SideBar
|
||||
pageContext={pageContext}
|
||||
location={location}
|
||||
basePath="/feed"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</Section>
|
||||
</Layout>
|
||||
)
|
||||
}
|
||||
|
||||
export default FeedPostTemplate
|
||||
|
||||
export const pageQuery = graphql`
|
||||
query FeedPostByPath($id: String!) {
|
||||
post: markdownRemark(id: { eq: $id }) {
|
||||
id
|
||||
html
|
||||
fields {
|
||||
slug
|
||||
}
|
||||
frontmatter {
|
||||
title
|
||||
description
|
||||
date(formatString: "MMMM DD, YYYY")
|
||||
author
|
||||
authorLink
|
||||
previewImg {
|
||||
childImageSharp {
|
||||
gatsbyImageData(layout: CONSTRAINED)
|
||||
}
|
||||
}
|
||||
tags
|
||||
}
|
||||
}
|
||||
}
|
||||
`
|
||||
@@ -1,12 +1,14 @@
|
||||
import React from 'react'
|
||||
import * as React from 'react'
|
||||
import { Link } from 'gatsby'
|
||||
import kebabCase from 'lodash/kebabCase'
|
||||
|
||||
import { GatsbyImage } from 'gatsby-plugin-image'
|
||||
import { GatsbyImage, IGatsbyImageData } from 'gatsby-plugin-image'
|
||||
|
||||
import styled from 'styled-components'
|
||||
|
||||
export const StyledLink = styled((props) => <Link {...props} />)`
|
||||
import type { PostNode } from '../../types'
|
||||
|
||||
export const StyledLink = styled(Link)`
|
||||
text-decoration: none;
|
||||
color: black;
|
||||
&:hover {
|
||||
@@ -16,7 +18,8 @@ export const StyledLink = styled((props) => <Link {...props} />)`
|
||||
`
|
||||
|
||||
const StyledTitle = styled.h5`
|
||||
font-family: 'Montserrat', 'HelveticaNeue', 'Helvetica Neue', Helvetica, Arial,
|
||||
font-family:
|
||||
'Montserrat', 'HelveticaNeue', 'Helvetica Neue', Helvetica, Arial,
|
||||
sans-serif;
|
||||
margin-bottom: 0;
|
||||
`
|
||||
@@ -45,7 +48,8 @@ const StyledContent = styled.div`
|
||||
padding: 15px 0;
|
||||
color: #666666;
|
||||
|
||||
font-family: 'Montserrat', 'HelveticaNeue', 'Helvetica Neue', Helvetica, Arial,
|
||||
font-family:
|
||||
'Montserrat', 'HelveticaNeue', 'Helvetica Neue', Helvetica, Arial,
|
||||
sans-serif;
|
||||
a {
|
||||
color: black;
|
||||
@@ -116,7 +120,12 @@ const StyledContent = styled.div`
|
||||
}
|
||||
`
|
||||
|
||||
const Post = ({ post, location }) => {
|
||||
interface PostProps {
|
||||
post: PostNode
|
||||
location?: Location
|
||||
}
|
||||
|
||||
const Post: React.FC<PostProps> = ({ post }) => {
|
||||
const tagsJSX = (post.frontmatter?.tags || []).map((tag, index) => (
|
||||
<span key={index}>
|
||||
{index > 0 && ', '}
|
||||
@@ -134,14 +143,18 @@ const Post = ({ post, location }) => {
|
||||
) : (
|
||||
<span>{post.frontmatter.author}</span>
|
||||
)
|
||||
const previewImgData = post.frontmatter.previewImg?.childImageSharp
|
||||
?.gatsbyImageData as IGatsbyImageData | undefined
|
||||
return (
|
||||
<div>
|
||||
{previewImgData && (
|
||||
<GatsbyImage
|
||||
image={post.frontmatter.previewImg.childImageSharp.gatsbyImageData}
|
||||
image={previewImgData}
|
||||
style={{ width: '100%', marginBottom: '1rem' }}
|
||||
imgStyle={{ objectFit: 'contain' }}
|
||||
alt={post.frontmatter.title}
|
||||
/>
|
||||
)}
|
||||
|
||||
<StyledTitle>{post.frontmatter.title}</StyledTitle>
|
||||
<StyledDate>
|
||||
|
||||
@@ -1,13 +1,15 @@
|
||||
import React from 'react'
|
||||
import * as React from 'react'
|
||||
import { Link } from 'gatsby'
|
||||
|
||||
import { GatsbyImage } from 'gatsby-plugin-image'
|
||||
import { GatsbyImage, IGatsbyImageData } from 'gatsby-plugin-image'
|
||||
|
||||
import styled from 'styled-components'
|
||||
|
||||
import getDescription from '../shared/getDescription'
|
||||
|
||||
export const StyledLink = styled((props) => <Link {...props} />)`
|
||||
import type { PostNode } from '../../types'
|
||||
|
||||
export const StyledLink = styled(Link)`
|
||||
text-decoration: none;
|
||||
color: black;
|
||||
&:hover {
|
||||
@@ -17,7 +19,8 @@ export const StyledLink = styled((props) => <Link {...props} />)`
|
||||
`
|
||||
|
||||
const StyledTitle = styled.h5`
|
||||
font-family: 'Montserrat', 'HelveticaNeue', 'Helvetica Neue', Helvetica, Arial,
|
||||
font-family:
|
||||
'Montserrat', 'HelveticaNeue', 'Helvetica Neue', Helvetica, Arial,
|
||||
sans-serif;
|
||||
font-size: 1rem;
|
||||
margin-bottom: 0;
|
||||
@@ -34,21 +37,31 @@ const StyledDesc = styled.p`
|
||||
margin-bottom: 100px;
|
||||
`
|
||||
|
||||
const Post = ({ post }) => (
|
||||
interface PostPreviewProps {
|
||||
post: PostNode
|
||||
}
|
||||
|
||||
const PostPreview: React.FC<PostPreviewProps> = ({ post }) => {
|
||||
const previewImgData = post.frontmatter.previewImg?.childImageSharp
|
||||
?.gatsbyImageData as IGatsbyImageData | undefined
|
||||
return (
|
||||
<div className="col-md-6">
|
||||
<StyledLink to={post.fields.slug}>
|
||||
{previewImgData && (
|
||||
<GatsbyImage
|
||||
image={post.frontmatter.previewImg.childImageSharp.gatsbyImageData}
|
||||
image={previewImgData}
|
||||
style={{ minHeight: '150px', maxHeight: '200px' }}
|
||||
imgStyle={{ objectFit: 'contain' }}
|
||||
alt={post.frontmatter.title}
|
||||
/>
|
||||
)}
|
||||
|
||||
<StyledTitle>{post.frontmatter.title}</StyledTitle>
|
||||
<StyledDate>{post.frontmatter.date}</StyledDate>
|
||||
<StyledDesc>{getDescription(post.html)}</StyledDesc>
|
||||
</StyledLink>
|
||||
</div>
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
export default Post
|
||||
export default PostPreview
|
||||
|
||||
@@ -7,7 +7,7 @@ const extractContent = (s: string): string => {
|
||||
return s
|
||||
}
|
||||
const getDescription = (content: string): string => {
|
||||
return extractContent(content).substr(0, 100) + '...'
|
||||
return extractContent(content).slice(0, 100) + '...'
|
||||
}
|
||||
|
||||
export default getDescription
|
||||
|
||||
+66
-20
@@ -1,12 +1,17 @@
|
||||
import React, { useState, useEffect } from 'react'
|
||||
import * as React from 'react'
|
||||
import { Link, navigate } from 'gatsby'
|
||||
import kebabCase from 'lodash/kebabCase'
|
||||
import styled, { css } from 'styled-components'
|
||||
import { pathPrefix } from '../../gatsby-config.js'
|
||||
import { FaRss } from 'react-icons/fa'
|
||||
import { pathPrefix, siteMetadata } from '../../gatsby-config'
|
||||
|
||||
import { StyledHeading } from '../styledComponents/blog'
|
||||
import { SolidButton } from '../components/shared/styledComponents'
|
||||
|
||||
import type { PageContext, RecentPost, TagCount } from '../types'
|
||||
|
||||
const rssFeedUrl = `${siteMetadata.siteUrl}rss.xml`
|
||||
|
||||
const styles = css`
|
||||
color: #314351;
|
||||
opacity: 0.8;
|
||||
@@ -29,10 +34,16 @@ const linkStyles = css`
|
||||
text-decoration: underline;
|
||||
}
|
||||
`
|
||||
const ArchiveLink = styled((props) => <Link {...props} />)`
|
||||
const ArchiveLink = styled(Link)`
|
||||
${linkStyles}
|
||||
`
|
||||
const RecentPostLink = styled((props) => <Link {...props} />)`
|
||||
const RssAnchor = styled.a`
|
||||
${linkStyles}
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
`
|
||||
const RecentPostLink = styled(Link)`
|
||||
${linkStyles}
|
||||
padding: 3px;
|
||||
border-bottom: 3px solid #e1e1e1;
|
||||
@@ -59,30 +70,44 @@ const StyledPara = styled.p`
|
||||
${styles}
|
||||
`
|
||||
|
||||
const Archives = ({ archives }) => (
|
||||
interface ArchivesProps {
|
||||
archives: Record<string, number>
|
||||
basePath?: string
|
||||
}
|
||||
|
||||
const Archives: React.FC<ArchivesProps> = ({ archives, basePath = '' }) => (
|
||||
<>
|
||||
{Object.keys(archives)
|
||||
.sort()
|
||||
.reverse()
|
||||
.map((year, index) => (
|
||||
<ArchiveLink key={year} to={`/${year}/`}>
|
||||
.map((year) => (
|
||||
<ArchiveLink key={year} to={`${basePath}/${year}/`}>
|
||||
{year} ({archives[year]})
|
||||
</ArchiveLink>
|
||||
))}
|
||||
</>
|
||||
)
|
||||
|
||||
const Categories = ({ tags }) => (
|
||||
interface CategoriesProps {
|
||||
tags: TagCount[]
|
||||
basePath?: string
|
||||
}
|
||||
|
||||
const Categories: React.FC<CategoriesProps> = ({ tags, basePath = '' }) => (
|
||||
<>
|
||||
{tags.map((tag, i) => (
|
||||
<ArchiveLink key={i} to={`/category/${kebabCase(tag.name)}/`}>
|
||||
<ArchiveLink key={i} to={`${basePath}/category/${kebabCase(tag.name)}/`}>
|
||||
{tag.name} ({tag.totalCount})
|
||||
</ArchiveLink>
|
||||
))}
|
||||
</>
|
||||
)
|
||||
|
||||
const RecentPosts = ({ posts }) => (
|
||||
interface RecentPostsProps {
|
||||
posts: RecentPost[]
|
||||
}
|
||||
|
||||
const RecentPosts: React.FC<RecentPostsProps> = ({ posts }) => (
|
||||
<>
|
||||
{posts.map((post) => (
|
||||
<RecentPostLink key={post.slug} to={post.slug}>
|
||||
@@ -92,22 +117,34 @@ const RecentPosts = ({ posts }) => (
|
||||
</>
|
||||
)
|
||||
|
||||
export const SideBar = ({ pageContext, location, notFoundPage = false }) => {
|
||||
interface SideBarProps {
|
||||
pageContext: PageContext
|
||||
location: Location
|
||||
notFoundPage?: boolean
|
||||
basePath?: string
|
||||
}
|
||||
|
||||
export const SideBar: React.FC<SideBarProps> = ({
|
||||
pageContext,
|
||||
location,
|
||||
notFoundPage = false,
|
||||
basePath = ''
|
||||
}) => {
|
||||
const params = new URLSearchParams(location.search.substring(1))
|
||||
const queryUrl = params?.get('s') || ''
|
||||
const [query, setQuery] = useState(queryUrl)
|
||||
useEffect(() => {
|
||||
const [query, setQuery] = React.useState(queryUrl)
|
||||
React.useEffect(() => {
|
||||
setQuery(queryUrl)
|
||||
}, [queryUrl])
|
||||
|
||||
const handleSubmit = (event) => {
|
||||
const handleSubmit = (event: React.FormEvent<HTMLFormElement>) => {
|
||||
event.preventDefault()
|
||||
navigate('/search?s=' + query)
|
||||
}
|
||||
|
||||
const _handleKeyDown = (event) => {
|
||||
const _handleKeyDown = (event: React.KeyboardEvent<HTMLInputElement>) => {
|
||||
if (event.key === 'Enter') {
|
||||
handleSubmit(event)
|
||||
navigate('/search?s=' + query)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -130,13 +167,16 @@ export const SideBar = ({ pageContext, location, notFoundPage = false }) => {
|
||||
type="text"
|
||||
value={query}
|
||||
onChange={(e) => setQuery(e.target.value)}
|
||||
onKeyDown={(e) => _handleKeyDown(e)}
|
||||
onKeyDown={_handleKeyDown}
|
||||
className="form-control"
|
||||
placeholder="Search"
|
||||
aria-label="Search"
|
||||
aria-describedby="button-addon2"
|
||||
/>
|
||||
<SolidButton theme="dark" onClick={handleSubmit}>
|
||||
<SolidButton
|
||||
theme="dark"
|
||||
onClick={() => navigate('/search?s=' + query)}
|
||||
>
|
||||
Search
|
||||
</SolidButton>
|
||||
<form onSubmit={handleSubmit}></form>
|
||||
@@ -168,11 +208,17 @@ export const SideBar = ({ pageContext, location, notFoundPage = false }) => {
|
||||
<>
|
||||
<SideBarSection>
|
||||
<StyledHeading>Archives</StyledHeading>
|
||||
<Archives archives={pageContext.archives} />
|
||||
<Archives archives={pageContext.archives} basePath={basePath} />
|
||||
</SideBarSection>
|
||||
<SideBarSection>
|
||||
<StyledHeading>Categories</StyledHeading>
|
||||
<Categories tags={pageContext.tags} />
|
||||
<Categories tags={pageContext.tags} basePath={basePath} />
|
||||
</SideBarSection>
|
||||
<SideBarSection>
|
||||
<StyledHeading>Subscribe</StyledHeading>
|
||||
<RssAnchor href={rssFeedUrl} target="_blank" rel="noopener">
|
||||
<FaRss /> RSS Feed
|
||||
</RssAnchor>
|
||||
</SideBarSection>
|
||||
</>
|
||||
)}
|
||||
|
||||
Vendored
+25
@@ -0,0 +1,25 @@
|
||||
declare module '*.png'
|
||||
declare module '*.jpg'
|
||||
declare module '*.jpeg'
|
||||
declare module '*.svg'
|
||||
declare module '*.gif'
|
||||
declare module '*.webp'
|
||||
declare module '*.ico'
|
||||
declare module '*.css'
|
||||
declare module '*.scss'
|
||||
|
||||
declare module 'react-use-flexsearch' {
|
||||
export function useFlexSearch(
|
||||
query: string | null,
|
||||
index: string,
|
||||
store: Record<string, unknown>
|
||||
): Array<Record<string, unknown>>
|
||||
}
|
||||
|
||||
declare module '@browniebroke/gatsby-image-gallery' {
|
||||
import * as React from 'react'
|
||||
const Gallery: React.FC<
|
||||
Record<string, unknown> & { images: Array<Record<string, unknown>> }
|
||||
>
|
||||
export default Gallery
|
||||
}
|
||||
Vendored
+10
@@ -0,0 +1,10 @@
|
||||
import 'react'
|
||||
|
||||
declare module 'react' {
|
||||
interface InputHTMLAttributes<T> extends HTMLAttributes<T> {
|
||||
rules?: string
|
||||
}
|
||||
interface TextareaHTMLAttributes<T> extends HTMLAttributes<T> {
|
||||
rules?: string
|
||||
}
|
||||
}
|
||||
@@ -1 +1,2 @@
|
||||
export * from './nostr'
|
||||
export * from './site'
|
||||
|
||||
@@ -0,0 +1,139 @@
|
||||
import { IGatsbyImageData } from 'gatsby-plugin-image'
|
||||
import * as React from 'react'
|
||||
import type { IconType } from 'react-icons'
|
||||
|
||||
export type PageType = 'index' | 'year' | 'category' | 'search'
|
||||
|
||||
export interface RecentPost {
|
||||
slug: string
|
||||
title: string
|
||||
}
|
||||
|
||||
export interface TagCount {
|
||||
name: string
|
||||
totalCount: number
|
||||
}
|
||||
|
||||
export interface PageContext {
|
||||
page: PageType
|
||||
archives: Record<string, number>
|
||||
recentPosts: RecentPost[]
|
||||
tags: TagCount[]
|
||||
filter?: unknown
|
||||
limit?: number
|
||||
skip?: number
|
||||
numPages?: number
|
||||
currentPage?: number
|
||||
year?: string
|
||||
tag?: string
|
||||
}
|
||||
|
||||
export interface PostFrontmatter {
|
||||
title: string
|
||||
date: string
|
||||
author?: string
|
||||
authorLink?: string
|
||||
description?: string
|
||||
tags?: string[]
|
||||
heading?: string
|
||||
desc?: string
|
||||
previewImg?: {
|
||||
childImageSharp?: {
|
||||
gatsbyImageData: IGatsbyImageData
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export interface PostNode {
|
||||
id?: string
|
||||
html: string
|
||||
fields: {
|
||||
slug: string
|
||||
}
|
||||
frontmatter: PostFrontmatter
|
||||
}
|
||||
|
||||
export interface LayoutProps {
|
||||
location: Location
|
||||
heroSection?: boolean
|
||||
heading?: string
|
||||
desc?: string
|
||||
children?: React.ReactNode
|
||||
}
|
||||
|
||||
export interface HeroSectionProps {
|
||||
location: Location
|
||||
heading?: string
|
||||
desc?: string
|
||||
}
|
||||
|
||||
export interface NavibarProps {
|
||||
location: Location
|
||||
}
|
||||
|
||||
export interface ContainerProps {
|
||||
children?: React.ReactNode
|
||||
}
|
||||
|
||||
export interface SectionProps {
|
||||
children?: React.ReactNode
|
||||
color?: string
|
||||
bgColor?: string
|
||||
bottomArrow?: boolean
|
||||
}
|
||||
|
||||
export interface SeoProps {
|
||||
description?: string
|
||||
lang?: string
|
||||
meta?: Array<{ name?: string; property?: string; content?: string }>
|
||||
title?: string
|
||||
previewImg?: string
|
||||
}
|
||||
|
||||
export interface SolidButtonProps {
|
||||
children: React.ReactNode
|
||||
theme?: 'light' | 'dark'
|
||||
type?: 'button' | 'submit' | 'reset'
|
||||
disabled?: boolean
|
||||
onClick?: React.MouseEventHandler<HTMLButtonElement>
|
||||
}
|
||||
|
||||
export interface OutlineButtonProps {
|
||||
children: React.ReactNode
|
||||
}
|
||||
|
||||
export interface FaqProps {
|
||||
question: string
|
||||
answer: string
|
||||
}
|
||||
|
||||
export interface FeatureProps {
|
||||
title: string
|
||||
desc: string
|
||||
Icon: IconType
|
||||
className?: string
|
||||
}
|
||||
|
||||
export interface ReasonProps {
|
||||
text: string
|
||||
bgColor?: string
|
||||
}
|
||||
|
||||
export interface ArtProps {
|
||||
src: string
|
||||
info?: string
|
||||
}
|
||||
|
||||
export interface StyledAnchorProps {
|
||||
href: string
|
||||
children?: React.ReactNode
|
||||
}
|
||||
|
||||
export type SearchPost = {
|
||||
id: string
|
||||
html: string
|
||||
slug: string
|
||||
title: string
|
||||
date: string
|
||||
previewImg?: PostFrontmatter['previewImg']
|
||||
}
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ESNext",
|
||||
"lib": ["dom", "dom.iterable", "esnext"],
|
||||
"allowJs": true,
|
||||
"skipLibCheck": true,
|
||||
"esModuleInterop": true,
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"strict": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"noEmit": true,
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "bundler",
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"jsx": "react-jsx"
|
||||
},
|
||||
"include": ["src/**/*", "gatsby-config.d.ts"],
|
||||
"exclude": ["node_modules", "public", ".cache"]
|
||||
}
|
||||
Reference in New Issue
Block a user