feat: feed feature, typescript, remove google, fix errs & warns
publish / Build-and-publish (push) Successful in 13m32s

This commit is contained in:
Unknown
2026-07-17 22:22:29 +01:00
parent 2c42ecae5f
commit 8a10d127c1
55 changed files with 1887 additions and 587 deletions
+1 -1
View File
@@ -13,7 +13,7 @@ jobs:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
- uses: actions/setup-node@v4 - uses: actions/setup-node@v4
with: with:
node-version: 20 node-version-file: '.nvmrc'
cache: 'npm' cache: 'npm'
- name: Install dependencies - name: Install dependencies
+2 -2
View File
@@ -11,10 +11,10 @@ jobs:
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
- name: Use Node.js 20 - name: Use Node.js from .nvmrc
uses: actions/setup-node@v4 uses: actions/setup-node@v4
with: with:
node-version: 20 node-version-file: .nvmrc
cache: npm cache: npm
- name: Install dependencies - name: Install dependencies
+1 -1
View File
@@ -1 +1 @@
v20.20.2 v24.18.0
+11
View File
@@ -30,3 +30,14 @@
- <img class="alignright" src="/wp-content/uploads/2021/04/2IrsV7v.png" alt="Title" width="352" height="442" /> - <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" /> - <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.
+21
View File
@@ -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!
+96
View File
@@ -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
View File
@@ -1,4 +1,5 @@
import "bootstrap/dist/css/bootstrap.min.css"; import "bootstrap/dist/css/bootstrap.min.css";
import "./src/styles/montserrat.css";
// Import all js dependencies. // Import all js dependencies.
import "jquery/dist/jquery.min.js"; import "jquery/dist/jquery.min.js";
+15
View File
@@ -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
View File
@@ -23,7 +23,6 @@ module.exports = {
name: `Data Controller | Flexible and Secure SAS® Data Modification`, name: `Data Controller | Flexible and Secure SAS® Data Modification`,
short_name: `Data Controller`, 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.`, 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: '/', start_url: '/',
background_color: '#fff', background_color: '#fff',
theme_color: '#314351', theme_color: '#314351',
@@ -51,6 +50,14 @@ module.exports = {
}, },
__key: 'blog' __key: 'blog'
}, },
{
resolve: `gatsby-source-filesystem`,
options: {
path: `./content/feed`,
name: `feed`
},
__key: 'feed'
},
{ {
resolve: `gatsby-source-filesystem`, resolve: `gatsby-source-filesystem`,
options: { options: {
@@ -103,13 +110,6 @@ module.exports = {
}, },
__key: 'pages' __key: 'pages'
}, },
{
resolve: `gatsby-plugin-google-fonts`,
options: {
fonts: [`Montserrat\:300,400,500`],
display: 'swap'
}
},
{ {
resolve: 'gatsby-plugin-local-search', resolve: 'gatsby-plugin-local-search',
options: { options: {
@@ -131,7 +131,7 @@ module.exports = {
// required. // required.
query: ` query: `
{ {
remark: allMarkdownRemark (filter: {fileAbsolutePath: {regex: "/content/blog/"}}) { remark: allMarkdownRemark (filter: {fileAbsolutePath: {regex: "/content\\/(blog|feed)\\//"}}) {
posts: edges { posts: edges {
post: node { post: node {
id id
@@ -184,6 +184,63 @@ module.exports = {
html: data.post.html 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
View File
@@ -6,6 +6,10 @@ const recentPosts = []
const archives = {} const archives = {}
const tagsFrequent = [] const tagsFrequent = []
const feedRecentPosts = []
const feedArchives = {}
const feedTagsFrequent = []
exports.createPages = async ({ graphql, actions, reporter }) => { exports.createPages = async ({ graphql, actions, reporter }) => {
const { createPage } = actions const { createPage } = actions
@@ -14,6 +18,10 @@ exports.createPages = async ({ graphql, actions, reporter }) => {
const blogListTemplate = path.resolve(`./src/templates/blog-list.tsx`) const blogListTemplate = path.resolve(`./src/templates/blog-list.tsx`)
const blogSearchTemplate = path.resolve(`./src/templates/blog-search.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 // Get all markdown blog posts sorted by date
const result = await graphql( 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 } }) { group(field: { frontmatter: { tags: SELECT } }) {
name: fieldValue name: fieldValue
totalCount totalCount
@@ -176,6 +187,166 @@ exports.createPages = async ({ graphql, actions, reporter }) => {
tags: tagsFrequent 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 }) => { exports.onCreatePage = ({ page, actions }) => {
+627 -217
View File
File diff suppressed because it is too large Load Diff
+28 -4
View File
@@ -14,16 +14,18 @@
"serve": "gatsby serve", "serve": "gatsby serve",
"clean": "gatsby clean", "clean": "gatsby clean",
"lint": "npx prettier --check \"src/**/*.+(ts|tsx|js|jsx|json|css|scss)\"", "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": { "dependencies": {
"@browniebroke/gatsby-image-gallery": "^8.2.0", "@browniebroke/gatsby-image-gallery": "^8.2.0",
"@mdx-js/mdx": "^3.1.1", "@mdx-js/mdx": "^3.1.1",
"@mdx-js/react": "^3.1.1", "@mdx-js/react": "^3.1.1",
"babel-plugin-styled-components": "^2.1.4", "babel-plugin-styled-components": "^2.1.4",
"baseline-browser-mapping": "^2.10.43",
"caniuse-lite": "^1.0.30001806",
"gatsby": "^5.15.0", "gatsby": "^5.15.0",
"gatsby-plugin-google-analytics": "^5.13.1", "gatsby-plugin-feed": "^5.16.0",
"gatsby-plugin-google-fonts": "^1.0.1",
"gatsby-plugin-image": "^3.15.0", "gatsby-plugin-image": "^3.15.0",
"gatsby-plugin-local-search": "^2.0.1", "gatsby-plugin-local-search": "^2.0.1",
"gatsby-plugin-manifest": "^5.15.0", "gatsby-plugin-manifest": "^5.15.0",
@@ -50,8 +52,30 @@
}, },
"devDependencies": { "devDependencies": {
"@popperjs/core": "^2.9.2", "@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", "bootstrap": "^5.0.0-beta3",
"cloudron-surfer": "^6.4.1", "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"
} }
} }
+24 -23
View File
@@ -1,7 +1,11 @@
import React from 'react' import React from 'react'
import { FaRss } from 'react-icons/fa'
import { Section } from '../shared' import { Section } from '../shared'
import { SolidButton } from '../shared/styledComponents' import { SolidButton } from '../shared/styledComponents'
import { StyledHeading, StyledDesc, InputStyled, StyledAnchor } from './style' import { StyledHeading, StyledDesc, InputStyled, StyledAnchor } from './style'
import { siteMetadata } from '../../../gatsby-config'
const rssFeedUrl = `${siteMetadata.siteUrl}rss.xml`
const anchorStyles = { const anchorStyles = {
color: '#888' color: '#888'
@@ -13,42 +17,39 @@ const Footer = () => (
<div className="col-md-3 me-md-5"> <div className="col-md-3 me-md-5">
<StyledHeading>Data Controller</StyledHeading> <StyledHeading>Data Controller</StyledHeading>
<StyledDesc> <StyledDesc>
Data Controller is the product of a UK company with a singular focus on{' '} Data Controller is the product of a UK company with a singular focus
<StyledAnchor on <StyledAnchor href="https://sasapps.io">SAS Web Apps</StyledAnchor>
href="https://sasapps.io" .
target="_blank"
rel="noopener"
>
SAS Web Apps
</StyledAnchor>.
</StyledDesc> </StyledDesc>
</div> </div>
<div className="col-md-3"> <div className="col-md-3">
<StyledHeading>Source Code</StyledHeading> <StyledHeading>Source Code</StyledHeading>
<StyledDesc> <StyledDesc>
All our source code can be found on our self-hosted{' '} Data Controller source is on our self-hosted{' '}
<StyledAnchor <StyledAnchor href="https://git.datacontroller.io">
href="https://git.datacontroller.io"
target="_blank"
rel="noopener"
>
Gitea Repository Gitea Repository
</StyledAnchor> </StyledAnchor>
. ; the underlying SASjs framework is on{' '}
<StyledAnchor href="https://github.com/sasjs">GitHub</StyledAnchor>.
</StyledDesc> </StyledDesc>
</div> </div>
<div className="col-md-3"> <div className="col-md-3">
<StyledHeading>Other Resources</StyledHeading> <StyledHeading>Other Resources</StyledHeading>
<StyledDesc> <StyledDesc>
Leverage our underlying tech stack on{' '} Connect on{' '}
<StyledAnchor <StyledAnchor href={siteMetadata.social.linkedin}>
href="https://github.com/sasjs" LinkedIn
target="_blank" </StyledAnchor>
rel="noopener" , read the{' '}
> <StyledAnchor href="https://docs.datacontroller.io">
Github docs
</StyledAnchor>
, or subscribe to the{' '}
<StyledAnchor href={rssFeedUrl}>
<FaRss style={{ marginRight: '6px', verticalAlign: 'middle' }} />
RSS feed
</StyledAnchor>{' '} </StyledAnchor>{' '}
and build your own SAS Powered Web Apps. for updates.
</StyledDesc> </StyledDesc>
</div> </div>
</div> </div>
@@ -1,6 +1,8 @@
import React from 'react' import * as React from 'react'
import styled from 'styled-components' import styled from 'styled-components'
import type { StyledAnchorProps } from '../../types'
export const StyledHeading = styled.h6` export const StyledHeading = styled.h6`
margin-bottom: 0.8rem; margin-bottom: 0.8rem;
text-transform: uppercase; 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"> <Anchor href={href} target="_blank" rel="noopener">
{children} {children}
</Anchor> </Anchor>
+4 -10
View File
@@ -1,21 +1,15 @@
import React from 'react' import React from 'react'
import { PageProps, Link } from 'gatsby' import { Link } from 'gatsby'
import styled from 'styled-components'
import { Hero, HeroHeading, HeroDesc } from './style' import { Hero, HeroHeading, HeroDesc } from './style'
import { BottomSectionArrow, OutlineButton } from '../shared/styledComponents' import { BottomSectionArrow, OutlineButton } from '../shared/styledComponents'
import { Container } from '../shared' import { Container } from '../shared'
import { pathPrefix } from '../../../gatsby-config.js' import { pathPrefix } from '../../../gatsby-config'
type DataProps = { import type { HeroSectionProps } from '../../types'
location: Location
heading: string
desc: string
}
const HeroSection: React.FC<PageProps<DataProps>> = ({ const HeroSection: React.FC<HeroSectionProps> = ({
location, location,
heading, heading,
desc desc
@@ -1,7 +1,7 @@
import styled from 'styled-components' import styled from 'styled-components'
import background from '../../images/home_hero_bg.png' import background from '../../images/home_hero_bg.png'
export const Hero = styled.main` export const Hero = styled.main<{ bg?: boolean }>`
position: relative; position: relative;
padding: 50px 0; padding: 50px 0;
color: white; color: white;
@@ -13,7 +13,8 @@ export const Hero = styled.main`
` `
export const HeroHeading = styled.h1` export const HeroHeading = styled.h1`
font-family: 'Montserrat', 'HelveticaNeue', 'Helvetica Neue', Helvetica, Arial, font-family:
'Montserrat', 'HelveticaNeue', 'Helvetica Neue', Helvetica, Arial,
sans-serif; sans-serif;
text-transform: uppercase; text-transform: uppercase;
` `
+2 -6
View File
@@ -1,16 +1,12 @@
import React, { useEffect } from 'react' import React, { useEffect } from 'react'
import { PageProps } from 'gatsby'
import Navibar from './navibar' import Navibar from './navibar'
import HeroSection from './herosection' import HeroSection from './herosection'
import Footer from './footer' import Footer from './footer'
type DataProps = { import type { LayoutProps } from '../types'
children?: React.ReactNode
heroSection: boolean
}
const Layout: React.FC<PageProps<DataProps>> = ({ const Layout: React.FC<LayoutProps> = ({
location, location,
children, children,
heroSection = true, heroSection = true,
+19 -38
View File
@@ -1,49 +1,34 @@
import React from 'react' import React from 'react'
import { Link, PageProps } from 'gatsby' import { Link } from 'gatsby'
import dcLogo from '../../images/dclogo.png' import dcLogo from '../../images/dclogo.png'
import { Container } from '../shared' import { Container } from '../shared'
import { logoStyles, CustomNavBar, ulStyles, Li, StyledLink } from './style' import { logoStyles, CustomNavBar, ulStyles, Li, StyledLink } from './style'
import { pathPrefix } from '../../../gatsby-config.js' import { pathPrefix } from '../../../gatsby-config'
const naviLinks = [ import type { NavibarProps } from '../../types'
{
name: 'Home', interface NaviLink {
url: '/', name: string
active: 'no' url: string
}, active: 'yes' | 'no'
{ }
name: 'About',
url: '/about/', const naviLinks: NaviLink[] = [
active: 'no' { name: 'Home', url: '/', active: 'no' },
}, { name: 'About', url: '/about/', active: 'no' },
{ { name: 'Blog', url: '/blog/', active: 'no' },
name: 'Blog', { name: 'Feed', url: '/feed/', active: 'no' },
url: '/blog/', { name: 'FAQ', url: '/faq/', active: 'no' },
active: 'no'
},
{
name: 'FAQ',
url: '/faq/',
active: 'no'
},
{ {
name: 'Documentation', name: 'Documentation',
url: 'https://docs.datacontroller.io/', url: 'https://docs.datacontroller.io/',
active: 'no' active: 'no'
}, },
{ { name: 'Pricing', url: '/pricing/', active: 'no' },
name: 'Pricing', { name: 'Book Demo', url: '/contact/', active: 'no' },
url: '/pricing/',
active: 'no'
},
{
name: 'Book Demo',
url: '/contact/',
active: 'no'
},
{ {
name: 'Source Code', name: 'Source Code',
url: 'https://git.datacontroller.io/dc/dc', url: 'https://git.datacontroller.io/dc/dc',
@@ -51,11 +36,7 @@ const naviLinks = [
} }
] ]
type DataProps = { const Navibar: React.FC<NavibarProps> = ({ location }) => {
location: Location
}
const Navibar: React.FC<PageProps<DataProps>> = ({ location }) => {
naviLinks.forEach((link) => (link.active = 'no')) naviLinks.forEach((link) => (link.active = 'no'))
const currentLink = naviLinks.find( const currentLink = naviLinks.find(
(link) => pathPrefix + link.url === location?.pathname (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 styled, { css } from 'styled-components'
import { Link } from 'gatsby' import { Link } from 'gatsby'
// styles
export const logoStyles = { export const logoStyles = {
height: '55px' height: '55px'
} }
@@ -295,7 +294,6 @@ const LinkUnderlineStyles = css`
transition: opacity 0.3s ease; transition: opacity 0.3s ease;
` `
// styled components
export const Li = styled.li` export const Li = styled.li`
position: relative; position: relative;
@media (min-width: 992px) { @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-right: 0.8rem !important;
padding-left: 0.8rem !important; padding-left: 0.8rem !important;
color: white !important; color: white !important;
+21 -22
View File
@@ -1,9 +1,16 @@
import * as React from 'react' import * as React from 'react'
import PropTypes from 'prop-types'
import { Helmet } from 'react-helmet' import { Helmet } from 'react-helmet'
import { useStaticQuery, graphql } from 'gatsby' 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` const { site } = useStaticQuery(graphql`
query { query {
site { site {
@@ -37,44 +44,36 @@ const Seo = ({ description, lang, meta, title, previewImg = undefined }) => {
lang lang
}} }}
title={pageTitle} 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: 'author', property: 'author', content: author },
{ {
name: 'description', name: 'description',
property: 'og:description', property: 'og:description',
content: metaDescription content: metaDescription
}, },
// { name: 'facebook:site', content: '', },
{ name: 'image', property: 'og:image', content: image }, { name: 'image', property: 'og:image', content: image },
{ {
name: `linkedin:site`, name: `linkedin:site`,
content: site.siteMetadata?.social?.linkedin || `` content: site.siteMetadata?.social?.linkedin || ``
}, },
{ name: `twitter:card`, content: `summary` }, { name: `twitter:card`, content: `summary` },
// { name: `twitter:creator`, content: site.siteMetadata?.social?.twitter || `` },
{ name: `twitter:description`, content: metaDescription }, { name: `twitter:description`, content: metaDescription },
// { name: 'twitter:site', content: `${site?.twitter}`, },
{ name: `twitter:title`, content: title }, { name: `twitter:title`, content: title },
// { name: 'youtube:site', content: `${site?.youtube}`, },
{ property: `og:title`, content: title }, { property: `og:title`, content: title },
{ property: `og:type`, content: `website` } { 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 export default Seo
+3 -6
View File
@@ -1,10 +1,7 @@
import React from 'react' import * as React from 'react'
import styled from 'styled-components' import styled from 'styled-components'
import { PageProps } from 'gatsby'
type DataProps = { import type { ContainerProps } from '../../types'
children?: React.ReactNode
}
const StyledDiv = styled.div` const StyledDiv = styled.div`
@media (min-width: 576px) { @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> return <StyledDiv className="container">{children}</StyledDiv>
} }
+1 -1
View File
@@ -4,7 +4,7 @@ import styled from 'styled-components'
import { FaEnvelope } from 'react-icons/fa' import { FaEnvelope } from 'react-icons/fa'
export const StyledLink = styled((props) => <Link {...props} />)` export const StyledLink = styled(Link)`
color: rgb(255, 255, 255); color: rgb(255, 255, 255);
background-color: rgb(144, 196, 69); background-color: rgb(144, 196, 69);
border-radius: 0px; border-radius: 0px;
+4 -10
View File
@@ -1,25 +1,19 @@
import React from 'react' import * as React from 'react'
import styled from 'styled-components' import styled from 'styled-components'
import { PageProps } from 'gatsby'
import { BottomSectionArrow } from './styledComponents' import { BottomSectionArrow } from './styledComponents'
import { Container } from './' import { Container } from './'
type DataProps = { import type { SectionProps } from '../../types'
children?: React.ReactNode
color?: string
bgColor?: string
bottomArrow?: boolean
}
const StyledSection = styled.div` const StyledSection = styled.div<{ color?: string; bgColor?: string }>`
position: relative; position: relative;
padding: 50px 0; padding: 50px 0;
color: ${(props) => props.color || 'white'}; color: ${(props) => props.color || 'white'};
background-color: ${(props) => props.bgColor || '#314351'}; background-color: ${(props) => props.bgColor || '#314351'};
` `
export const Section: React.FC<PageProps<DataProps>> = ({ export const Section: React.FC<SectionProps> = ({
children, children,
bgColor, bgColor,
color, color,
@@ -1,6 +1,8 @@
import React from 'react' import * as React from 'react'
import styled from 'styled-components' import styled from 'styled-components'
import type { SolidButtonProps, OutlineButtonProps } from '../../types'
const BottomArrow = styled.div` const BottomArrow = styled.div`
width: 50px; width: 50px;
height: 50px; height: 50px;
@@ -9,9 +11,6 @@ const BottomArrow = styled.div`
background: inherit; background: inherit;
transform: translateX(-50%) rotate(45deg); transform: translateX(-50%) rotate(45deg);
left: 50%; left: 50%;
// right: 0;
// margin-left: auto;
// margin-right: auto;
z-index: 10; z-index: 10;
` `
const BottomArrowWrapper = styled.div` const BottomArrowWrapper = styled.div`
@@ -29,16 +28,17 @@ export const BottomSectionArrow = () => (
</BottomArrowWrapper> </BottomArrowWrapper>
) )
export const SectionHeading = styled.h2` export const SectionHeading = styled.h2<{ center?: string }>`
text-align: ${(props) => (props.center === 'no' ? 'left' : 'center')}; text-align: ${(props) => (props.center === 'no' ? 'left' : 'center')};
letter-spacing: 1px; letter-spacing: 1px;
font-weight: 400; font-weight: 400;
font-family: 'Montserrat', 'HelveticaNeue', 'Helvetica Neue', Helvetica, Arial, font-family:
'Montserrat', 'HelveticaNeue', 'Helvetica Neue', Helvetica, Arial,
sans-serif; sans-serif;
text-transform: uppercase; 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')}; text-align: ${(props) => (props.center === 'no' ? 'left' : 'center')};
opacity: ${(props) => props.opacity ?? 0.6}; opacity: ${(props) => props.opacity ?? 0.6};
a { a {
@@ -59,12 +59,12 @@ const StyledSolidButton = styled.button`
} }
` `
export const SolidButton = ({ export const SolidButton: React.FC<SolidButtonProps> = ({
children, children,
theme = 'light', theme = 'light',
type = 'submit', type = 'submit',
disabled = false, disabled = false,
onClick = undefined onClick
}) => ( }) => (
<StyledSolidButton <StyledSolidButton
type={type} type={type}
@@ -83,7 +83,7 @@ const StyledOutlineButton = styled.button`
border-width: 2px; border-width: 2px;
` `
export const OutlineButton = ({ children }) => ( export const OutlineButton: React.FC<OutlineButtonProps> = ({ children }) => (
<StyledOutlineButton type="button" className="btn btn-outline-light"> <StyledOutlineButton type="button" className="btn btn-outline-light">
{children} {children}
</StyledOutlineButton> </StyledOutlineButton>
+6 -6
View File
@@ -34,7 +34,7 @@ export class NostrController extends EventEmitter {
/** /**
* Function will publish provided event to the provided relays * 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 simplePool = new SimplePool()
const promises = simplePool.publish(relays, event) const promises = simplePool.publish(relays, event)
@@ -47,7 +47,7 @@ export class NostrController extends EventEmitter {
}) })
if (publishedRelays.length === 0) { if (publishedRelays.length === 0) {
const failedPublishes: any[] = [] const failedPublishes: Array<{ relay: string; error: string }> = []
const fallbackRejectionReason = const fallbackRejectionReason =
'Attempt to publish an event has been rejected with unknown reason.' 'Attempt to publish an event has been rejected with unknown reason.'
@@ -92,7 +92,7 @@ export class NostrController extends EventEmitter {
return Promise.resolve(signedEvent) return Promise.resolve(signedEvent)
} }
nip04Encrypt = async (receiver: string, content: string) => { nip04Encrypt = async (receiver: string, content: string): Promise<string> => {
if (!this.generatedKeys) { if (!this.generatedKeys) {
throw new Error(`Private & public key pair is not found.`) 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 isSigner Boolean indicating whether the recipient is a signer or viewer.
* @param setAuthUrl Function to set the authentication URL in the component state. * @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 // Set up timeout promise to handle encryption timeout
const timeoutPromise = new Promise<never>((_, reject) => { const timeoutPromise = new Promise<never>((_, reject) => {
setTimeout(() => { setTimeout(() => {
@@ -159,7 +159,7 @@ export class NostrController extends EventEmitter {
* @param hexKey hex private or public key * @param hexKey hex private or public key
* @returns whether or not is key valid * @returns whether or not is key valid
*/ */
validateHex = (hexKey: string) => { validateHex = (hexKey: string): RegExpMatchArray | null => {
return hexKey.match(/^[a-f0-9]{64}$/) return hexKey.match(/^[a-f0-9]{64}$/)
} }
@@ -193,7 +193,7 @@ export class NostrController extends EventEmitter {
return { private: nip19.nsecEncode(nsec), public: getPublicKey(nsec) } return { private: nip19.nsecEncode(nsec), public: getPublicKey(nsec) }
} }
verifySignedEvent = (event: SignedEvent) => { verifySignedEvent = (event: SignedEvent): void => {
const isGood = verifyEvent(event) const isGood = verifyEvent(event)
if (!isGood) { if (!isGood) {
+8 -3
View File
@@ -1,5 +1,4 @@
import React from 'react' import * as React from 'react'
// import { Link } from 'gatsby'
import Layout from '../components/layout' import Layout from '../components/layout'
import Seo from '../components/seo' import Seo from '../components/seo'
@@ -7,7 +6,13 @@ import Seo from '../components/seo'
import { Section } from '../components/shared' import { Section } from '../components/shared'
import { SideBar } from '../templates/sidebar' 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}> <Layout location={location} heroSection={false}>
<Seo title="Page Not Found" /> <Seo title="Page Not Found" />
<Section color="black" bgColor="white" bottomArrow={false}> <Section color="black" bgColor="white" bottomArrow={false}>
+3 -13
View File
@@ -1,5 +1,5 @@
import { PageProps, Link, graphql } from 'gatsby' import { PageProps, Link } from 'gatsby'
import React from 'react' import * as React from 'react'
import Layout from '../components/layout' import Layout from '../components/layout'
import Seo from '../components/seo' import Seo from '../components/seo'
@@ -10,17 +10,7 @@ import {
SectionDesc SectionDesc
} from '../components/shared/styledComponents' } from '../components/shared/styledComponents'
type DataProps = { const About: React.FC<PageProps<unknown>> = ({ location }) => {
site: {
meta: {
title: string
description: string
social: { linkedin: string }
}
}
}
const About: React.FC<PageProps<DataProps>> = ({ data, location }) => {
return ( return (
<Layout <Layout
location={location} location={location}
+11 -21
View File
@@ -1,5 +1,5 @@
import { PageProps, Link, graphql } from 'gatsby' import { PageProps } from 'gatsby'
import React, { useState } from 'react' import * as React from 'react'
import Layout from '../components/layout' import Layout from '../components/layout'
import Seo from '../components/seo' import Seo from '../components/seo'
@@ -22,25 +22,15 @@ import contactBg from '../images/contact_bg.jpg'
import '../styledComponents/contact.css' import '../styledComponents/contact.css'
import { NostrController } from '../controllers' import { NostrController } from '../controllers'
type DataProps = { const Contact: React.FC<PageProps<unknown>> = ({ location }) => {
site: {
meta: {
title: string
description: string
social: { linkedin: string }
}
}
}
const Contact: React.FC<PageProps<DataProps>> = ({ data, location }) => {
const nostrController = NostrController.getInstance() const nostrController = NostrController.getInstance()
const [name, setName] = useState<string>() const [name, setName] = React.useState<string>()
const [email, setEmail] = useState<string>() const [email, setEmail] = React.useState<string>()
const [emailError, setEmailError] = useState<boolean>(false) const [emailError, setEmailError] = React.useState<boolean>(false)
const [subject, setSubject] = useState<string>() const [subject, setSubject] = React.useState<string>()
const [message, setMessage] = useState<string>() const [message, setMessage] = React.useState<string>()
const [notification, setNotification] = useState<string>() const [notification, setNotification] = React.useState<string>()
const handleEmailChange = (e: React.ChangeEvent<HTMLInputElement>) => { const handleEmailChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const value = e.target.value const value = e.target.value
@@ -98,7 +88,7 @@ Email: ${email}
Subject: ${subject} Subject: ${subject}
Message: ${message}` Message: ${message}`
) )
.catch((err) => { .catch((err: unknown) => {
setNotification( setNotification(
`Something went wrong. Please check the console for more information. Please try one more time.` `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" className="form-control contactFormStyles"
id="mesage" id="mesage"
name="message" name="message"
rows="5" rows={5}
rules="required|max:200" rules="required|max:200"
onChange={(evt) => { onChange={(evt) => {
setMessage(evt.target.value) setMessage(evt.target.value)
+3 -13
View File
@@ -1,5 +1,5 @@
import { PageProps, Link, graphql } from 'gatsby' import { PageProps, Link } from 'gatsby'
import React from 'react' import * as React from 'react'
import Layout from '../components/layout' import Layout from '../components/layout'
import Seo from '../components/seo' import Seo from '../components/seo'
@@ -13,17 +13,7 @@ import {
import { FAQ } from '../styledComponents/faq' import { FAQ } from '../styledComponents/faq'
type DataProps = { const Faq: React.FC<PageProps<unknown>> = ({ location }) => {
site: {
meta: {
title: string
description: string
social: { linkedin: string }
}
}
}
const Faq: React.FC<PageProps<DataProps>> = ({ data, location }) => {
return ( return (
<Layout <Layout
location={location} location={location}
+15 -23
View File
@@ -1,5 +1,5 @@
import { Link, graphql } from 'gatsby' import * as React from 'react'
import React from 'react' import { Link, graphql, PageProps } from 'gatsby'
import Gallery from '@browniebroke/gatsby-image-gallery' import Gallery from '@browniebroke/gatsby-image-gallery'
import { IGatsbyImageData } from 'gatsby-plugin-image' import { IGatsbyImageData } from 'gatsby-plugin-image'
@@ -34,31 +34,31 @@ import dcDesign from '../images/data-controller-design.png'
interface ImageSharpEdge { interface ImageSharpEdge {
node: { node: {
name: string
childImageSharp: { childImageSharp: {
thumb: IGatsbyImageData thumb: IGatsbyImageData
full: IGatsbyImageData full: IGatsbyImageData
meta: {
originalName: string
}
} }
} }
} }
interface PageProps { interface IndexPageData {
data: {
images: { images: {
edges: ImageSharpEdge[] 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 }) => ({ const images = data.images.edges.map(({ node }) => ({
...node.childImageSharp, ...node.childImageSharp,
// Use original name as caption. caption: node.name
// The `originalName` is queried in a nested field,
// but the `Gallery` component expects `caption` at the top level.
caption: node.childImageSharp.meta.originalName
})) }))
return ( return (
@@ -231,13 +231,7 @@ const Home: React.FC<PageProps> = ({ data, location }) => {
</Section> </Section>
<Section color="black" bgColor="white"> <Section color="black" bgColor="white">
<SectionHeading>See How it Looks</SectionHeading> <SectionHeading>See How it Looks</SectionHeading>
<Gallery <Gallery images={images} gutter="0" customWrapper={CustomWrapper} />
images={images}
gutter={0}
customWrapper={({ children, onClick }) => (
<ThumbnailWrapper onClick={onClick}>{children}</ThumbnailWrapper>
)}
/>
</Section> </Section>
<ScheduleDemo /> <ScheduleDemo />
</Layout> </Layout>
@@ -254,6 +248,7 @@ export const pageQuery = graphql`
) { ) {
edges { edges {
node { node {
name
childImageSharp { childImageSharp {
thumb: gatsbyImageData( thumb: gatsbyImageData(
width: 300 width: 300
@@ -261,9 +256,6 @@ export const pageQuery = graphql`
placeholder: BLURRED placeholder: BLURRED
) )
full: gatsbyImageData(layout: FULL_WIDTH) full: gatsbyImageData(layout: FULL_WIDTH)
meta: fixed {
originalName
}
} }
} }
} }
@@ -1,5 +1,5 @@
import React from 'react' import * as React from 'react'
import { graphql } from 'gatsby' import { graphql, PageProps } from 'gatsby'
import styled from 'styled-components' import styled from 'styled-components'
import Layout from '../components/layout' import Layout from '../components/layout'
@@ -7,6 +7,8 @@ import Seo from '../components/seo'
import { Section } from '../components/shared' import { Section } from '../components/shared'
import type { PostNode, PageContext } from '../types'
const StyledMarkdown = styled.div` const StyledMarkdown = styled.div`
color: rgb(102, 102, 102); color: rgb(102, 102, 102);
h2, h2,
@@ -18,13 +20,20 @@ const StyledMarkdown = styled.div`
h2 { h2 {
text-transform: uppercase; text-transform: uppercase;
letter-spacing: 1px; letter-spacing: 1px;
font-family: 'Montserrat', 'HelveticaNeue', 'Helvetica Neue', Helvetica, font-family:
Arial, sans-serif; 'Montserrat', 'HelveticaNeue', 'Helvetica Neue', Helvetica, Arial,
sans-serif;
} }
` `
export default function Template({ data, location }) { interface MarkdownPageData {
const { frontmatter, html } = data?.markdownRemark markdownRemark: PostNode
}
const MarkdownPageTemplate: React.FC<
PageProps<MarkdownPageData, PageContext>
> = ({ data, location }) => {
const { frontmatter, html } = data.markdownRemark
return ( return (
<Layout <Layout
location={location} location={location}
@@ -39,6 +48,8 @@ export default function Template({ data, location }) {
) )
} }
export default MarkdownPageTemplate
export const pageQuery = graphql` export const pageQuery = graphql`
query ($id: String!) { query ($id: String!) {
markdownRemark(id: { eq: $id }) { markdownRemark(id: { eq: $id }) {
@@ -1,7 +1,8 @@
import styled from 'styled-components' import styled from 'styled-components'
export const StyledHeading = styled.h5` export const StyledHeading = styled.h5`
font-family: 'Montserrat', 'HelveticaNeue', 'Helvetica Neue', Helvetica, Arial, font-family:
'Montserrat', 'HelveticaNeue', 'Helvetica Neue', Helvetica, Arial,
sans-serif; sans-serif;
text-transform: uppercase; text-transform: uppercase;
letter-spacing: 1px; letter-spacing: 1px;
-17
View File
@@ -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%;
`
+20
View File
@@ -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 styled from 'styled-components'
import { FcFaq } from 'react-icons/fc' import { FcFaq } from 'react-icons/fc'
import type { FaqProps } from '../types'
const StyledFaq = styled.div` const StyledFaq = styled.div`
margin-bottom: 20px; margin-bottom: 20px;
` `
const StyledQuestion = styled.h5` const StyledQuestion = styled.h5`
color: #222; color: #222;
font-family: 'Montserrat', 'HelveticaNeue', 'Helvetica Neue', Helvetica, Arial, font-family:
'Montserrat', 'HelveticaNeue', 'Helvetica Neue', Helvetica, Arial,
sans-serif; sans-serif;
text-transform: uppercase; text-transform: uppercase;
` `
@@ -33,7 +36,7 @@ const FaqDetails = styled.div`
margin-bottom: 20px; margin-bottom: 20px;
` `
export const FAQ = ({ question, answer }) => ( export const FAQ: React.FC<FaqProps> = ({ question, answer }) => (
<StyledFaq> <StyledFaq>
<IconHolderStyled> <IconHolderStyled>
<FcFaq size={40} /> <FcFaq size={40} />
@@ -1,6 +1,9 @@
import React from 'react' import * as React from 'react'
import styled from 'styled-components' import styled from 'styled-components'
import { ImArrowRight } from 'react-icons/im' import { ImArrowRight } from 'react-icons/im'
import type { IconType } from 'react-icons'
import type { ArtProps, ReasonProps, FeatureProps } from '../types'
export const CenteredText = styled.p` export const CenteredText = styled.p`
text-align: center; text-align: center;
@@ -9,20 +12,19 @@ export const CenteredText = styled.p`
margin: 0 auto; margin: 0 auto;
font-size: 1.2rem; font-size: 1.2rem;
line-height: 1; line-height: 1;
font-family: 'Montserrat', 'HelveticaNeue', 'Helvetica Neue', Helvetica, Arial, font-family:
'Montserrat', 'HelveticaNeue', 'Helvetica Neue', Helvetica, Arial,
sans-serif; sans-serif;
` `
export const Art = styled.img.attrs((props) => ({ export const Art = styled.img.attrs<ArtProps>((props) => ({
alt: props.info || '', alt: props.info || '',
title: props.info || '' title: props.info || ''
}))` }))`
max-width: 100%; max-width: 100%;
display: block; height: auto;
margin: 0 auto;
padding: 20px;
` `
const ArrowHolder = styled.div` const ArrowHolder = styled.div<{ bgColor?: string }>`
display: inline-block; display: inline-block;
border-radius: 50px; border-radius: 50px;
padding: 15px 17px; padding: 15px 17px;
@@ -30,7 +32,7 @@ const ArrowHolder = styled.div`
background-color: ${(props) => background-color: ${(props) =>
props.bgColor === 'red' ? '#ed1c1c' : '#90c445'}; props.bgColor === 'red' ? '#ed1c1c' : '#90c445'};
` `
const RightArrow = ({ bgColor = '' }) => ( const RightArrow: React.FC<{ bgColor?: string }> = ({ bgColor = '' }) => (
<ArrowHolder bgColor={bgColor}> <ArrowHolder bgColor={bgColor}>
<ImArrowRight size={24} /> <ImArrowRight size={24} />
</ArrowHolder> </ArrowHolder>
@@ -46,7 +48,7 @@ const ReasonDesc = styled.p`
text-transform: uppercase; text-transform: uppercase;
` `
export const Reason = ({ text, bgColor = '' }) => ( export const Reason: React.FC<ReasonProps> = ({ text, bgColor = '' }) => (
<> <>
<RightArrow bgColor={bgColor} /> <RightArrow bgColor={bgColor} />
<ReasonDesc>{text}</ReasonDesc> <ReasonDesc>{text}</ReasonDesc>
@@ -62,7 +64,7 @@ const IconHolderStyled = styled.div`
vertical-align: top; vertical-align: top;
` `
const IconHolder = ({ Icon }) => { const IconHolder = ({ Icon }: { Icon: IconType }) => {
return ( return (
<IconHolderStyled> <IconHolderStyled>
<Icon size={24} /> <Icon size={24} />
@@ -77,7 +79,8 @@ const FeatureDetails = styled.div`
margin-bottom: 20px; margin-bottom: 20px;
` `
const FeatureTitle = styled.h5` const FeatureTitle = styled.h5`
font-family: 'Montserrat', 'HelveticaNeue', 'Helvetica Neue', Helvetica, Arial, font-family:
'Montserrat', 'HelveticaNeue', 'Helvetica Neue', Helvetica, Arial,
sans-serif; sans-serif;
text-transform: uppercase; text-transform: uppercase;
` `
@@ -86,7 +89,12 @@ const FeatureDesc = styled.p`
font-size: 0.9rem; 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'}> <div className={className ?? 'col-md-4'}>
<IconHolder Icon={Icon} /> <IconHolder Icon={Icon} />
<FeatureDetails> <FeatureDetails>
+35
View File
@@ -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
View File
@@ -1,5 +1,5 @@
import { Link, graphql } from 'gatsby' import { Link, graphql, PageProps } from 'gatsby'
import React from 'react' import * as React from 'react'
import kebabCase from 'lodash/kebabCase' import kebabCase from 'lodash/kebabCase'
import styled from 'styled-components' import styled from 'styled-components'
@@ -16,27 +16,39 @@ import {
import Post from './postpreview' import Post from './postpreview'
import { SideBar } from './sidebar' 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 posts = data.remark.posts
const iniPath = const iniPath: string =
pageContext.page == 'index' pageContext.page === 'index'
? `/blog/` ? `/blog/`
: pageContext.page == 'year' : pageContext.page === 'year'
? `/${pageContext.year}/` ? `/${pageContext.year}/`
: pageContext.page == 'category' : pageContext.page === 'category'
? `/category/${kebabCase(pageContext.tag)}/` ? `/category/${kebabCase(pageContext.tag ?? '')}/`
: null : `/blog/`
const pageInfo = `Page ${pageContext.currentPage} of ${pageContext.numPages}` const pageInfo = `Page ${pageContext.currentPage} of ${pageContext.numPages}`
Array.from({ length: 5 }, (v, k) => k + 1)
const paginationJSX = Array.from( const paginationJSX = Array.from(
{ length: pageContext.numPages }, { length: pageContext.numPages ?? 0 },
(_, i) => i + 1 (_, i) => i + 1
).map((pageIndex) => { ).map((pageIndex) => {
const link = pageIndex === 1 ? iniPath : `${iniPath}page/${pageIndex}` const link = pageIndex === 1 ? iniPath : `${iniPath}page/${pageIndex}`
return ( return (
<Link <Link
key={pageIndex}
to={link} to={link}
className={`btn btn-outline-dark btn-sm ${ className={`btn btn-outline-dark btn-sm ${
pageIndex === pageContext.currentPage ? 'disabled' : '' pageIndex === pageContext.currentPage ? 'disabled' : ''
@@ -62,8 +74,8 @@ const BlogListTemplate = ({ data, location, pageContext }) => {
<div className="row"> <div className="row">
<div className="col-md-7"> <div className="col-md-7">
<div className="row"> <div className="row">
{posts.map((data, i) => ( {posts.map(({ post }, i) => (
<Post key={i} post={data.post} /> <Post key={i} post={post} />
))} ))}
</div> </div>
<span className="float-start">{paginationJSX}</span> <span className="float-start">{paginationJSX}</span>
@@ -114,5 +126,3 @@ export const pageQuery = graphql`
} }
} }
` `
// tags
+9 -5
View File
@@ -1,5 +1,5 @@
import React from 'react' import * as React from 'react'
import { Link, graphql } from 'gatsby' import { graphql, PageProps } from 'gatsby'
import Layout from '../components/layout' import Layout from '../components/layout'
import Seo from '../components/seo' import Seo from '../components/seo'
@@ -8,15 +8,19 @@ import { Section } from '../components/shared'
import Post from './post' import Post from './post'
import { SideBar } from './sidebar' 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 { post } = data
const { previewImg } = post.frontmatter const { previewImg } = post.frontmatter
return ( return (
<Layout location={location} heroSection={false}> <Layout location={location} heroSection={false}>
<Seo <Seo
title={post?.frontmatter?.title} title={post.frontmatter.title}
description={post?.frontmatter?.description} description={post.frontmatter.description}
previewImg={ previewImg={
previewImg?.childImageSharp?.gatsbyImageData?.images?.fallback?.src previewImg?.childImageSharp?.gatsbyImageData?.images?.fallback?.src
} }
+19 -9
View File
@@ -1,8 +1,6 @@
import { Link, graphql, useStaticQuery } from 'gatsby' import { Link, graphql, useStaticQuery, PageProps } from 'gatsby'
import React from 'react' import * as React from 'react'
import kebabCase from 'lodash/kebabCase'
import { useFlexSearch } from 'react-use-flexsearch' import { useFlexSearch } from 'react-use-flexsearch'
import styled from 'styled-components'
import Layout from '../components/layout' import Layout from '../components/layout'
import Seo from '../components/seo' import Seo from '../components/seo'
@@ -17,7 +15,19 @@ import {
import Post from './postpreview' import Post from './postpreview'
import { SideBar } from './sidebar' 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` const queryData = useStaticQuery(graphql`
query { query {
localSearchBlog { localSearchBlog {
@@ -34,12 +44,12 @@ const BlogSearchTemplate = ({ location, pageContext }) => {
query, query,
queryData.localSearchBlog.index, queryData.localSearchBlog.index,
queryData.localSearchBlog.store queryData.localSearchBlog.store
) ) as SearchPost[]
const postsJSX = [] const postsJSX: React.ReactElement[] = []
if (query) { if (query) {
posts.forEach((post, i) => { posts.forEach((post, i) => {
const _post = { const _post: PostNode = {
id: post.id, id: post.id,
html: post.html, html: post.html,
fields: { fields: {
@@ -48,7 +58,7 @@ const BlogSearchTemplate = ({ location, pageContext }) => {
frontmatter: { frontmatter: {
title: post.title, title: post.title,
date: post.date, date: post.date,
previewImg: post.previewImg previewImg: post.previewImg as PostNode['frontmatter']['previewImg']
} }
} }
+131
View File
@@ -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 Controllers 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)
}
}
}
}
}
}
}
`
+71
View File
@@ -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
}
}
}
`
+20 -7
View File
@@ -1,12 +1,14 @@
import React from 'react' import * as React from 'react'
import { Link } from 'gatsby' import { Link } from 'gatsby'
import kebabCase from 'lodash/kebabCase' import kebabCase from 'lodash/kebabCase'
import { GatsbyImage } from 'gatsby-plugin-image' import { GatsbyImage, IGatsbyImageData } from 'gatsby-plugin-image'
import styled from 'styled-components' 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; text-decoration: none;
color: black; color: black;
&:hover { &:hover {
@@ -16,7 +18,8 @@ export const StyledLink = styled((props) => <Link {...props} />)`
` `
const StyledTitle = styled.h5` const StyledTitle = styled.h5`
font-family: 'Montserrat', 'HelveticaNeue', 'Helvetica Neue', Helvetica, Arial, font-family:
'Montserrat', 'HelveticaNeue', 'Helvetica Neue', Helvetica, Arial,
sans-serif; sans-serif;
margin-bottom: 0; margin-bottom: 0;
` `
@@ -45,7 +48,8 @@ const StyledContent = styled.div`
padding: 15px 0; padding: 15px 0;
color: #666666; color: #666666;
font-family: 'Montserrat', 'HelveticaNeue', 'Helvetica Neue', Helvetica, Arial, font-family:
'Montserrat', 'HelveticaNeue', 'Helvetica Neue', Helvetica, Arial,
sans-serif; sans-serif;
a { a {
color: black; 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) => ( const tagsJSX = (post.frontmatter?.tags || []).map((tag, index) => (
<span key={index}> <span key={index}>
{index > 0 && ', '} {index > 0 && ', '}
@@ -134,14 +143,18 @@ const Post = ({ post, location }) => {
) : ( ) : (
<span>{post.frontmatter.author}</span> <span>{post.frontmatter.author}</span>
) )
const previewImgData = post.frontmatter.previewImg?.childImageSharp
?.gatsbyImageData as IGatsbyImageData | undefined
return ( return (
<div> <div>
{previewImgData && (
<GatsbyImage <GatsbyImage
image={post.frontmatter.previewImg.childImageSharp.gatsbyImageData} image={previewImgData}
style={{ width: '100%', marginBottom: '1rem' }} style={{ width: '100%', marginBottom: '1rem' }}
imgStyle={{ objectFit: 'contain' }} imgStyle={{ objectFit: 'contain' }}
alt={post.frontmatter.title} alt={post.frontmatter.title}
/> />
)}
<StyledTitle>{post.frontmatter.title}</StyledTitle> <StyledTitle>{post.frontmatter.title}</StyledTitle>
<StyledDate> <StyledDate>
+20 -7
View File
@@ -1,13 +1,15 @@
import React from 'react' import * as React from 'react'
import { Link } from 'gatsby' import { Link } from 'gatsby'
import { GatsbyImage } from 'gatsby-plugin-image' import { GatsbyImage, IGatsbyImageData } from 'gatsby-plugin-image'
import styled from 'styled-components' import styled from 'styled-components'
import getDescription from '../shared/getDescription' 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; text-decoration: none;
color: black; color: black;
&:hover { &:hover {
@@ -17,7 +19,8 @@ export const StyledLink = styled((props) => <Link {...props} />)`
` `
const StyledTitle = styled.h5` const StyledTitle = styled.h5`
font-family: 'Montserrat', 'HelveticaNeue', 'Helvetica Neue', Helvetica, Arial, font-family:
'Montserrat', 'HelveticaNeue', 'Helvetica Neue', Helvetica, Arial,
sans-serif; sans-serif;
font-size: 1rem; font-size: 1rem;
margin-bottom: 0; margin-bottom: 0;
@@ -34,15 +37,24 @@ const StyledDesc = styled.p`
margin-bottom: 100px; 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"> <div className="col-md-6">
<StyledLink to={post.fields.slug}> <StyledLink to={post.fields.slug}>
{previewImgData && (
<GatsbyImage <GatsbyImage
image={post.frontmatter.previewImg.childImageSharp.gatsbyImageData} image={previewImgData}
style={{ minHeight: '150px', maxHeight: '200px' }} style={{ minHeight: '150px', maxHeight: '200px' }}
imgStyle={{ objectFit: 'contain' }} imgStyle={{ objectFit: 'contain' }}
alt={post.frontmatter.title} alt={post.frontmatter.title}
/> />
)}
<StyledTitle>{post.frontmatter.title}</StyledTitle> <StyledTitle>{post.frontmatter.title}</StyledTitle>
<StyledDate>{post.frontmatter.date}</StyledDate> <StyledDate>{post.frontmatter.date}</StyledDate>
@@ -50,5 +62,6 @@ const Post = ({ post }) => (
</StyledLink> </StyledLink>
</div> </div>
) )
}
export default Post export default PostPreview
+1 -1
View File
@@ -7,7 +7,7 @@ const extractContent = (s: string): string => {
return s return s
} }
const getDescription = (content: string): string => { const getDescription = (content: string): string => {
return extractContent(content).substr(0, 100) + '...' return extractContent(content).slice(0, 100) + '...'
} }
export default getDescription export default getDescription
+66 -20
View File
@@ -1,12 +1,17 @@
import React, { useState, useEffect } from 'react' import * as React from 'react'
import { Link, navigate } from 'gatsby' import { Link, navigate } from 'gatsby'
import kebabCase from 'lodash/kebabCase' import kebabCase from 'lodash/kebabCase'
import styled, { css } from 'styled-components' 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 { StyledHeading } from '../styledComponents/blog'
import { SolidButton } from '../components/shared/styledComponents' import { SolidButton } from '../components/shared/styledComponents'
import type { PageContext, RecentPost, TagCount } from '../types'
const rssFeedUrl = `${siteMetadata.siteUrl}rss.xml`
const styles = css` const styles = css`
color: #314351; color: #314351;
opacity: 0.8; opacity: 0.8;
@@ -29,10 +34,16 @@ const linkStyles = css`
text-decoration: underline; text-decoration: underline;
} }
` `
const ArchiveLink = styled((props) => <Link {...props} />)` const ArchiveLink = styled(Link)`
${linkStyles} ${linkStyles}
` `
const RecentPostLink = styled((props) => <Link {...props} />)` const RssAnchor = styled.a`
${linkStyles}
display: flex;
align-items: center;
gap: 6px;
`
const RecentPostLink = styled(Link)`
${linkStyles} ${linkStyles}
padding: 3px; padding: 3px;
border-bottom: 3px solid #e1e1e1; border-bottom: 3px solid #e1e1e1;
@@ -59,30 +70,44 @@ const StyledPara = styled.p`
${styles} ${styles}
` `
const Archives = ({ archives }) => ( interface ArchivesProps {
archives: Record<string, number>
basePath?: string
}
const Archives: React.FC<ArchivesProps> = ({ archives, basePath = '' }) => (
<> <>
{Object.keys(archives) {Object.keys(archives)
.sort() .sort()
.reverse() .reverse()
.map((year, index) => ( .map((year) => (
<ArchiveLink key={year} to={`/${year}/`}> <ArchiveLink key={year} to={`${basePath}/${year}/`}>
{year} ({archives[year]}) {year} ({archives[year]})
</ArchiveLink> </ArchiveLink>
))} ))}
</> </>
) )
const Categories = ({ tags }) => ( interface CategoriesProps {
tags: TagCount[]
basePath?: string
}
const Categories: React.FC<CategoriesProps> = ({ tags, basePath = '' }) => (
<> <>
{tags.map((tag, i) => ( {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}) {tag.name} ({tag.totalCount})
</ArchiveLink> </ArchiveLink>
))} ))}
</> </>
) )
const RecentPosts = ({ posts }) => ( interface RecentPostsProps {
posts: RecentPost[]
}
const RecentPosts: React.FC<RecentPostsProps> = ({ posts }) => (
<> <>
{posts.map((post) => ( {posts.map((post) => (
<RecentPostLink key={post.slug} to={post.slug}> <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 params = new URLSearchParams(location.search.substring(1))
const queryUrl = params?.get('s') || '' const queryUrl = params?.get('s') || ''
const [query, setQuery] = useState(queryUrl) const [query, setQuery] = React.useState(queryUrl)
useEffect(() => { React.useEffect(() => {
setQuery(queryUrl) setQuery(queryUrl)
}, [queryUrl]) }, [queryUrl])
const handleSubmit = (event) => { const handleSubmit = (event: React.FormEvent<HTMLFormElement>) => {
event.preventDefault() event.preventDefault()
navigate('/search?s=' + query) navigate('/search?s=' + query)
} }
const _handleKeyDown = (event) => { const _handleKeyDown = (event: React.KeyboardEvent<HTMLInputElement>) => {
if (event.key === 'Enter') { if (event.key === 'Enter') {
handleSubmit(event) navigate('/search?s=' + query)
} }
} }
@@ -130,13 +167,16 @@ export const SideBar = ({ pageContext, location, notFoundPage = false }) => {
type="text" type="text"
value={query} value={query}
onChange={(e) => setQuery(e.target.value)} onChange={(e) => setQuery(e.target.value)}
onKeyDown={(e) => _handleKeyDown(e)} onKeyDown={_handleKeyDown}
className="form-control" className="form-control"
placeholder="Search" placeholder="Search"
aria-label="Search" aria-label="Search"
aria-describedby="button-addon2" aria-describedby="button-addon2"
/> />
<SolidButton theme="dark" onClick={handleSubmit}> <SolidButton
theme="dark"
onClick={() => navigate('/search?s=' + query)}
>
Search Search
</SolidButton> </SolidButton>
<form onSubmit={handleSubmit}></form> <form onSubmit={handleSubmit}></form>
@@ -168,11 +208,17 @@ export const SideBar = ({ pageContext, location, notFoundPage = false }) => {
<> <>
<SideBarSection> <SideBarSection>
<StyledHeading>Archives</StyledHeading> <StyledHeading>Archives</StyledHeading>
<Archives archives={pageContext.archives} /> <Archives archives={pageContext.archives} basePath={basePath} />
</SideBarSection> </SideBarSection>
<SideBarSection> <SideBarSection>
<StyledHeading>Categories</StyledHeading> <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> </SideBarSection>
</> </>
)} )}
+25
View File
@@ -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
}
+10
View File
@@ -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
View File
@@ -1 +1,2 @@
export * from './nostr' export * from './nostr'
export * from './site'
+139
View File
@@ -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.
+20
View File
@@ -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"]
}