init
75
src/components/footer/index.tsx
Normal file
@ -0,0 +1,75 @@
|
||||
import React from 'react'
|
||||
import { Link } from 'gatsby'
|
||||
|
||||
// import dcLogo from "../../images/dclogo.png";
|
||||
|
||||
import { Container, Section } from '../shared'
|
||||
import { SolidButton } from '../shared/styledComponents'
|
||||
import { StyledHeading, StyledDesc, InputStyled, StyledAnchor } from './style'
|
||||
|
||||
const anchorStyles = {
|
||||
color: '#888'
|
||||
}
|
||||
|
||||
const Footer = () => (
|
||||
<Section bottomArrow={false}>
|
||||
<div className="row">
|
||||
<div className="col-md-3 me-md-5">
|
||||
<StyledHeading>Data Controller</StyledHeading>
|
||||
<StyledDesc>
|
||||
Data Controller is a product of 4GL Apps, a brand of Bowe IO Ltd,
|
||||
which is a UK company with a focus on SAS Software,{' '}
|
||||
<StyledAnchor href="https://sasapps.io">Apps</StyledAnchor>, and
|
||||
Services.
|
||||
</StyledDesc>
|
||||
</div>
|
||||
<div className="col-md-3">
|
||||
<StyledHeading>Newsletter</StyledHeading>
|
||||
<form
|
||||
className="kwes-form"
|
||||
method="POST"
|
||||
action="https://kwes.io/api/foreign/forms/mxKuyK4lxZWnG2WNH3ga"
|
||||
>
|
||||
<div className="mb-3">
|
||||
<InputStyled
|
||||
type="email"
|
||||
name="email"
|
||||
className="form-control"
|
||||
aria-describedby="emailHelp"
|
||||
placeholder="Email Address*"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<div className="mb-3">
|
||||
<InputStyled
|
||||
type="text"
|
||||
name="name"
|
||||
className="form-control"
|
||||
placeholder="First Name"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<div className="mb-3">
|
||||
<InputStyled
|
||||
type="text"
|
||||
name="lastName"
|
||||
className="form-control"
|
||||
placeholder="Last Name"
|
||||
/>
|
||||
</div>
|
||||
<SolidButton>Subscribe</SolidButton>
|
||||
</form>
|
||||
</div>
|
||||
<div className="col-md-3">
|
||||
<StyledHeading>Other Resources</StyledHeading>
|
||||
<StyledDesc>
|
||||
Visit our educational and fun SAS® software quiz{' '}
|
||||
<StyledAnchor href="https://sasensei.com">Sasensei</StyledAnchor> and
|
||||
test your knowledge of SAS topics.
|
||||
</StyledDesc>
|
||||
</div>
|
||||
</div>
|
||||
</Section>
|
||||
)
|
||||
|
||||
export default Footer
|
40
src/components/footer/style.js
Normal file
@ -0,0 +1,40 @@
|
||||
import React from 'react'
|
||||
import styled from 'styled-components'
|
||||
|
||||
export const StyledHeading = styled.h6`
|
||||
margin-bottom: 0.8rem;
|
||||
text-transform: uppercase;
|
||||
color: #888;
|
||||
`
|
||||
export const StyledDesc = styled.p`
|
||||
color: #aaaaaa;
|
||||
font-size: 0.9rem;
|
||||
`
|
||||
|
||||
export const InputStyled = styled.input`
|
||||
background: transparent;
|
||||
border: none;
|
||||
outline: none;
|
||||
font-size: 0.9rem;
|
||||
&:focus {
|
||||
color: inherit;
|
||||
background: transparent;
|
||||
border: none;
|
||||
outline: none;
|
||||
box-shadow: none;
|
||||
}
|
||||
`
|
||||
|
||||
const Anchor = styled.a`
|
||||
color: #d4d4d4;
|
||||
text-decoration: none;
|
||||
&:hover {
|
||||
color: white;
|
||||
}
|
||||
`
|
||||
|
||||
export const StyledAnchor = ({ children, href }) => (
|
||||
<Anchor href={href} target="_blank" rel="noopener">
|
||||
{children}
|
||||
</Anchor>
|
||||
)
|
37
src/components/herosection/index.tsx
Normal file
@ -0,0 +1,37 @@
|
||||
import React from 'react'
|
||||
import { PageProps, Link } from 'gatsby'
|
||||
|
||||
import styled from 'styled-components'
|
||||
|
||||
import { Hero, HeroHeading, HeroDesc } from './style'
|
||||
import { BottomSectionArrow, OutlineButton } from '../shared/styledComponents'
|
||||
import { Container } from '../shared'
|
||||
|
||||
import { pathPrefix } from '../../../gatsby-config.js'
|
||||
|
||||
type DataProps = {
|
||||
location: Location
|
||||
heading: string
|
||||
desc: string
|
||||
}
|
||||
|
||||
const HeroSection: React.FC<PageProps<DataProps>> = ({
|
||||
location,
|
||||
heading,
|
||||
desc
|
||||
}) => (
|
||||
<Hero bg={location.pathname === pathPrefix + '/'}>
|
||||
<Container>
|
||||
<HeroHeading>{heading}</HeroHeading>
|
||||
<HeroDesc>{desc}</HeroDesc>
|
||||
{location.pathname === pathPrefix + '/' && (
|
||||
<Link to="/contact/">
|
||||
<OutlineButton>Try Data Controller</OutlineButton>
|
||||
</Link>
|
||||
)}
|
||||
</Container>
|
||||
<BottomSectionArrow />
|
||||
</Hero>
|
||||
)
|
||||
|
||||
export default HeroSection
|
23
src/components/herosection/style.js
Normal file
@ -0,0 +1,23 @@
|
||||
import styled from 'styled-components'
|
||||
import background from '../../images/home_hero_bg.png'
|
||||
|
||||
export const Hero = styled.main`
|
||||
position: relative;
|
||||
padding: 50px 0;
|
||||
color: white;
|
||||
background-color: #314351;
|
||||
background-repeat: no-repeat;
|
||||
background-image: ${(props) => (props.bg ? `url(${background})` : 'none')};
|
||||
background-attachment: scroll;
|
||||
background-position: bottom right;
|
||||
`
|
||||
|
||||
export const HeroHeading = styled.h1`
|
||||
font-family: 'Montserrat', 'HelveticaNeue', 'Helvetica Neue', Helvetica, Arial,
|
||||
sans-serif;
|
||||
text-transform: uppercase;
|
||||
`
|
||||
|
||||
export const HeroDesc = styled.p`
|
||||
opacity: 0.8;
|
||||
`
|
41
src/components/layout.tsx
Normal file
@ -0,0 +1,41 @@
|
||||
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
|
||||
}
|
||||
|
||||
const Layout: React.FC<PageProps<DataProps>> = ({
|
||||
location,
|
||||
children,
|
||||
heroSection = true,
|
||||
heading,
|
||||
desc
|
||||
}) => {
|
||||
useEffect(() => {
|
||||
if (document.querySelector('script[data-name="kwes-script"]')) return
|
||||
|
||||
const kwesScript = document.createElement('script')
|
||||
kwesScript.setAttribute('rel', 'noopener')
|
||||
kwesScript.setAttribute('src', 'https://kwes.io/v2/kwes-script.js')
|
||||
kwesScript.setAttribute('data-name', 'kwes-script')
|
||||
document.head.appendChild(kwesScript)
|
||||
})
|
||||
return (
|
||||
<>
|
||||
<Navibar location={location} />
|
||||
{heroSection && (
|
||||
<HeroSection location={location} heading={heading} desc={desc} />
|
||||
)}
|
||||
{children}
|
||||
<Footer />
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default Layout
|
105
src/components/navibar/index.tsx
Normal file
@ -0,0 +1,105 @@
|
||||
import React from 'react'
|
||||
import { Link, PageProps } 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'
|
||||
|
||||
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'
|
||||
},
|
||||
{
|
||||
name: 'Documentation',
|
||||
url: 'https://docs.datacontroller.io/',
|
||||
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',
|
||||
active: 'no'
|
||||
}
|
||||
]
|
||||
|
||||
type DataProps = {
|
||||
location: Location
|
||||
}
|
||||
|
||||
const Navibar: React.FC<PageProps<DataProps>> = ({ location }) => {
|
||||
naviLinks.forEach((link) => (link.active = 'no'))
|
||||
const currentLink = naviLinks.find(
|
||||
(link) => pathPrefix + link.url === location?.pathname
|
||||
)
|
||||
if (currentLink) currentLink.active = 'yes'
|
||||
return (
|
||||
<CustomNavBar className="navbar navbar-expand-lg">
|
||||
<Container>
|
||||
<Link to="/">
|
||||
<img src={dcLogo} style={logoStyles} alt="Data Controller Logo" />
|
||||
</Link>
|
||||
<button
|
||||
className="navbar-toggler collapsed"
|
||||
type="button"
|
||||
data-bs-toggle="collapse"
|
||||
data-bs-target="#navbarSupportedContent"
|
||||
aria-controls="navbarSupportedContent"
|
||||
aria-expanded="false"
|
||||
aria-label="Toggle navigation"
|
||||
>
|
||||
<div className="navbar-toggler-icon" id="nav-icon4">
|
||||
<span></span>
|
||||
<span></span>
|
||||
<span></span>
|
||||
</div>
|
||||
</button>
|
||||
<div className="collapse navbar-collapse" id="navbarSupportedContent">
|
||||
<ul className="navbar-nav mb-2 mb-lg-0" style={ulStyles}>
|
||||
{naviLinks.map((link, index) => (
|
||||
<Li key={index} className="nav-item">
|
||||
<StyledLink
|
||||
to={link.url}
|
||||
className="nav-link"
|
||||
active={link.active}
|
||||
>
|
||||
{link.name}
|
||||
</StyledLink>
|
||||
</Li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
</Container>
|
||||
</CustomNavBar>
|
||||
)
|
||||
}
|
||||
|
||||
export default Navibar
|
333
src/components/navibar/style.js
Normal file
@ -0,0 +1,333 @@
|
||||
import React from 'react'
|
||||
import styled, { css } from 'styled-components'
|
||||
import { Link } from 'gatsby'
|
||||
|
||||
// styles
|
||||
export const logoStyles = {
|
||||
height: '55px'
|
||||
}
|
||||
export const ulStyles = {
|
||||
marginLeft: 'auto'
|
||||
}
|
||||
|
||||
export const CustomNavBar = styled.nav`
|
||||
padding: 0;
|
||||
background-color: #314351;
|
||||
color: white;
|
||||
font-size: 0.85rem;
|
||||
|
||||
/* Icon 1 */
|
||||
#nav-icon1,
|
||||
#nav-icon2,
|
||||
#nav-icon3,
|
||||
#nav-icon4 {
|
||||
width: 60px;
|
||||
height: 45px;
|
||||
position: relative;
|
||||
margin: 10px auto;
|
||||
-webkit-transform: rotate(0deg);
|
||||
-moz-transform: rotate(0deg);
|
||||
-o-transform: rotate(0deg);
|
||||
transform: rotate(0deg);
|
||||
-webkit-transition: 0.5s ease-in-out;
|
||||
-moz-transition: 0.5s ease-in-out;
|
||||
-o-transition: 0.5s ease-in-out;
|
||||
transition: 0.5s ease-in-out;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
#nav-icon1 span,
|
||||
#nav-icon3 span,
|
||||
#nav-icon4 span {
|
||||
display: block;
|
||||
position: absolute;
|
||||
height: 9px;
|
||||
width: 100%;
|
||||
background: #79a843;
|
||||
border-radius: 9px;
|
||||
opacity: 1;
|
||||
left: 0;
|
||||
-webkit-transform: rotate(0deg);
|
||||
-moz-transform: rotate(0deg);
|
||||
-o-transform: rotate(0deg);
|
||||
transform: rotate(0deg);
|
||||
-webkit-transition: 0.25s ease-in-out;
|
||||
-moz-transition: 0.25s ease-in-out;
|
||||
-o-transition: 0.25s ease-in-out;
|
||||
transition: 0.25s ease-in-out;
|
||||
}
|
||||
|
||||
#nav-icon1 span:nth-child(1) {
|
||||
top: 0px;
|
||||
}
|
||||
|
||||
#nav-icon1 span:nth-child(2) {
|
||||
top: 18px;
|
||||
}
|
||||
|
||||
#nav-icon1 span:nth-child(3) {
|
||||
top: 36px;
|
||||
}
|
||||
|
||||
.navbar-toggler:not(.collapsed) #nav-icon1 span:nth-child(1) {
|
||||
top: 18px;
|
||||
-webkit-transform: rotate(135deg);
|
||||
-moz-transform: rotate(135deg);
|
||||
-o-transform: rotate(135deg);
|
||||
transform: rotate(135deg);
|
||||
}
|
||||
|
||||
.navbar-toggler:not(.collapsed) #nav-icon1 span:nth-child(2) {
|
||||
opacity: 0;
|
||||
left: -60px;
|
||||
}
|
||||
|
||||
.navbar-toggler:not(.collapsed) #nav-icon1 span:nth-child(3) {
|
||||
top: 18px;
|
||||
-webkit-transform: rotate(-135deg);
|
||||
-moz-transform: rotate(-135deg);
|
||||
-o-transform: rotate(-135deg);
|
||||
transform: rotate(-135deg);
|
||||
}
|
||||
|
||||
/* Icon 2 */
|
||||
|
||||
#nav-icon2 {
|
||||
}
|
||||
|
||||
#nav-icon2 span {
|
||||
display: block;
|
||||
position: absolute;
|
||||
height: 9px;
|
||||
width: 50%;
|
||||
background: #d3531a;
|
||||
opacity: 1;
|
||||
-webkit-transform: rotate(0deg);
|
||||
-moz-transform: rotate(0deg);
|
||||
-o-transform: rotate(0deg);
|
||||
transform: rotate(0deg);
|
||||
-webkit-transition: 0.25s ease-in-out;
|
||||
-moz-transition: 0.25s ease-in-out;
|
||||
-o-transition: 0.25s ease-in-out;
|
||||
transition: 0.25s ease-in-out;
|
||||
}
|
||||
|
||||
#nav-icon2 span:nth-child(even) {
|
||||
left: 50%;
|
||||
border-radius: 0 9px 9px 0;
|
||||
}
|
||||
|
||||
#nav-icon2 span:nth-child(odd) {
|
||||
left: 0px;
|
||||
border-radius: 9px 0 0 9px;
|
||||
}
|
||||
|
||||
#nav-icon2 span:nth-child(1),
|
||||
#nav-icon2 span:nth-child(2) {
|
||||
top: 0px;
|
||||
}
|
||||
|
||||
#nav-icon2 span:nth-child(3),
|
||||
#nav-icon2 span:nth-child(4) {
|
||||
top: 18px;
|
||||
}
|
||||
|
||||
#nav-icon2 span:nth-child(5),
|
||||
#nav-icon2 span:nth-child(6) {
|
||||
top: 36px;
|
||||
}
|
||||
|
||||
.navbar-toggler:not(.collapsed) #nav-icon2 span:nth-child(1),
|
||||
.navbar-toggler:not(.collapsed) #nav-icon2 span:nth-child(6) {
|
||||
-webkit-transform: rotate(45deg);
|
||||
-moz-transform: rotate(45deg);
|
||||
-o-transform: rotate(45deg);
|
||||
transform: rotate(45deg);
|
||||
}
|
||||
|
||||
.navbar-toggler:not(.collapsed) #nav-icon2 span:nth-child(2),
|
||||
.navbar-toggler:not(.collapsed) #nav-icon2 span:nth-child(5) {
|
||||
-webkit-transform: rotate(-45deg);
|
||||
-moz-transform: rotate(-45deg);
|
||||
-o-transform: rotate(-45deg);
|
||||
transform: rotate(-45deg);
|
||||
}
|
||||
|
||||
.navbar-toggler:not(.collapsed) #nav-icon2 span:nth-child(1) {
|
||||
left: 5px;
|
||||
top: 7px;
|
||||
}
|
||||
|
||||
.navbar-toggler:not(.collapsed) #nav-icon2 span:nth-child(2) {
|
||||
left: calc(50% - 5px);
|
||||
top: 7px;
|
||||
}
|
||||
|
||||
.navbar-toggler:not(.collapsed) #nav-icon2 span:nth-child(3) {
|
||||
left: -50%;
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.navbar-toggler:not(.collapsed) #nav-icon2 span:nth-child(4) {
|
||||
left: 100%;
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.navbar-toggler:not(.collapsed) #nav-icon2 span:nth-child(5) {
|
||||
left: 5px;
|
||||
top: 29px;
|
||||
}
|
||||
|
||||
.navbar-toggler:not(.collapsed) #nav-icon2 span:nth-child(6) {
|
||||
left: calc(50% - 5px);
|
||||
top: 29px;
|
||||
}
|
||||
|
||||
/* Icon 3 */
|
||||
|
||||
#nav-icon3 span:nth-child(1) {
|
||||
top: 0px;
|
||||
}
|
||||
|
||||
#nav-icon3 span:nth-child(2),
|
||||
#nav-icon3 span:nth-child(3) {
|
||||
top: 18px;
|
||||
}
|
||||
|
||||
#nav-icon3 span:nth-child(4) {
|
||||
top: 36px;
|
||||
}
|
||||
|
||||
.navbar-toggler:not(.collapsed) #nav-icon3 span:nth-child(1) {
|
||||
top: 18px;
|
||||
width: 0%;
|
||||
left: 50%;
|
||||
}
|
||||
|
||||
.navbar-toggler:not(.collapsed) #nav-icon3 span:nth-child(2) {
|
||||
-webkit-transform: rotate(45deg);
|
||||
-moz-transform: rotate(45deg);
|
||||
-o-transform: rotate(45deg);
|
||||
transform: rotate(45deg);
|
||||
}
|
||||
|
||||
.navbar-toggler:not(.collapsed) #nav-icon3 span:nth-child(3) {
|
||||
-webkit-transform: rotate(-45deg);
|
||||
-moz-transform: rotate(-45deg);
|
||||
-o-transform: rotate(-45deg);
|
||||
transform: rotate(-45deg);
|
||||
}
|
||||
|
||||
.navbar-toggler:not(.collapsed) #nav-icon3 span:nth-child(4) {
|
||||
top: 18px;
|
||||
width: 0%;
|
||||
left: 50%;
|
||||
}
|
||||
|
||||
/* Icon 4 */
|
||||
|
||||
#nav-icon4 {
|
||||
}
|
||||
|
||||
#nav-icon4 span:nth-child(1) {
|
||||
top: 0px;
|
||||
-webkit-transform-origin: left center;
|
||||
-moz-transform-origin: left center;
|
||||
-o-transform-origin: left center;
|
||||
transform-origin: left center;
|
||||
}
|
||||
|
||||
#nav-icon4 span:nth-child(2) {
|
||||
top: 18px;
|
||||
-webkit-transform-origin: left center;
|
||||
-moz-transform-origin: left center;
|
||||
-o-transform-origin: left center;
|
||||
transform-origin: left center;
|
||||
}
|
||||
|
||||
#nav-icon4 span:nth-child(3) {
|
||||
top: 36px;
|
||||
-webkit-transform-origin: left center;
|
||||
-moz-transform-origin: left center;
|
||||
-o-transform-origin: left center;
|
||||
transform-origin: left center;
|
||||
}
|
||||
|
||||
.navbar-toggler:not(.collapsed) #nav-icon4 span:nth-child(1) {
|
||||
-webkit-transform: rotate(45deg);
|
||||
-moz-transform: rotate(45deg);
|
||||
-o-transform: rotate(45deg);
|
||||
transform: rotate(45deg);
|
||||
top: -3px;
|
||||
left: 8px;
|
||||
}
|
||||
|
||||
.navbar-toggler:not(.collapsed) #nav-icon4 span:nth-child(2) {
|
||||
width: 0%;
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.navbar-toggler:not(.collapsed) #nav-icon4 span:nth-child(3) {
|
||||
-webkit-transform: rotate(-45deg);
|
||||
-moz-transform: rotate(-45deg);
|
||||
-o-transform: rotate(-45deg);
|
||||
transform: rotate(-45deg);
|
||||
top: 39px;
|
||||
left: 8px;
|
||||
}
|
||||
|
||||
.navbar-toggler {
|
||||
padding: 0;
|
||||
&:focus {
|
||||
box-shadow: none;
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
const LinkUnderlineStyles = css`
|
||||
content: ' ';
|
||||
position: absolute;
|
||||
width: calc(100% - 1.6rem);
|
||||
height: 2px;
|
||||
bottom: 0;
|
||||
background: white;
|
||||
opacity: 1;
|
||||
transition: opacity 0.3s ease;
|
||||
`
|
||||
|
||||
// styled components
|
||||
export const Li = styled.li`
|
||||
position: relative;
|
||||
@media (min-width: 992px) {
|
||||
&:after {
|
||||
content: ' ';
|
||||
position: absolute;
|
||||
width: 1px;
|
||||
height: 50%;
|
||||
top: 25%;
|
||||
right: 0;
|
||||
background: white;
|
||||
}
|
||||
&:nth-last-child(1) {
|
||||
&:after {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
`
|
||||
export const StyledLink = styled((props) => <Link {...props} />)`
|
||||
padding-right: 0.8rem !important;
|
||||
padding-left: 0.8rem !important;
|
||||
color: white;
|
||||
|
||||
&:before {
|
||||
${LinkUnderlineStyles}
|
||||
opacity: ${(props) => (props.active === 'yes' ? '1' : 0)};
|
||||
}
|
||||
&:hover {
|
||||
color: white;
|
||||
&:before {
|
||||
${LinkUnderlineStyles}
|
||||
}
|
||||
}
|
||||
`
|
80
src/components/seo.tsx
Normal file
@ -0,0 +1,80 @@
|
||||
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 }) => {
|
||||
const { site } = useStaticQuery(graphql`
|
||||
query {
|
||||
site {
|
||||
siteMetadata {
|
||||
title
|
||||
description
|
||||
siteUrl
|
||||
author {
|
||||
name
|
||||
}
|
||||
social {
|
||||
linkedin
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`)
|
||||
|
||||
const author = site.siteMetadata?.author?.name
|
||||
const metaDescription = description || site.siteMetadata.description
|
||||
const defaultTitle = site.siteMetadata?.title
|
||||
const pageTitle = title ? `${title} | ${defaultTitle}` : defaultTitle
|
||||
const siteUrl = site.siteMetadata?.siteUrl
|
||||
const image = previewImg
|
||||
? `${siteUrl}${previewImg}`
|
||||
: `${siteUrl}/img/data-controller.svg`
|
||||
|
||||
return (
|
||||
<Helmet
|
||||
htmlAttributes={{
|
||||
lang
|
||||
}}
|
||||
title={pageTitle}
|
||||
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)}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
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
|
18
src/components/shared/container.tsx
Normal file
@ -0,0 +1,18 @@
|
||||
import React from 'react'
|
||||
import styled from 'styled-components'
|
||||
import { PageProps } from 'gatsby'
|
||||
|
||||
type DataProps = {
|
||||
children?: React.ReactNode
|
||||
}
|
||||
|
||||
const StyledDiv = styled.div`
|
||||
@media (min-width: 576px) {
|
||||
max-width: 1310px;
|
||||
padding: 0px 50px;
|
||||
}
|
||||
`
|
||||
|
||||
export const Container: React.FC<PageProps<DataProps>> = ({ children }) => {
|
||||
return <StyledDiv className="container">{children}</StyledDiv>
|
||||
}
|
3
src/components/shared/index.ts
Normal file
@ -0,0 +1,3 @@
|
||||
export { Container } from './container'
|
||||
export { Section } from './section'
|
||||
export { ScheduleDemo } from './scheduleDemo'
|
53
src/components/shared/scheduleDemo.tsx
Normal file
@ -0,0 +1,53 @@
|
||||
import React from 'react'
|
||||
import { Link } from 'gatsby'
|
||||
import styled from 'styled-components'
|
||||
|
||||
import { FaEnvelope } from 'react-icons/fa'
|
||||
|
||||
export const StyledLink = styled((props) => <Link {...props} />)`
|
||||
color: rgb(255, 255, 255);
|
||||
background-color: rgb(144, 196, 69);
|
||||
border-radius: 0px;
|
||||
padding: 50px 10px;
|
||||
width: 100%;
|
||||
margin: 0px;
|
||||
border: none;
|
||||
position: relative;
|
||||
display: block;
|
||||
text-decoration: none;
|
||||
font-size: 1.3rem;
|
||||
line-height: 1.2em;
|
||||
text-align: center;
|
||||
max-width: 100%;
|
||||
font-family: Montserrat, HelveticaNeue, 'Helvetica Neue', Helvetica, Arial;
|
||||
|
||||
svg {
|
||||
opacity: 0;
|
||||
transition: all 0.5s ease;
|
||||
}
|
||||
&:hover {
|
||||
color: white;
|
||||
background-color: #314351;
|
||||
svg {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
transition: all 0.3s ease;
|
||||
`
|
||||
|
||||
const iconStyles = { marginTop: '-2px', marginLeft: '5px' }
|
||||
const textStyles = { opacity: '0.7', fontSize: '1rem', margin: '12px auto 0' }
|
||||
|
||||
export const ScheduleDemo = () => {
|
||||
return (
|
||||
<StyledLink to="/contact">
|
||||
<span>
|
||||
Schedule a Free Demo <FaEnvelope size={18} style={iconStyles} />
|
||||
</span>
|
||||
<p style={textStyles}>
|
||||
Contact us for a free demonstration of Data Controller.
|
||||
</p>
|
||||
</StyledLink>
|
||||
)
|
||||
}
|
34
src/components/shared/section.tsx
Normal file
@ -0,0 +1,34 @@
|
||||
import 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
|
||||
}
|
||||
|
||||
const StyledSection = styled.div`
|
||||
position: relative;
|
||||
padding: 50px 0;
|
||||
color: ${(props) => props.color || 'white'};
|
||||
background-color: ${(props) => props.bgColor || '#314351'};
|
||||
`
|
||||
|
||||
export const Section: React.FC<PageProps<DataProps>> = ({
|
||||
children,
|
||||
bgColor,
|
||||
color,
|
||||
bottomArrow = true
|
||||
}) => {
|
||||
return (
|
||||
<StyledSection bgColor={bgColor} color={color}>
|
||||
<Container>{children}</Container>
|
||||
{bottomArrow && <BottomSectionArrow />}
|
||||
</StyledSection>
|
||||
)
|
||||
}
|
88
src/components/shared/styledComponents.js
Normal file
@ -0,0 +1,88 @@
|
||||
import React from 'react'
|
||||
import styled from 'styled-components'
|
||||
|
||||
const BottomArrow = styled.div`
|
||||
width: 50px;
|
||||
height: 50px;
|
||||
position: absolute;
|
||||
bottom: 10px;
|
||||
background: inherit;
|
||||
transform: translateX(-50%) rotate(45deg);
|
||||
left: 50%;
|
||||
// right: 0;
|
||||
// margin-left: auto;
|
||||
// margin-right: auto;
|
||||
z-index: 10;
|
||||
`
|
||||
const BottomArrowWrapper = styled.div`
|
||||
width: 100%;
|
||||
height: 20px;
|
||||
position: absolute;
|
||||
overflow: hidden;
|
||||
margin-top: 50px;
|
||||
background-color: inherit;
|
||||
`
|
||||
|
||||
export const BottomSectionArrow = () => (
|
||||
<BottomArrowWrapper>
|
||||
<BottomArrow />
|
||||
</BottomArrowWrapper>
|
||||
)
|
||||
|
||||
export const SectionHeading = styled.h2`
|
||||
text-align: ${(props) => (props.center === 'no' ? 'left' : 'center')};
|
||||
letter-spacing: 1px;
|
||||
font-weight: 400;
|
||||
font-family: 'Montserrat', 'HelveticaNeue', 'Helvetica Neue', Helvetica, Arial,
|
||||
sans-serif;
|
||||
text-transform: uppercase;
|
||||
`
|
||||
|
||||
export const SectionDesc = styled.p`
|
||||
text-align: ${(props) => (props.center === 'no' ? 'left' : 'center')};
|
||||
opacity: ${(props) => props.opacity ?? 0.6};
|
||||
a {
|
||||
color: inherit;
|
||||
}
|
||||
`
|
||||
const StyledSolidButton = styled.button`
|
||||
padding: 0.75rem 1.5rem;
|
||||
font-size: 0.75rem;
|
||||
border-width: 2px;
|
||||
width: 100%;
|
||||
max-width: 250px;
|
||||
&:hover {
|
||||
opacity: 0.9;
|
||||
}
|
||||
&.btn-dark {
|
||||
background-color: #2e4252;
|
||||
}
|
||||
`
|
||||
|
||||
export const SolidButton = ({
|
||||
children,
|
||||
theme = 'light',
|
||||
type = 'submit',
|
||||
onClick = undefined
|
||||
}) => (
|
||||
<StyledSolidButton
|
||||
type={type}
|
||||
className={`btn btn-${theme}`}
|
||||
onClick={onClick}
|
||||
>
|
||||
{children}
|
||||
</StyledSolidButton>
|
||||
)
|
||||
|
||||
const StyledOutlineButton = styled.button`
|
||||
margin: 50px 0;
|
||||
padding: 0.75rem 1.5rem;
|
||||
font-size: 0.75rem;
|
||||
border-width: 2px;
|
||||
`
|
||||
|
||||
export const OutlineButton = ({ children }) => (
|
||||
<StyledOutlineButton type="button" className="btn btn-outline-light">
|
||||
{children}
|
||||
</StyledOutlineButton>
|
||||
)
|
BIN
src/images/contact_bg.jpg
Normal file
After Width: | Height: | Size: 101 KiB |
BIN
src/images/data-controller-design.png
Normal file
After Width: | Height: | Size: 113 KiB |
BIN
src/images/dc-software.png
Normal file
After Width: | Height: | Size: 8.4 KiB |
BIN
src/images/dclogo.png
Normal file
After Width: | Height: | Size: 7.8 KiB |
BIN
src/images/favicon.png
Normal file
After Width: | Height: | Size: 34 KiB |
BIN
src/images/gallery/01-select-table.png
Normal file
After Width: | Height: | Size: 134 KiB |
BIN
src/images/gallery/02-review-approve.png
Normal file
After Width: | Height: | Size: 212 KiB |
BIN
src/images/gallery/03-submitted.png
Normal file
After Width: | Height: | Size: 222 KiB |
BIN
src/images/gallery/04-viewer-table.png
Normal file
After Width: | Height: | Size: 218 KiB |
BIN
src/images/gallery/05-staged-data.png
Normal file
After Width: | Height: | Size: 188 KiB |
BIN
src/images/gallery/06-approve-data.png
Normal file
After Width: | Height: | Size: 224 KiB |
BIN
src/images/gallery/07-edit-success.png
Normal file
After Width: | Height: | Size: 145 KiB |
BIN
src/images/gallery/08-edit.png
Normal file
After Width: | Height: | Size: 147 KiB |
BIN
src/images/gallery/09-table-select.png
Normal file
After Width: | Height: | Size: 155 KiB |
BIN
src/images/gallery/10-query-builder.png
Normal file
After Width: | Height: | Size: 170 KiB |
BIN
src/images/home_hero_bg.png
Normal file
After Width: | Height: | Size: 5.8 KiB |
BIN
src/images/home_illustration1.png
Normal file
After Width: | Height: | Size: 6.1 KiB |
BIN
src/images/home_illustration2.png
Normal file
After Width: | Height: | Size: 9.8 KiB |
BIN
src/images/sasapps/cdoquiz.png
Normal file
After Width: | Height: | Size: 12 KiB |
BIN
src/images/sasapps/insure.png
Normal file
After Width: | Height: | Size: 15 KiB |
BIN
src/images/sasapps/js.png
Normal file
After Width: | Height: | Size: 18 KiB |
BIN
src/images/sasapps/pilot.png
Normal file
After Width: | Height: | Size: 24 KiB |
BIN
src/images/sasapps/pygrail.png
Normal file
After Width: | Height: | Size: 6.7 KiB |
BIN
src/images/sasapps/quizr.png
Normal file
After Width: | Height: | Size: 12 KiB |
BIN
src/images/sasapps/ruby.png
Normal file
After Width: | Height: | Size: 8.4 KiB |
BIN
src/images/sasapps/sasensei.png
Normal file
After Width: | Height: | Size: 7.7 KiB |
BIN
src/images/sasapps/serbian.png
Normal file
After Width: | Height: | Size: 31 KiB |
BIN
src/images/sasapps/yoga.png
Normal file
After Width: | Height: | Size: 28 KiB |
BIN
src/images/sasensei_featured.png
Normal file
After Width: | Height: | Size: 3.1 KiB |
137
src/markdown-pages/pricing.md
Normal file
@ -0,0 +1,137 @@
|
||||
---
|
||||
slug: 'pricing'
|
||||
title: 'Pricing'
|
||||
heading: 'Data Controller Pricing'
|
||||
previewImg: './static/home_illustration2-00649b1f413e09cfec86a2afba69dacc.png'
|
||||
desc: 'Our Pricing Goals are to be Transparent, Simple, and Fair'
|
||||
description: Data Controller for SAS® pricing starts from FREE, is fully TRANSPARENT, and - at Enterprise Level - totally FLEXIBLE.
|
||||
---
|
||||
|
||||
<style>
|
||||
table {
|
||||
width: 100%;
|
||||
}
|
||||
</style>
|
||||
|
||||
## Packages
|
||||
|
||||
Data Controller is available in four packages, with no limits on number of users (since [v6](https://datacontroller.io/v6-0-api-explorer/)). Data Controller can run on ANY flavour of SAS - be that Viya, EBI, or [SASjs Server](https://server.sasjs.io).
|
||||
|
||||
| | Community | Bronze | Silver | Gold |
|
||||
|--------------------------|-----------------|-----------|-----------|------------|
|
||||
| Number of Users | Unlimited | Unlimited | Unlimited | Unlimited |
|
||||
| Environments | Unlimited | Unlimited | Unlimited | Unlimited |
|
||||
| Libraries | Unlimited | Unlimited | Unlimited | Unlimited |
|
||||
| Tables (per library) | 35 | 100 | Unlimited | Unlimited |
|
||||
| Rows (VIEW) | 15 | 100 | Unlimited | Unlimited |
|
||||
| Rows (UPLOAD) | 5 | 100 | Unlimited | Unlimited |
|
||||
| Rows (HISTORY) | 15 | 100 | Unlimited | Unlimited |
|
||||
| Lineage | 3 per day | 100 | Unlimited | Unlimited |
|
||||
| Viewboxes | 1 | 3 | 6 (max) | 6 (max) |
|
||||
| Data Controller Support | Best Endeavours | 8hr SLA | 8hr SLA | 4hr SLA |
|
||||
| SASjs Support | ❌ | ✅ | ✅ | ✅ |
|
||||
| In-house SAS App Support | ❌ | ❌ | ✅ | ✅ |
|
||||
| Developer Days | ❌ | ❌ | ❌ | ✅ |
|
||||
| Consulting Days | ❌ | ❌ | ❌ | ✅ |
|
||||
| Perpetual Licence Option | ❌ | ❌ | ❌ | ✅ |
|
||||
| Price | FREE | 12k EUR pa | 25k EUR pa| Contact Us |
|
||||
|
||||
</br>
|
||||
<i>Data Controller Development Days (to build new product features etc) are available for all plans at a price of 600 EUR per day. Consulting rates are published <a href="https://sasapps.io/pricing">here</a>.</i>
|
||||
|
||||
|
||||
## Support
|
||||
|
||||
### Tickets
|
||||
|
||||
Our support goal is to get you back up and running as soon as possible in the case of any incidents. In addition, we are on hand to assist with troubleshooting, debugging, strategic advice, and to answer any questions in relation to your SAS App(s) - be they from admins, users, or developers.
|
||||
|
||||
Tickets may be raised at either:
|
||||
|
||||
1. Support Desk: https://support.4gl.io
|
||||
2. Support Mailbox: support@datacontroller.io
|
||||
|
||||
|
||||
The corresponding severity level should also be specified, eg as follows:
|
||||
|
||||
1. Level 1 - App is non-functional and cannot be used
|
||||
2. Level 2 - Critical issue, however the app is still functional
|
||||
3. Level 3 - Important issue
|
||||
4. Level 4 - Minor Issue
|
||||
|
||||
The severity level will be initially assigned by the client, and is subject to the agreement of all parties. Circumstances may change during the lifecycle of an issue, and hence the priority may change (again, subject to all party agreement).
|
||||
|
||||
"Response Time" means the number of hours between the logging of the Support Request, and the time that a technician begins active work on the Support Request. The beginning of active work by the technician can be evidenced by either the logon for Remote Access, or the e-mail by 4GL Ltd support staff.
|
||||
|
||||
"Resolution Time" means the number of hours between the logging of the Support Request and the provision by 4GL of a fix, or satisfactory response.
|
||||
|
||||
All times are deemed to be elapsed during UK Business hours (9am to 5pm), and following the UK holiday schedule.
|
||||
|
||||
Severity Level 4 Support Requests will be investigated on a first in / first out basis, after all Level 1/2/3 Support Requests have been solved.
|
||||
|
||||
Should a Severity Level 1 or 2 Support Request fail to be cleared within the Resolution Time, all applicable resources within 4GL will be called upon to assist in the issue.
|
||||
|
||||
If an issue cannot be reproduced, then the Resolution Time will be suspended until such time as it happens again. Meanwhile, the support team will be on alert for the re-occurrence, and to foresee the actions needed to reproduce. On reproduction, the Resolution Time will start again at zero.
|
||||
|
||||
We aim to meet these targets in 90% of all incidents reported. Support issues will be worked on until a mutually agreed resolution is achieved, or until the customer requests the issue to be closed. If there is no response or update from a customer on an issue for more than 30 calendar days, the issue will be deemed closed.
|
||||
|
||||
The following items are assumed:
|
||||
|
||||
1. 4GL Ltd will have full access to the source code of any supported apps
|
||||
2. Any necessary systems access will be provided
|
||||
3. Development work will be performed offsite, using sample data
|
||||
4. The client is able to provide relevant artefacts (logs, sample data, clarifications)
|
||||
|
||||
### Escalation
|
||||
If a ticket cannot be resolved within the agreed SLA conditions, escalation has to be started by the assigned support department. Escalation will be made via video call between managers at both the client and 4GL.
|
||||
|
||||
Complaints on services should be directed to both the 4GL Engagement Manager and the client Application Owner.
|
||||
|
||||
|
||||
### In Scope Support
|
||||
|
||||
Support is unlimited, but restricted to items that actually relate to the tool (eg we cannot help with permissions, or data issues, beyond the guidance in the documentation). All support is provided remotely, using video calls / screen sharing technology.no
|
||||
|
||||
|
||||
Examples of ‘in-scope’ (not an exhaustive list):
|
||||
|
||||
* configuration & usage advice
|
||||
* troubleshooting
|
||||
* installation / deployment support
|
||||
* integration advice (actual integrations are a consulting effort)
|
||||
|
||||
### Out of Scope Support
|
||||
|
||||
Our support goal is to help you by answering questions and solving problems that relate specifically to the Data Controller for SAS® software, SASjs, or other in-house SAS-Powered Web Apps per the subscription plan.
|
||||
|
||||
Occasionally a situation may arise where the scope is outside of the support service, including (but not limited to) the items below.
|
||||
|
||||
#### Data Issues
|
||||
|
||||
Whilst Data Controller has been hardened over many years to deal with most data issues, there are some things that cannot be solved by the product itself, such as:
|
||||
|
||||
- Library configuration issues
|
||||
- Permissions issues (OS or metadata)
|
||||
- Physical locks (eg on SAS tables)
|
||||
- System issues (outages)
|
||||
|
||||
#### New Features
|
||||
|
||||
The Data Controller Support Team can assist with troubleshooting and issue support, but they cannot create additional functionality. In this case a request may be made to enhance the product (Developer Days), or our Professional Services may be engaged to deliver a bespoke solution.
|
||||
|
||||
#### Integrations
|
||||
|
||||
Data Controller was built to ‘play nicely’ with other tools and platforms. The Support Team can assist with education and advice on how those integrations work, but they cannot perform those integrations for you. In this case, our Professional Services team should be engaged.
|
||||
|
||||
#### Non Standard Situations
|
||||
|
||||
There may be situations where support cannot give immediate fixes, such as (but not limited to):
|
||||
|
||||
- Issues with client computing environments
|
||||
- Inability of the client to provide remote access
|
||||
- Regulatory or Force Majeure events In such cases the issue will be referred to management and a call arranged to agree a way forward.
|
||||
|
||||
### Partner Purchases
|
||||
|
||||
Where Data Controller is purchased directly from one of our partners, the partner may provide first line support as per your agreement with them.
|
||||
|
23
src/pages/404.tsx
Normal file
@ -0,0 +1,23 @@
|
||||
import React from 'react'
|
||||
// import { Link } from 'gatsby'
|
||||
|
||||
import Layout from '../components/layout'
|
||||
import Seo from '../components/seo'
|
||||
|
||||
import { Section } from '../components/shared'
|
||||
import { SideBar } from '../templates/sidebar'
|
||||
|
||||
const NotFound = ({ data, location, pageContext }) => (
|
||||
<Layout location={location} heroSection={false}>
|
||||
<Seo title="Page Not Found" />
|
||||
<Section color="black" bgColor="white" bottomArrow={false}>
|
||||
<SideBar
|
||||
pageContext={pageContext}
|
||||
location={location}
|
||||
notFoundPage={true}
|
||||
/>
|
||||
</Section>
|
||||
</Layout>
|
||||
)
|
||||
|
||||
export default NotFound
|
215
src/pages/about.tsx
Normal file
@ -0,0 +1,215 @@
|
||||
import { PageProps, Link, graphql } from 'gatsby'
|
||||
import React from 'react'
|
||||
|
||||
import Layout from '../components/layout'
|
||||
import Seo from '../components/seo'
|
||||
|
||||
import { Section } from '../components/shared'
|
||||
import {
|
||||
SectionHeading,
|
||||
SectionDesc
|
||||
} from '../components/shared/styledComponents'
|
||||
|
||||
import {
|
||||
Card,
|
||||
CardImg,
|
||||
CardBody,
|
||||
FeaturedImg,
|
||||
FeaturedDesc
|
||||
} from '../styledComponents/about'
|
||||
|
||||
import sasenseiLogo from '../images/sasapps/sasensei.png'
|
||||
import rubyLogo from '../images/sasapps/ruby.png'
|
||||
import pygrailLogo from '../images/sasapps/pygrail.png'
|
||||
import pilotLogo from '../images/sasapps/pilot.png'
|
||||
import yogaLogo from '../images/sasapps/yoga.png'
|
||||
import jsLogo from '../images/sasapps/js.png'
|
||||
import serbianLogo from '../images/sasapps/serbian.png'
|
||||
import quizrLogo from '../images/sasapps/quizr.png'
|
||||
import cdoquizLogo from '../images/sasapps/cdoquiz.png'
|
||||
import insureLogo from '../images/sasapps/insure.png'
|
||||
|
||||
import sasenseiFeatured from '../images/sasensei_featured.png'
|
||||
|
||||
type DataProps = {
|
||||
site: {
|
||||
meta: {
|
||||
title: string
|
||||
description: string
|
||||
social: { linkedin: string }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const SasApps = [
|
||||
{
|
||||
name: 'Sasensei',
|
||||
desc: 'Sasensei. SAS Software Quiz, Challenge, Compete & Learn',
|
||||
link: 'https://sasensei.com',
|
||||
themeColor: '#2196F3',
|
||||
logo: sasenseiLogo
|
||||
},
|
||||
{
|
||||
name: 'Ruby',
|
||||
desc: 'Ruby Cards - a Community Quiz Game for Rubyists',
|
||||
link: 'https://ruby.cards',
|
||||
themeColor: '#aa1502',
|
||||
logo: rubyLogo
|
||||
},
|
||||
{
|
||||
name: 'PyGrail',
|
||||
desc: 'PyGrail - a Community Quiz Game for Monty Pythonistas',
|
||||
link: 'https://pygrail.com',
|
||||
themeColor: '#efbf2c',
|
||||
logo: pygrailLogo
|
||||
},
|
||||
{
|
||||
name: 'Pilot',
|
||||
desc: 'Pilot Cards Leaderboard - How High Can You Climb?',
|
||||
link: 'https://pilot.cards',
|
||||
themeColor: '#446ff3',
|
||||
logo: pilotLogo
|
||||
},
|
||||
{
|
||||
name: 'Yoga.cards',
|
||||
desc: 'Yoga Cards - a Community Quiz Game for Student Yogis',
|
||||
link: 'https://yoga.cards',
|
||||
themeColor: '#6d69fb',
|
||||
logo: yogaLogo
|
||||
},
|
||||
{
|
||||
name: 'JS.cards',
|
||||
desc: 'JS Cards - a Community Quiz Game for JavaScript Ninjas',
|
||||
link: 'https://js.cards',
|
||||
themeColor: '#00cc99',
|
||||
logo: jsLogo
|
||||
},
|
||||
{
|
||||
name: 'Serbian.cards',
|
||||
desc: 'Serbian Cards - How Much Serbian Do You Know?',
|
||||
link: 'https://serbian.cards',
|
||||
themeColor: '#c6363c',
|
||||
logo: serbianLogo
|
||||
},
|
||||
{
|
||||
name: 'Quizr',
|
||||
desc: 'quizR - a community quiz game for useRs',
|
||||
link: 'https://quizr.io',
|
||||
themeColor: '#51904c',
|
||||
logo: quizrLogo
|
||||
},
|
||||
{
|
||||
name: 'CDOquiz',
|
||||
desc: 'CDO Quiz - a Royally Challenging Data Adventure',
|
||||
link: 'https://cdoquiz.com/',
|
||||
themeColor: '#a54499',
|
||||
logo: cdoquizLogo
|
||||
},
|
||||
{
|
||||
name: 'Insure.cards',
|
||||
desc: 'Insure Cards - Are You An Insurance Expert? Prove It',
|
||||
link: 'https://insure.cards/',
|
||||
themeColor: '#5f8ba5',
|
||||
logo: insureLogo
|
||||
}
|
||||
]
|
||||
|
||||
const About: React.FC<PageProps<DataProps>> = ({ data, location }) => {
|
||||
return (
|
||||
<Layout
|
||||
location={location}
|
||||
heading="Behind Data Controller"
|
||||
desc="Data Controller is a product of 4GL, a brand of Bowe IO Ltd"
|
||||
>
|
||||
<Seo title="About" />
|
||||
<Section color="black" bgColor="white">
|
||||
<div className="row">
|
||||
<div className="offset-md-2 col-md-8">
|
||||
<SectionHeading center="no">About Data Controller</SectionHeading>
|
||||
<SectionDesc center="no">
|
||||
Data Controller is sold through Bowe IO Ltd, a company based
|
||||
in the UK and operating since 2013.
|
||||
<br />
|
||||
The company supports the development of the Data Controller web
|
||||
application and other{' '}
|
||||
<a href="https://sasapps.io" title="SAS Apps">
|
||||
SAS Apps
|
||||
</a>
|
||||
, and the Macro Cards range of websites listed below:
|
||||
</SectionDesc>
|
||||
</div>
|
||||
</div>
|
||||
<div className="row row-cols-1 row-cols-sm-2 row-cols-md-3 row-cols-lg-4 row-cols-xl-5">
|
||||
{SasApps.map((app) => (
|
||||
<div className="col">
|
||||
<Card
|
||||
className="card"
|
||||
href={app.link}
|
||||
target="_blank"
|
||||
rel="noopener"
|
||||
>
|
||||
<CardImg
|
||||
className="card-img-top"
|
||||
src={app.logo}
|
||||
alt={app.name}
|
||||
title={app.name}
|
||||
/>
|
||||
<CardBody className="card-body">
|
||||
<h5 className="card-title">{app.name}</h5>
|
||||
<p className="card-text">{app.desc}</p>
|
||||
</CardBody>
|
||||
</Card>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</Section>
|
||||
<Section>
|
||||
<FeaturedImg src={sasenseiFeatured} alt="Sasensei" title="Sasensei" />
|
||||
<FeaturedDesc>
|
||||
Sasensei is a free question-based learning system dedicated to various
|
||||
aspects of the
|
||||
<br />
|
||||
SAS® software such as SAS® Programming, Administration, Risk
|
||||
Dimensions, Viya, AF/SCL, and much more.
|
||||
</FeaturedDesc>
|
||||
<FeaturedDesc>
|
||||
This SAS® quiz allows the player to test their knowledge, challenge
|
||||
their peers and submit their own questions for others to enjoy.
|
||||
<br />
|
||||
Sasensei is constantly improving by adding new features which
|
||||
contribute to the player experience and increase the educational
|
||||
value.
|
||||
</FeaturedDesc>
|
||||
</Section>
|
||||
<Section color="black" bgColor="#eaeaea" bottomArrow={false}>
|
||||
<div className="row row-cols-1 row-cols-sm-2 row-cols-md-3 row-cols-lg-4 row-cols-xl-5">
|
||||
<div className="col">
|
||||
1000+
|
||||
<br />
|
||||
Active Players
|
||||
</div>
|
||||
<div className="col">
|
||||
650+
|
||||
<br />
|
||||
Questions
|
||||
</div>
|
||||
<div className="col">
|
||||
20+
|
||||
<br />
|
||||
New Players Every Day
|
||||
</div>
|
||||
<div className="col">
|
||||
6<br />
|
||||
Unique Features
|
||||
</div>
|
||||
<div className="col">
|
||||
7<br />
|
||||
Difficulty Levels
|
||||
</div>
|
||||
</div>
|
||||
</Section>
|
||||
</Layout>
|
||||
)
|
||||
}
|
||||
|
||||
export default About
|
124
src/pages/contact.tsx
Normal file
@ -0,0 +1,124 @@
|
||||
import { PageProps, Link, graphql } from 'gatsby'
|
||||
import React from 'react'
|
||||
|
||||
import Layout from '../components/layout'
|
||||
import Seo from '../components/seo'
|
||||
|
||||
import { Section } from '../components/shared'
|
||||
import {
|
||||
SectionHeading,
|
||||
SectionDesc,
|
||||
SolidButton
|
||||
} from '../components/shared/styledComponents'
|
||||
|
||||
import {
|
||||
StyledHeading,
|
||||
StyledLabel,
|
||||
ContactBackground
|
||||
} from '../styledComponents/contact'
|
||||
|
||||
import contactBg from '../images/contact_bg.jpg'
|
||||
|
||||
import '../styledComponents/contact.css'
|
||||
|
||||
type DataProps = {
|
||||
site: {
|
||||
meta: {
|
||||
title: string
|
||||
description: string
|
||||
social: { linkedin: string }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const Contact: React.FC<PageProps<DataProps>> = ({ data, location }) => {
|
||||
return (
|
||||
<Layout
|
||||
location={location}
|
||||
heading="Book a Demo"
|
||||
desc="Schedule a demonstration of the Data Controller to see our latest features and explore use cases"
|
||||
>
|
||||
<Seo title="Contact" />
|
||||
<Section color="black" bgColor="white" bottomArrow={false}>
|
||||
<div className="row">
|
||||
<div className="col-md-6">
|
||||
<SectionHeading center="no">Schedule a Call</SectionHeading>
|
||||
<SectionDesc center="no">
|
||||
Feel free to contact us at any time to book a demo and start your
|
||||
Data Controller journey. Our team will respond within 24 hours and
|
||||
help you proceed with the registration process.
|
||||
</SectionDesc>
|
||||
<StyledHeading>Contact Us</StyledHeading>
|
||||
|
||||
<form
|
||||
className="kwes-form"
|
||||
method="POST"
|
||||
action="https://kwes.io/api/foreign/forms/mxKuyK4lxZWnG2WNH3ga"
|
||||
>
|
||||
<div className="mb-3">
|
||||
<StyledLabel htmlFor="name" className="form-label">
|
||||
Name
|
||||
</StyledLabel>
|
||||
<input
|
||||
type="text"
|
||||
className="form-control contactFormStyles"
|
||||
id="name"
|
||||
name="name"
|
||||
rules="required|max:50"
|
||||
/>
|
||||
</div>
|
||||
<div className="mb-3">
|
||||
<StyledLabel htmlFor="email" className="form-label">
|
||||
Email address
|
||||
</StyledLabel>
|
||||
<input
|
||||
type="email"
|
||||
className="form-control contactFormStyles"
|
||||
id="email"
|
||||
name="email"
|
||||
rules="required|email"
|
||||
aria-describedby="emailHelp"
|
||||
/>
|
||||
<div id="emailHelp" className="form-text">
|
||||
We'll never share your email with anyone else.
|
||||
</div>
|
||||
</div>
|
||||
<div className="mb-3">
|
||||
<StyledLabel htmlFor="subject" className="form-label">
|
||||
Subject
|
||||
</StyledLabel>
|
||||
<input
|
||||
type="text"
|
||||
className="form-control contactFormStyles"
|
||||
id="subject"
|
||||
name="subject"
|
||||
rules="required|max:50"
|
||||
/>
|
||||
</div>
|
||||
<div className="mb-3">
|
||||
<StyledLabel htmlFor="mesage" className="form-label">
|
||||
Message
|
||||
</StyledLabel>
|
||||
<textarea
|
||||
className="form-control contactFormStyles"
|
||||
id="mesage"
|
||||
name="message"
|
||||
rows="5"
|
||||
rules="required|max:200"
|
||||
></textarea>
|
||||
</div>
|
||||
<div className="mb-3">
|
||||
<SolidButton theme="dark">Submit</SolidButton>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<div className="col-md-6">
|
||||
<ContactBackground src={contactBg} info="Book a Demo" />
|
||||
</div>
|
||||
</div>
|
||||
</Section>
|
||||
</Layout>
|
||||
)
|
||||
}
|
||||
|
||||
export default Contact
|
79
src/pages/faq.tsx
Normal file
@ -0,0 +1,79 @@
|
||||
import { PageProps, Link, graphql } from 'gatsby'
|
||||
import React from 'react'
|
||||
|
||||
import Layout from '../components/layout'
|
||||
import Seo from '../components/seo'
|
||||
|
||||
import { Section } from '../components/shared'
|
||||
import {
|
||||
SectionHeading,
|
||||
SectionDesc,
|
||||
SolidButton
|
||||
} from '../components/shared/styledComponents'
|
||||
|
||||
import { FAQ } from '../styledComponents/faq'
|
||||
|
||||
type DataProps = {
|
||||
site: {
|
||||
meta: {
|
||||
title: string
|
||||
description: string
|
||||
social: { linkedin: string }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const Faq: React.FC<PageProps<DataProps>> = ({ data, location }) => {
|
||||
return (
|
||||
<Layout
|
||||
location={location}
|
||||
heading="Data Controller – FAQ"
|
||||
desc="Most common user questions and answers regarding the Data Controller"
|
||||
>
|
||||
<Seo title="FAQ" />
|
||||
<Section color="black" bgColor="white">
|
||||
<div className="row">
|
||||
<div className="col-md-8">
|
||||
<FAQ
|
||||
question="DOES THE DATA CONTROLLER ENABLE USERS TO CHANGE THE DATA MODEL (DATA STRUCTURE)?"
|
||||
answer="A: No. This is not (and will not be) part of the feature set. Users can submit changes to data, but they cannot affect the number or type of columns in a table."
|
||||
></FAQ>
|
||||
<FAQ
|
||||
question="Will this tool allow tables to be updated in my preferred database?"
|
||||
answer="A: If you have a SAS connector (ACCESS engine) then the answer is likely yes. However, if your tables have database specific features (eg auto-generated keys) or non-standard column types (eg images or BLOBS) then the table may not be compatible. Please contact us for more information, as there are likely to be workarounds."
|
||||
></FAQ>
|
||||
<FAQ
|
||||
question="Will this tool work on tables with Retained / Surrogate keys?"
|
||||
answer="A: Yes, just define your business / natural key in the RK_UNDERLYING field of the configuration table."
|
||||
></FAQ>
|
||||
</div>
|
||||
</div>
|
||||
</Section>
|
||||
<Section>
|
||||
<div className="row">
|
||||
<div className="offset-md-3 col-md-6">
|
||||
<SectionDesc opacity="1">
|
||||
For any further information regarding Data Controller please
|
||||
reference our <Link to="/contact/">Contact page</Link> and send us
|
||||
your questions. Our team will review it and follow up within 24
|
||||
hours.
|
||||
</SectionDesc>
|
||||
</div>
|
||||
</div>
|
||||
</Section>
|
||||
<Section color="black" bgColor="white" bottomArrow={false}>
|
||||
<Link
|
||||
to="/contact/"
|
||||
className="d-flex justify-content-center"
|
||||
style={{
|
||||
textDecoration: 'none'
|
||||
}}
|
||||
>
|
||||
<SolidButton theme="dark">Contact Us</SolidButton>
|
||||
</Link>
|
||||
</Section>
|
||||
</Layout>
|
||||
)
|
||||
}
|
||||
|
||||
export default Faq
|
272
src/pages/index.tsx
Normal file
@ -0,0 +1,272 @@
|
||||
import { Link, graphql } from 'gatsby'
|
||||
import React from 'react'
|
||||
import Gallery from '@browniebroke/gatsby-image-gallery'
|
||||
import { IGatsbyImageData } from 'gatsby-plugin-image'
|
||||
|
||||
import Layout from '../components/layout'
|
||||
import Seo from '../components/seo'
|
||||
|
||||
import { Section, ScheduleDemo } from '../components/shared'
|
||||
import {
|
||||
SectionHeading,
|
||||
SectionDesc
|
||||
} from '../components/shared/styledComponents'
|
||||
import {
|
||||
Art,
|
||||
Reason,
|
||||
CenteredText,
|
||||
Feature,
|
||||
ThumbnailWrapper
|
||||
} from '../styledComponents'
|
||||
|
||||
import leftArt from '../images/home_illustration2.png'
|
||||
import rightArt from '../images/home_illustration1.png'
|
||||
import dcSoftwareArt from '../images/dc-software.png'
|
||||
|
||||
import { AiOutlineClockCircle, AiFillLock } from 'react-icons/ai'
|
||||
import { TiTick } from 'react-icons/ti'
|
||||
import { HiOutlineRefresh } from 'react-icons/hi'
|
||||
import { MdHistory } from 'react-icons/md'
|
||||
import { BsCode } from 'react-icons/bs'
|
||||
import { BiNetworkChart } from 'react-icons/bi'
|
||||
|
||||
import dcDesign from '../images/data-controller-design.png'
|
||||
|
||||
interface ImageSharpEdge {
|
||||
node: {
|
||||
childImageSharp: {
|
||||
thumb: IGatsbyImageData
|
||||
full: IGatsbyImageData
|
||||
meta: {
|
||||
originalName: string
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
interface PageProps {
|
||||
data: {
|
||||
images: {
|
||||
edges: ImageSharpEdge[]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const Home: React.FC<PageProps> = ({ 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
|
||||
}))
|
||||
|
||||
return (
|
||||
<Layout
|
||||
location={location}
|
||||
heading="Flexible and Secure Data Modification"
|
||||
desc="Data Controller for SAS® enables business users to update IT owned source tables, secured by review / approve workflow and full audit trail."
|
||||
>
|
||||
<Seo />
|
||||
<Section color="black" bgColor="white">
|
||||
<div className="row">
|
||||
<div className="col-md-3">
|
||||
<Art src={leftArt} info="Business Owner" />
|
||||
</div>
|
||||
<div className="col-md-6">
|
||||
<SectionHeading>Who is Data Controller for?</SectionHeading>
|
||||
<SectionDesc>
|
||||
Data Controller is designed to provide business users with a tool
|
||||
which can be included in their overall armoury for regulatory
|
||||
reporting.
|
||||
</SectionDesc>
|
||||
<SectionDesc>
|
||||
Having worked in data-intensive SAS environments for many years,
|
||||
the founders of Data Controller experienced first hand the
|
||||
inefficient practices that reporting teams had to endure when
|
||||
changes needed to be made to the data and source system access
|
||||
wasn’t possible.
|
||||
</SectionDesc>
|
||||
<SectionDesc>
|
||||
It was clear to see that users needed the ability to self-serve
|
||||
their data updates. That data owners should have the chance to
|
||||
review and approve those updates. And that all changes to data
|
||||
should be safe and secure, as well as tracked and visible for
|
||||
audit reasons.
|
||||
</SectionDesc>
|
||||
<SectionDesc>
|
||||
The Data Controller is useful for any Enterprise that needs to
|
||||
perform manual data uploads into their preferred database, in
|
||||
real-time, with full validation, approval, security, and control.
|
||||
</SectionDesc>
|
||||
</div>
|
||||
<div className="col-md-3">
|
||||
<Art src={rightArt} info="Clinical Research Data" />
|
||||
</div>
|
||||
</div>
|
||||
</Section>
|
||||
<Section color="black" bgColor="white">
|
||||
<div className="row">
|
||||
<div className="offset-md-2 col-md-8">
|
||||
<SectionDesc>Devoted to</SectionDesc>
|
||||
<SectionHeading>SAS® Users, Admins and Developers</SectionHeading>
|
||||
<SectionDesc>
|
||||
Most (if not all) SAS users have the basic need to load, modify or
|
||||
delete data in SAS. They can typically achieve this by:
|
||||
</SectionDesc>
|
||||
</div>
|
||||
</div>
|
||||
<br />
|
||||
<br />
|
||||
<br />
|
||||
<div className="row">
|
||||
<div className="col-md-4">
|
||||
<Reason text="Placing files on network drives ready for a batch program to load them" />
|
||||
<Reason text="Updating data directly using SAS® code" />
|
||||
<Reason text="Opening a dataset in Enterprise Guide and changing a value" />
|
||||
<Reason text="Asking a DBA to run a script update" />
|
||||
</div>
|
||||
<div className="col-md-4">
|
||||
<CenteredText>
|
||||
The approaches mentioned are less than ideal for a number of
|
||||
reasons:
|
||||
</CenteredText>
|
||||
<Art src={dcSoftwareArt} info="Data Controller Software" />
|
||||
</div>
|
||||
<div className="col-md-4">
|
||||
<Reason
|
||||
text="Every new piece of data requires a new job or program to be created to load it"
|
||||
bgColor="red"
|
||||
/>
|
||||
<Reason
|
||||
text="END USERS NEED TO BE GRANTED MODIFY ACCESS TO SENSITIVE DATA LOCATIONS"
|
||||
bgColor="red"
|
||||
/>
|
||||
<Reason text="High risk of locking datasets" bgColor="red" />
|
||||
<Reason
|
||||
text="Need for change requests and delays to the process"
|
||||
bgColor="red"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<br />
|
||||
<br />
|
||||
<br />
|
||||
<div className="row">
|
||||
<div className="offset-md-2 col-md-8">
|
||||
<SectionHeading>Data Controller Features</SectionHeading>
|
||||
<SectionDesc>
|
||||
Our goal with Data Controller is to focus on great user experience
|
||||
and auditor satisfaction.
|
||||
<br />
|
||||
Our software saves years of development and testing, as well as
|
||||
avoiding a ‘black box’ custom build.
|
||||
<br />
|
||||
It is a SAS® Powered Web Application with the following features:
|
||||
</SectionDesc>
|
||||
</div>
|
||||
</div>
|
||||
<br />
|
||||
<br />
|
||||
<br />
|
||||
<div className="row">
|
||||
<Feature
|
||||
title="Real-time"
|
||||
desc="Intra day, concurrent updates are managed using a lock table and queuing mechanism. Updates are aborted if another user has changed the table since the approval diff was generated."
|
||||
Icon={AiOutlineClockCircle}
|
||||
className="col-sm-6 col-md-6 col-xl-4"
|
||||
/>
|
||||
<Feature
|
||||
title="Secure"
|
||||
desc="The existing SASLogon framework is used to manage Authentication & Authorisation. End users do not require direct access to the target tables."
|
||||
Icon={AiFillLock}
|
||||
className="col-sm-6 col-md-6 col-xl-4"
|
||||
/>
|
||||
<Feature
|
||||
title="Approval Mechanism"
|
||||
desc="All data changes require 1 or more approvals before updating the table. The approver sees only the changes that will be applied to the target (new/deleted/changed rows)."
|
||||
Icon={TiTick}
|
||||
className="col-sm-6 col-md-6 col-xl-4"
|
||||
/>
|
||||
<Feature
|
||||
title="Loading Tables of Different Types"
|
||||
desc="Using SAS® libname engines, you can securely update tables in your preferred database. Data Controller supports Retained keys, SCD2 loads, Bitemporal, and composite primary keys."
|
||||
Icon={HiOutlineRefresh}
|
||||
className="col-sm-6 col-md-6 col-xl-3"
|
||||
/>
|
||||
<Feature
|
||||
title="Full Audit History"
|
||||
desc="You are able to track every change of data, including who made the change, when, why, and what the actual change was. You are able to access this feature on the History page."
|
||||
Icon={MdHistory}
|
||||
className="col-sm-6 col-md-4 col-xl-3"
|
||||
/>
|
||||
<Feature
|
||||
title="Zero Code"
|
||||
desc="Adding new tables is a matter of configuration, which can perform entirely within the tool itself. No need to define column types, lengths etc – these are dynamically figured out at runtime."
|
||||
Icon={BsCode}
|
||||
className="col-sm-6 col-md-4 col-xl-3"
|
||||
/>
|
||||
<Feature
|
||||
title="Workflow"
|
||||
desc="You are able to configure “Hook scripts” to execute before and after each action. So you could run a data quality check after uploading a mapping table or run a model after changing a parameter."
|
||||
Icon={BiNetworkChart}
|
||||
className="col-sm-6 col-md-4 col-xl-3"
|
||||
/>
|
||||
</div>
|
||||
</Section>
|
||||
<Section color="black" bgColor="#f8f8f8">
|
||||
<div className="row">
|
||||
<div className="offset-md-2 col-md-8">
|
||||
<SectionDesc>Take a look at</SectionDesc>
|
||||
<SectionHeading>Data Controller Design</SectionHeading>
|
||||
<SectionDesc>
|
||||
We designed Data Controller to work on multiple devices and
|
||||
different screen types. This, combined with SAS® scalability and
|
||||
security, gives you full flexibility and location independence
|
||||
when managing your data.
|
||||
</SectionDesc>
|
||||
<Art src={dcDesign} info="Data Controller Design" />
|
||||
</div>
|
||||
</div>
|
||||
</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>
|
||||
)}
|
||||
/>
|
||||
</Section>
|
||||
<ScheduleDemo />
|
||||
</Layout>
|
||||
)
|
||||
}
|
||||
|
||||
export default Home
|
||||
|
||||
export const pageQuery = graphql`
|
||||
query ImagesForGallery {
|
||||
images: allFile(
|
||||
filter: { relativeDirectory: { eq: "gallery" } }
|
||||
sort: { fields: name }
|
||||
) {
|
||||
edges {
|
||||
node {
|
||||
childImageSharp {
|
||||
thumb: gatsbyImageData(
|
||||
width: 300
|
||||
height: 270
|
||||
placeholder: BLURRED
|
||||
)
|
||||
full: gatsbyImageData(layout: FULL_WIDTH)
|
||||
meta: fixed {
|
||||
originalName
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`
|
54
src/pages/{MarkdownRemark.frontmatter__slug}.tsx
Normal file
@ -0,0 +1,54 @@
|
||||
import React from 'react'
|
||||
import { graphql } from 'gatsby'
|
||||
import styled from 'styled-components'
|
||||
|
||||
import Layout from '../components/layout'
|
||||
import Seo from '../components/seo'
|
||||
|
||||
import { Section } from '../components/shared'
|
||||
|
||||
const StyledMarkdown = styled.div`
|
||||
color: rgb(102, 102, 102);
|
||||
h2,
|
||||
h3,
|
||||
h4,
|
||||
strong {
|
||||
color: rgb(34, 34, 34);
|
||||
}
|
||||
h2 {
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 1px;
|
||||
font-family: 'Montserrat', 'HelveticaNeue', 'Helvetica Neue', Helvetica,
|
||||
Arial, sans-serif;
|
||||
}
|
||||
`
|
||||
|
||||
export default function Template({ data, location }) {
|
||||
const { frontmatter, html } = data?.markdownRemark
|
||||
return (
|
||||
<Layout
|
||||
location={location}
|
||||
heading={frontmatter.heading}
|
||||
desc={frontmatter.desc}
|
||||
>
|
||||
<Seo title={frontmatter.title} />
|
||||
<Section color="black" bgColor="white" bottomArrow={false}>
|
||||
<StyledMarkdown dangerouslySetInnerHTML={{ __html: html }} />
|
||||
</Section>
|
||||
</Layout>
|
||||
)
|
||||
}
|
||||
|
||||
export const pageQuery = graphql`
|
||||
query ($id: String!) {
|
||||
markdownRemark(id: { eq: $id }) {
|
||||
html
|
||||
frontmatter {
|
||||
slug
|
||||
title
|
||||
heading
|
||||
desc
|
||||
}
|
||||
}
|
||||
}
|
||||
`
|
28
src/styledComponents/about.js
Normal file
@ -0,0 +1,28 @@
|
||||
import styled from 'styled-components'
|
||||
|
||||
export const Card = styled.a`
|
||||
color: black;
|
||||
text-decoration: none;
|
||||
overflow: hidden;
|
||||
margin-bottom: 20px;
|
||||
min-height: 255px;
|
||||
`
|
||||
|
||||
export const CardImg = styled.img`
|
||||
margin: -50px 0;
|
||||
`
|
||||
|
||||
export const CardBody = styled.div`
|
||||
background: white;
|
||||
`
|
||||
|
||||
export const FeaturedImg = styled.img`
|
||||
display: block;
|
||||
margin: 20px auto;
|
||||
`
|
||||
|
||||
export const FeaturedDesc = styled.p`
|
||||
color: rgb(244, 244, 244);
|
||||
font-size: 0.8rem;
|
||||
text-align: center;
|
||||
`
|
8
src/styledComponents/blog.js
Normal file
@ -0,0 +1,8 @@
|
||||
import styled from 'styled-components'
|
||||
|
||||
export const StyledHeading = styled.h5`
|
||||
font-family: 'Montserrat', 'HelveticaNeue', 'Helvetica Neue', Helvetica, Arial,
|
||||
sans-serif;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 1px;
|
||||
`
|
10
src/styledComponents/contact.css
Normal file
@ -0,0 +1,10 @@
|
||||
.contactFormStyles {
|
||||
border-color: #e1e1e1;
|
||||
background-color: #f8f8f8;
|
||||
color: #919191;
|
||||
}
|
||||
|
||||
.contactFormStyles:focus {
|
||||
background-color: #f8f8f8;
|
||||
box-shadow: none;
|
||||
}
|
17
src/styledComponents/contact.js
Normal file
@ -0,0 +1,17 @@
|
||||
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%;
|
||||
`
|
46
src/styledComponents/faq.js
Normal file
@ -0,0 +1,46 @@
|
||||
import React from 'react'
|
||||
import styled from 'styled-components'
|
||||
|
||||
import { FcFaq } from 'react-icons/fc'
|
||||
|
||||
const StyledFaq = styled.div`
|
||||
margin-bottom: 20px;
|
||||
`
|
||||
|
||||
const StyledQuestion = styled.h5`
|
||||
color: #222;
|
||||
font-family: 'Montserrat', 'HelveticaNeue', 'Helvetica Neue', Helvetica, Arial,
|
||||
sans-serif;
|
||||
text-transform: uppercase;
|
||||
`
|
||||
const StyledAnswer = styled.p`
|
||||
color: #919191;
|
||||
`
|
||||
|
||||
const IconHolderStyled = styled.div`
|
||||
display: inline-block;
|
||||
border: 1px solid #314351;
|
||||
background-color: #314351;
|
||||
border-radius: 50px;
|
||||
padding: 15px 17px;
|
||||
vertical-align: top;
|
||||
`
|
||||
|
||||
const FaqDetails = styled.div`
|
||||
display: inline-block;
|
||||
width: calc(100% - 80px);
|
||||
padding-left: 20px;
|
||||
margin-bottom: 20px;
|
||||
`
|
||||
|
||||
export const FAQ = ({ question, answer }) => (
|
||||
<StyledFaq>
|
||||
<IconHolderStyled>
|
||||
<FcFaq size={40} />
|
||||
</IconHolderStyled>
|
||||
<FaqDetails>
|
||||
<StyledQuestion>{question}</StyledQuestion>
|
||||
<StyledAnswer>{answer}</StyledAnswer>
|
||||
</FaqDetails>
|
||||
</StyledFaq>
|
||||
)
|
116
src/styledComponents/index.js
Normal file
@ -0,0 +1,116 @@
|
||||
import React from 'react'
|
||||
import styled from 'styled-components'
|
||||
import { ImArrowRight } from 'react-icons/im'
|
||||
|
||||
export const CenteredText = styled.p`
|
||||
text-align: center;
|
||||
max-width: 300px;
|
||||
display: block;
|
||||
margin: 0 auto;
|
||||
font-size: 1.2rem;
|
||||
line-height: 1;
|
||||
font-family: 'Montserrat', 'HelveticaNeue', 'Helvetica Neue', Helvetica, Arial,
|
||||
sans-serif;
|
||||
`
|
||||
export const Art = styled.img.attrs((props) => ({
|
||||
alt: props.info || '',
|
||||
title: props.info || ''
|
||||
}))`
|
||||
max-width: 100%;
|
||||
display: block;
|
||||
margin: 0 auto;
|
||||
padding: 20px;
|
||||
`
|
||||
|
||||
const ArrowHolder = styled.div`
|
||||
display: inline-block;
|
||||
border-radius: 50px;
|
||||
padding: 15px 17px;
|
||||
color: white;
|
||||
background-color: ${(props) =>
|
||||
props.bgColor === 'red' ? '#ed1c1c' : '#90c445'};
|
||||
`
|
||||
const RightArrow = ({ bgColor = '' }) => (
|
||||
<ArrowHolder bgColor={bgColor}>
|
||||
<ImArrowRight size={24} />
|
||||
</ArrowHolder>
|
||||
)
|
||||
const ReasonDesc = styled.p`
|
||||
display: inline-block;
|
||||
width: calc(100% - 60px);
|
||||
vertical-align: top;
|
||||
margin: 20px 0 0;
|
||||
min-height: 60px;
|
||||
line-height: 1;
|
||||
padding-left: 20px;
|
||||
text-transform: uppercase;
|
||||
`
|
||||
|
||||
export const Reason = ({ text, bgColor = '' }) => (
|
||||
<>
|
||||
<RightArrow bgColor={bgColor} />
|
||||
<ReasonDesc>{text}</ReasonDesc>
|
||||
</>
|
||||
)
|
||||
|
||||
const IconHolderStyled = styled.div`
|
||||
display: inline-block;
|
||||
border: 1px solid #314351;
|
||||
color: #314351;
|
||||
border-radius: 50px;
|
||||
padding: 15px 17px;
|
||||
vertical-align: top;
|
||||
`
|
||||
|
||||
const IconHolder = ({ Icon }) => {
|
||||
return (
|
||||
<IconHolderStyled>
|
||||
<Icon size={24} />
|
||||
</IconHolderStyled>
|
||||
)
|
||||
}
|
||||
|
||||
const FeatureDetails = styled.div`
|
||||
display: inline-block;
|
||||
width: calc(100% - 60px);
|
||||
padding-left: 20px;
|
||||
margin-bottom: 20px;
|
||||
`
|
||||
const FeatureTitle = styled.h5`
|
||||
font-family: 'Montserrat', 'HelveticaNeue', 'Helvetica Neue', Helvetica, Arial,
|
||||
sans-serif;
|
||||
text-transform: uppercase;
|
||||
`
|
||||
const FeatureDesc = styled.p`
|
||||
opacity: 0.5;
|
||||
font-size: 0.9rem;
|
||||
`
|
||||
|
||||
export const Feature = ({ title, desc, Icon, className }) => (
|
||||
<div className={className ?? 'col-md-4'}>
|
||||
<IconHolder Icon={Icon} />
|
||||
<FeatureDetails>
|
||||
<FeatureTitle>{title}</FeatureTitle>
|
||||
<FeatureDesc>{desc}</FeatureDesc>
|
||||
</FeatureDetails>
|
||||
</div>
|
||||
)
|
||||
|
||||
export const ThumbnailWrapper = styled.div`
|
||||
-webkit-box-flex: 0;
|
||||
flex: 0 0 33.3333%;
|
||||
-webkit-box-align: center;
|
||||
align-items: center;
|
||||
-webkit-box-pack: center;
|
||||
justify-content: center;
|
||||
display: flex;
|
||||
max-width: 33.3333%;
|
||||
@media (min-width: 576px) {
|
||||
flex-basis: 25%;
|
||||
max-width: 25%;
|
||||
}
|
||||
cursor: pointer;
|
||||
&:hover {
|
||||
opacity: 0.5;
|
||||
}
|
||||
`
|
118
src/templates/blog-list.tsx
Normal file
@ -0,0 +1,118 @@
|
||||
import { Link, graphql } from 'gatsby'
|
||||
import React from 'react'
|
||||
import kebabCase from 'lodash/kebabCase'
|
||||
import styled from 'styled-components'
|
||||
|
||||
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'
|
||||
|
||||
const BlogListTemplate = ({ data, location, pageContext }) => {
|
||||
const posts = data.remark.posts
|
||||
|
||||
const iniPath =
|
||||
pageContext.page == 'index'
|
||||
? `/blog/`
|
||||
: pageContext.page == 'year'
|
||||
? `/${pageContext.year}/`
|
||||
: pageContext.page == 'category'
|
||||
? `/category/${kebabCase(pageContext.tag)}/`
|
||||
: null
|
||||
|
||||
const pageInfo = `Page ${pageContext.currentPage} of ${pageContext.numPages}`
|
||||
Array.from({ length: 5 }, (v, k) => k + 1)
|
||||
const paginationJSX = Array.from(
|
||||
{ length: pageContext.numPages },
|
||||
(_, i) => i + 1
|
||||
).map((pageIndex) => {
|
||||
const link = pageIndex === 1 ? iniPath : `${iniPath}page/${pageIndex}`
|
||||
return (
|
||||
<Link
|
||||
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 Knowledge Base"
|
||||
desc="A section dedicated to news, updates and educational pieces."
|
||||
>
|
||||
<Seo title="Blog" />
|
||||
<Section color="black" bgColor="white" bottomArrow={false}>
|
||||
<div className="row">
|
||||
<div className="col-md-7">
|
||||
<div className="row">
|
||||
{posts.map((data, i) => (
|
||||
<Post key={i} post={data.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} />
|
||||
</div>
|
||||
</div>
|
||||
</Section>
|
||||
</Layout>
|
||||
)
|
||||
}
|
||||
|
||||
export default BlogListTemplate
|
||||
|
||||
export const pageQuery = graphql`
|
||||
query BlogListQuery(
|
||||
$filter: MarkdownRemarkFilterInput!
|
||||
$skip: Int!
|
||||
$limit: Int!
|
||||
) {
|
||||
remark: allMarkdownRemark(
|
||||
filter: $filter
|
||||
limit: $limit
|
||||
skip: $skip
|
||||
sort: { fields: [frontmatter___date], order: DESC }
|
||||
) {
|
||||
posts: edges {
|
||||
post: node {
|
||||
html
|
||||
fields {
|
||||
slug
|
||||
}
|
||||
frontmatter {
|
||||
title
|
||||
date(formatString: "MMMM DD, YYYY")
|
||||
author
|
||||
authorLink
|
||||
previewImg {
|
||||
childImageSharp {
|
||||
gatsbyImageData(layout: CONSTRAINED)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
// tags
|
63
src/templates/blog-post.tsx
Normal file
@ -0,0 +1,63 @@
|
||||
import React from 'react'
|
||||
import { Link, graphql } 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'
|
||||
|
||||
const BlogPostTemplate = ({ 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} />
|
||||
</div>
|
||||
</div>
|
||||
</Section>
|
||||
</Layout>
|
||||
)
|
||||
}
|
||||
|
||||
export default BlogPostTemplate
|
||||
|
||||
export const pageQuery = graphql`
|
||||
query PostByPath($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
|
||||
}
|
||||
}
|
||||
}
|
||||
`
|
88
src/templates/blog-search.tsx
Normal file
@ -0,0 +1,88 @@
|
||||
import { Link, graphql, useStaticQuery } from 'gatsby'
|
||||
import React from 'react'
|
||||
import kebabCase from 'lodash/kebabCase'
|
||||
import { useFlexSearch } from 'react-use-flexsearch'
|
||||
import styled from 'styled-components'
|
||||
|
||||
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'
|
||||
|
||||
const BlogSearchTemplate = ({ location, pageContext }) => {
|
||||
const queryData = useStaticQuery(graphql`
|
||||
query {
|
||||
localSearchBlog {
|
||||
index
|
||||
store
|
||||
}
|
||||
}
|
||||
`)
|
||||
|
||||
const params = new URLSearchParams(location.search.substring(1))
|
||||
const query = params.get('s')
|
||||
|
||||
const posts = useFlexSearch(
|
||||
query,
|
||||
queryData.localSearchBlog.index,
|
||||
queryData.localSearchBlog.store
|
||||
)
|
||||
const postsJSX = []
|
||||
|
||||
if (query) {
|
||||
posts.forEach((post, i) => {
|
||||
const _post = {
|
||||
id: post.id,
|
||||
html: post.html,
|
||||
fields: {
|
||||
slug: post.slug
|
||||
},
|
||||
frontmatter: {
|
||||
title: post.title,
|
||||
date: post.date,
|
||||
previewImg: post.previewImg
|
||||
}
|
||||
}
|
||||
|
||||
postsJSX.push(<Post key={i} post={_post} />)
|
||||
})
|
||||
}
|
||||
return (
|
||||
<Layout
|
||||
location={location}
|
||||
heading="Data Controller’s Knowledge Base"
|
||||
desc="A section dedicated to news, updates and educational pieces."
|
||||
>
|
||||
<Seo title="Blog" />
|
||||
<Section color="black" bgColor="white" bottomArrow={false}>
|
||||
<div className="row">
|
||||
<div className="col-md-7">
|
||||
<div className="row">
|
||||
{query ? (
|
||||
<h1>
|
||||
{postsJSX.length} results found related to "{query}"
|
||||
</h1>
|
||||
) : (
|
||||
<h1>No Query Entered</h1>
|
||||
)}
|
||||
{postsJSX}
|
||||
</div>
|
||||
</div>
|
||||
<div className="col-md-5">
|
||||
<SideBar pageContext={pageContext} location={location} />
|
||||
</div>
|
||||
</div>
|
||||
</Section>
|
||||
</Layout>
|
||||
)
|
||||
}
|
||||
|
||||
export default BlogSearchTemplate
|
159
src/templates/post/index.tsx
Normal file
@ -0,0 +1,159 @@
|
||||
import React from 'react'
|
||||
import { Link } from 'gatsby'
|
||||
import kebabCase from 'lodash/kebabCase'
|
||||
|
||||
import { GatsbyImage } from 'gatsby-plugin-image'
|
||||
|
||||
import styled from 'styled-components'
|
||||
|
||||
export const StyledLink = styled((props) => <Link {...props} />)`
|
||||
text-decoration: none;
|
||||
color: black;
|
||||
&:hover {
|
||||
opacity: 0.8;
|
||||
color: black;
|
||||
}
|
||||
`
|
||||
|
||||
const StyledTitle = styled.h5`
|
||||
font-family: 'Montserrat', 'HelveticaNeue', 'Helvetica Neue', Helvetica, Arial,
|
||||
sans-serif;
|
||||
margin-bottom: 0;
|
||||
`
|
||||
|
||||
const StyledDate = styled.span`
|
||||
opacity: 0.5;
|
||||
font-size: 0.8rem;
|
||||
a {
|
||||
color: black;
|
||||
text-decoration: none;
|
||||
&:hover {
|
||||
text-decoration: underline;
|
||||
color: black;
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
`
|
||||
const StyledDesc = styled.p`
|
||||
margin-top: 10px;
|
||||
opacity: 0.7;
|
||||
font-size: 0.8rem;
|
||||
margin-bottom: 100px;
|
||||
`
|
||||
|
||||
const StyledContent = styled.div`
|
||||
padding: 15px 0;
|
||||
color: #666666;
|
||||
|
||||
font-family: 'Montserrat', 'HelveticaNeue', 'Helvetica Neue', Helvetica, Arial,
|
||||
sans-serif;
|
||||
a {
|
||||
color: black;
|
||||
text-decoration: none;
|
||||
&:hover {
|
||||
text-decoration: underline;
|
||||
color: #666666;
|
||||
}
|
||||
}
|
||||
h1,
|
||||
h2,
|
||||
h3,
|
||||
h4,
|
||||
h5,
|
||||
h6 {
|
||||
color: #222222;
|
||||
}
|
||||
* + h2,
|
||||
* + h3 {
|
||||
margin: 2rem auto 0.8rem;
|
||||
}
|
||||
img {
|
||||
float: right;
|
||||
max-width: 100%;
|
||||
margin: 10px;
|
||||
&.alignleft {
|
||||
float: left;
|
||||
}
|
||||
&.aligncenter {
|
||||
float: none;
|
||||
max-width: 90%;
|
||||
display: block;
|
||||
margin: 10px auto;
|
||||
}
|
||||
}
|
||||
.imgHolder {
|
||||
padding: 5px;
|
||||
margin-left: 5px;
|
||||
float: right;
|
||||
text-align: center;
|
||||
border: 1px solid #e1e1e1;
|
||||
border-radius: 2px;
|
||||
font-style: italic;
|
||||
font-family: Georgia, 'Times New Roman';
|
||||
img {
|
||||
float: none;
|
||||
}
|
||||
> div {
|
||||
display: flex;
|
||||
span {
|
||||
flex-grow: 1;
|
||||
width: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
pre {
|
||||
background-image: linear-gradient(
|
||||
rgba(0, 0, 0, 0.05) 50%,
|
||||
transparent 50%,
|
||||
transparent
|
||||
);
|
||||
background-size: 100% 4em;
|
||||
border: 1px solid #e1e1e1;
|
||||
|
||||
padding: 2em;
|
||||
line-height: 2em;
|
||||
font-family: Monaco, 'Andale Mono', 'Courier New', Courier, monospace;
|
||||
}
|
||||
`
|
||||
|
||||
const Post = ({ post, location }) => {
|
||||
const tagsJSX = (post.frontmatter?.tags || []).map((tag, index) => (
|
||||
<span key={index}>
|
||||
{index > 0 && ', '}
|
||||
<Link to={`/category/${kebabCase(tag)}/`} rel="category tag">
|
||||
{tag}
|
||||
</Link>
|
||||
</span>
|
||||
))
|
||||
const authorJSX = post.frontmatter.authorLink ? (
|
||||
<span>
|
||||
<a href={post.frontmatter.authorLink} target="_blank" rel="noopener">
|
||||
{post.frontmatter.author}
|
||||
</a>
|
||||
</span>
|
||||
) : (
|
||||
<span>{post.frontmatter.author}</span>
|
||||
)
|
||||
return (
|
||||
<div>
|
||||
<GatsbyImage
|
||||
image={post.frontmatter.previewImg.childImageSharp.gatsbyImageData}
|
||||
style={{ width: '100%', marginBottom: '1rem' }}
|
||||
imgStyle={{ objectFit: 'contain' }}
|
||||
alt={post.frontmatter.title}
|
||||
/>
|
||||
|
||||
<StyledTitle>{post.frontmatter.title}</StyledTitle>
|
||||
<StyledDate>
|
||||
{post.frontmatter.date} / in {tagsJSX} / by {authorJSX}
|
||||
</StyledDate>
|
||||
<StyledContent
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: post.html
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default Post
|
54
src/templates/postpreview/index.tsx
Normal file
@ -0,0 +1,54 @@
|
||||
import React from 'react'
|
||||
import { Link } from 'gatsby'
|
||||
|
||||
import { GatsbyImage } from 'gatsby-plugin-image'
|
||||
|
||||
import styled from 'styled-components'
|
||||
|
||||
import getDescription from '../shared/getDescription'
|
||||
|
||||
export const StyledLink = styled((props) => <Link {...props} />)`
|
||||
text-decoration: none;
|
||||
color: black;
|
||||
&:hover {
|
||||
opacity: 0.8;
|
||||
color: black;
|
||||
}
|
||||
`
|
||||
|
||||
const StyledTitle = styled.h5`
|
||||
font-family: 'Montserrat', 'HelveticaNeue', 'Helvetica Neue', Helvetica, Arial,
|
||||
sans-serif;
|
||||
font-size: 1rem;
|
||||
margin-bottom: 0;
|
||||
`
|
||||
|
||||
const StyledDate = styled.span`
|
||||
opacity: 0.5;
|
||||
font-size: 0.8rem;
|
||||
`
|
||||
const StyledDesc = styled.p`
|
||||
margin-top: 10px;
|
||||
opacity: 0.7;
|
||||
font-size: 0.8rem;
|
||||
margin-bottom: 100px;
|
||||
`
|
||||
|
||||
const Post = ({ post }) => (
|
||||
<div className="col-md-6">
|
||||
<StyledLink to={post.fields.slug}>
|
||||
<GatsbyImage
|
||||
image={post.frontmatter.previewImg.childImageSharp.gatsbyImageData}
|
||||
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
|
13
src/templates/shared/getDescription.ts
Normal file
@ -0,0 +1,13 @@
|
||||
const extractContent = (s: string): string => {
|
||||
if (typeof document !== `undefined`) {
|
||||
const span = document.createElement('span')
|
||||
span.innerHTML = s
|
||||
return span.textContent || span.innerText
|
||||
}
|
||||
return s
|
||||
}
|
||||
const getDescription = (content: string): string => {
|
||||
return extractContent(content).substr(0, 100) + '...'
|
||||
}
|
||||
|
||||
export default getDescription
|
181
src/templates/sidebar.tsx
Normal file
@ -0,0 +1,181 @@
|
||||
import React, { useState, useEffect } 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 { StyledHeading } from '../styledComponents/blog'
|
||||
import { SolidButton } from '../components/shared/styledComponents'
|
||||
|
||||
const styles = css`
|
||||
color: #314351;
|
||||
opacity: 0.8;
|
||||
font:
|
||||
13px/1.65em 'HelveticaNeue',
|
||||
'Helvetica Neue',
|
||||
Helvetica,
|
||||
Arial,
|
||||
sans-serif;
|
||||
font-size: 0.9rem;
|
||||
`
|
||||
|
||||
const linkStyles = css`
|
||||
${styles}
|
||||
text-decoration: none;
|
||||
display: block;
|
||||
&:hover {
|
||||
color: #314351;
|
||||
opacity: 1;
|
||||
text-decoration: underline;
|
||||
}
|
||||
`
|
||||
const ArchiveLink = styled((props) => <Link {...props} />)`
|
||||
${linkStyles}
|
||||
`
|
||||
const RecentPostLink = styled((props) => <Link {...props} />)`
|
||||
${linkStyles}
|
||||
padding: 3px;
|
||||
border-bottom: 3px solid #e1e1e1;
|
||||
font-style: italic;
|
||||
&:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
`
|
||||
const SideBarSection = styled.div`
|
||||
margin-bottom: 20px;
|
||||
`
|
||||
|
||||
const StyledInput = styled.input`
|
||||
border-color: #e1e1e1;
|
||||
background-color: #f8f8f8;
|
||||
color: #919191;
|
||||
&:focus {
|
||||
background-color: #f8f8f8;
|
||||
box-shadow: none;
|
||||
}
|
||||
`
|
||||
|
||||
const StyledPara = styled.p`
|
||||
${styles}
|
||||
`
|
||||
|
||||
const Archives = ({ archives }) => (
|
||||
<>
|
||||
{Object.keys(archives)
|
||||
.sort()
|
||||
.reverse()
|
||||
.map((year, index) => (
|
||||
<ArchiveLink key={year} to={`/${year}/`}>
|
||||
{year} ({archives[year]})
|
||||
</ArchiveLink>
|
||||
))}
|
||||
</>
|
||||
)
|
||||
|
||||
const Categories = ({ tags }) => (
|
||||
<>
|
||||
{tags.map((tag, i) => (
|
||||
<ArchiveLink key={i} to={`/category/${kebabCase(tag.name)}/`}>
|
||||
{tag.name} ({tag.totalCount})
|
||||
</ArchiveLink>
|
||||
))}
|
||||
</>
|
||||
)
|
||||
|
||||
const RecentPosts = ({ posts }) => (
|
||||
<>
|
||||
{posts.map((post) => (
|
||||
<RecentPostLink key={post.slug} to={post.slug}>
|
||||
{post.title}
|
||||
</RecentPostLink>
|
||||
))}
|
||||
</>
|
||||
)
|
||||
|
||||
export const SideBar = ({ pageContext, location, notFoundPage = false }) => {
|
||||
const params = new URLSearchParams(location.search.substring(1))
|
||||
const queryUrl = params?.get('s') || ''
|
||||
const [query, setQuery] = useState(queryUrl)
|
||||
useEffect(() => {
|
||||
setQuery(queryUrl)
|
||||
}, [queryUrl])
|
||||
|
||||
const handleSubmit = (event) => {
|
||||
event.preventDefault()
|
||||
navigate('/search?s=' + query)
|
||||
}
|
||||
|
||||
const _handleKeyDown = (event) => {
|
||||
if (event.key === 'Enter') {
|
||||
handleSubmit(event)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<SideBarSection>
|
||||
{notFoundPage && (
|
||||
<>
|
||||
<StyledPara>
|
||||
<b>Nothing Found</b>
|
||||
</StyledPara>
|
||||
<StyledPara>
|
||||
Sorry, the post you are looking for is not available. Maybe you
|
||||
want to perform a search?
|
||||
</StyledPara>
|
||||
</>
|
||||
)}
|
||||
<div className="input-group mb-3">
|
||||
<StyledInput
|
||||
type="text"
|
||||
value={query}
|
||||
onChange={(e) => setQuery(e.target.value)}
|
||||
onKeyDown={(e) => _handleKeyDown(e)}
|
||||
className="form-control"
|
||||
placeholder="Search"
|
||||
aria-label="Search"
|
||||
aria-describedby="button-addon2"
|
||||
/>
|
||||
<SolidButton theme="dark" onClick={handleSubmit}>
|
||||
Search
|
||||
</SolidButton>
|
||||
<form onSubmit={handleSubmit}></form>
|
||||
</div>
|
||||
{notFoundPage && (
|
||||
<>
|
||||
<StyledPara>
|
||||
For best search results, mind the following suggestions:
|
||||
</StyledPara>
|
||||
<StyledPara>
|
||||
<ul>
|
||||
<li>Always double check your spelling.</li>
|
||||
<li>
|
||||
Try similar keywords, for example: tablet instead of laptop.
|
||||
</li>
|
||||
<li>Try using more than one keyword.</li>
|
||||
</ul>
|
||||
</StyledPara>
|
||||
</>
|
||||
)}
|
||||
<StyledHeading>
|
||||
{notFoundPage
|
||||
? 'Feel like browsing some posts instead?'
|
||||
: 'Recent Posts'}
|
||||
</StyledHeading>
|
||||
<RecentPosts posts={pageContext.recentPosts} />
|
||||
</SideBarSection>
|
||||
{!notFoundPage && (
|
||||
<>
|
||||
<SideBarSection>
|
||||
<StyledHeading>Archives</StyledHeading>
|
||||
<Archives archives={pageContext.archives} />
|
||||
</SideBarSection>
|
||||
<SideBarSection>
|
||||
<StyledHeading>Categories</StyledHeading>
|
||||
<Categories tags={pageContext.tags} />
|
||||
</SideBarSection>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|