(and also cool headless SilverStripe things)
(even by the recently-expanded definition)
(for linting, automated tests, and deployment)
(slightly over-engineered though)
yarn init -y
yarn add next react react-dom
{
"name": "@indiefin/website",
"version": "1.0.0",
"private": true,
"scripts": {
"dev": "next",
"build": "next build",
"start": "next start",
"docker:start": "next start -p 8080 -H 0.0.0.0",
...
},
"dependencies": {
...
},
"devDependencies": {
...
},
"resolutions": {
...
},
"workspaces": [
...
]
}
// package.json
const HomePage = () => {
return <div>A new website!</div>
}
export default HomePage
// pages/index.js
(heyday/silverstripe-menumanager)
(silverstripe/blog)
(fork of silverstripers/markdown)
(that's just like...a data object, man)
(because no API module is good enough)
probably because we don't understand how they work
import axios from 'axios'
import https from 'https'
const requestCms = axios.create({
baseURL: process.env.HOST_CMS,
headers: { Accept: 'application/json;charset=UTF-8' },
httpsAgent: process.env.HOST_CMS.startsWith('https')
? new https.Agent({ rejectUnauthorized: false })
: undefined,
})
<Product
title="Funeral Cover"
description="Funeral Cover that doubles after 2 years at no extra cost."
/>
const Product = ({ title, description }) => {
return (
<div className="product-card">
<div className="product-card-title">{title}</div>
<div className="product-card-description">{description}</div>
</div>
)
}
export { Product }
import { useEffect, useState } from 'react'
const Product = ({ title, description }) => {
const [dynamicTitle, setDynamicTitle] = useState(title)
const [dyanmicDescription, setDynamicDescription] = useState(description)
useEffect(() => {
fetch(`http://localhost:3000/api/reusable?name=${title}`)
.then((response) => response.json())
.then(({ content }) => setDynamicTitle(content))
}, [title])
useEffect(() => {
fetch(`http://localhost:3000/api/reusable?name=${description}`)
.then((response) => response.json())
.then(({ content }) => setDynamicDescription(content))
}, [description])
return (
<div>
<div>{dynamicTitle}</div>
<div>{dyanmicDescription}</div>
</div>
)
}
export { Product }
return (
<ProductCard>
<ProductCardTitle>
<Reusable name={title} />
</ProductCardTitle>
<ProductCardDescription>
<Reusable name={description} />
</ProductCardDescription>
</ProductCard>
);
import { useEffect, useState } from 'react'
const Reusable = ({ name }) => {
const [content, setContent] = useState(null)
useEffect(() => {
fetch(`http://localhost:3000/api/reusable?name=${name}`)
.then((response) => response.json())
.then(({ content }) => setContent(content))
}, [name])
return content
}
export { Reusable }
import { createContext } from 'react'
const ReusableContext = createContext(undefined)
const DefaultOffReusableContextProvider = ({ children }) => (
<ReusableContext.Provider value={{ isReusable: false }}>
{children}
</ReusableContext.Provider>
)
const DefaultOnReusableContextProvider = ({ children }) => (
<ReusableContext.Provider value={{ isReusable: true }}>
{children}
</ReusableContext.Provider>
)
export {
ReusableContext,
DefaultOffReusableContextProvider,
DefaultOnReusableContextProvider,
}
import {
DefaultOffReusableContextProvider,
DefaultOnReusableContextProvider,
} from '../state'
import { Product, Reusable } from '../components'
const HomePage = () => {
return (
<DefaultOffReusableContextProvider>
<Product
title="Testing a new Product"
description="...with a lovely description!"
/>
<DefaultOnReusableContextProvider>
<Product
title="funeral-cover-title"
description="funeral-cover-description"
/>
</DefaultOnReusableContextProvider>
</DefaultOffReusableContextProvider>
)
}
export default HomePage
import { useContext } from 'react'
import { ReusableContext } from '../state'
import { Reusable } from '../components'
const Product = ({ title, description }) => {
const { isReusable } = useContext(ReusableContext)
return (
<ProductCard>
<ProductCardTitle>
{isReusable ? <Reusable name={title} /> : title}
</ProductCardTitle>
<ProductCardDescription>
{isReusable ? <Reusable name={description} /> : description}
</ProductCardDescription>
</ProductCard>
)
}
export { Product }
const valueOf = (value) => {
if (typeof value === 'function') {
return value()
}
if (value.startsWith('reusable:')) {
value = value.replace('reusable:', '')
value = <Reusable name={value} />
}
return value
}
import { valueOf as 🐮 } from '../helpers'
const Product = ({ title, description }) => {
return (
<ProductCard>
<ProductCardTitle>{🐮(title)}</ProductCardTitle>
<ProductCardDescription>{🐮(description)}</ProductCardDescription>
</ProductCard>
)
}
export { Product }
import { useEffect, useState } from 'react'
const queued = []
const BatchedReusable = ({ name }) => {
const [content, setContent] = useState(null)
queued.push({ name, setContent })
return content
}
const Fetcher = () => {
useEffect(() => {
fetchReusableContent()
return () => {
// maybe cancel these on unmount...
}
}, [])
return null
}
const fetchReusableContent = () => {
const joined = queued.map((queued) => queued.name).join(',')
fetch(`http://localhost:3000/api/reusable?names=${joined}`)
.then((response) => response.json())
.then((contents) => {
for (let {name, setContent} of queued) {
if (contents[name]) {
setContent(contents[name])
}
}
})
}
export {
BatchedReusable,
Fetcher,
}
import { Fetcher } from '../components'
const Layout = ({ children }) => {
return (
<div className="layout">
{children}
<Fetcher />
</div>
)
}
export { Layout }
import { useEffect, useReducer } from 'react'
const initial = {
posts: [],
loading: false,
viewing: undefined,
}
const reducer = (state, { type, payload }) => {
if (type === 'load') return {
...state,
loading: true,
}
if (type === 'loaded') return {
...state,
loading: false,
posts: payload,
}
if (type === 'view') return {
...state,
viewing: payload,
}
if (type === 'back') return {
...state,
viewing: undefined,
}
return state
}
import { useEffect, useReducer } from 'react'
// ...
const Blog = () => {
const [{ posts, loading, viewing }, dispatch] = useReducer(reducer, initial);
useEffect(() => {
dispatch({ type: 'load' })
fetch('https://localhost:3000/api/posts')
.then((response) => response.json())
.then(({ posts }) => dispatch({ type: 'loaded', payload: posts }))
}, [])
if (loading) {
return 'Loading...'
}
if (viewing) {
return <Post {...viewing} onBack={() => dispatch({ type: 'back' })} />
}
return <Posts posts={posts} onSelect={(post) => dispatch({ type: 'view', payload: post })} />
}
import { createContext } from 'react'
import { initial as articlesInitial, reducer as articlesReducer } from './articles'
import { initial as productsInitial, reducer as productsReducer } from './products'
export const initial = {
...articlesInitial,
...productsInitial,
}
export const reducer = (state, action) => {
state = articlesReducer(state, action)
state = productsReducer(state, action)
return state
}
export const context = createContext(undefined)
import { context as AppContext, initial, reducer } from '../reducers'
import { Router } from '../screens'
const AppProvider = ({ children }) => {
const [state, dispatch] = useReducer(reducer, initial)
return (
<AppContext.Provider value={[state, dispatch]}>
{children}
</AppContext.Provider>
)
}
const App = () => (
<AppProvider>
<Router />
</AppProvider>
)
export { App }
import React, { useEffect, useContext } from 'react'
import { context } from '../reducers'
import { load } from '../reducers/products'
import { Product } from '../components'
const ListProducts = ({ match }) => {
const [{ products }, dispatch] = useContext(context)
useEffect(() => {
dispatch(load())
}, [])
return (
<div>
{products.loading && <div>loading products...</div>}
{products.items.length ? (
<ol>
{products.items.map(product => (
<Product key={product.id} {...product} />
))}
</ol>
) : (
<div>no products</div>
)}
</div>
)
}
export { ListProducts }
namespace Indie\Models;
use SilverStripe\Blog\Model\Blog;
use SilverStripe\ORM\HiddenClass;
// hide this page type when creating new pages...
class HiddenBlog extends Blog implements HiddenClass
{
// hide the standard blog section page type...
private static $hide_ancestor = Blog::class;
}
namespace Indie\Models;
use SilverStripe\Blog\Model\Blog;
class LearnSection extends Blog
{
private static $table_name = 'IndieLearnSection';
private static $allowed_children = [LearnPost::class];
private static $description = 'Adds a learn to your website.';
public function canCreate($member = null, $context = [])
{
// can't be a child of another page...
if (isset($context['Parent'])) {
return false;
}
// only one learn (blog) section at a time...
return !LearnSection::get()->first();
}
}
import React from 'react'
import MDX from '@mdx-js/runtime'
const components = {
h1: props => <h1 className="larger-text" {...props} />,
}
const mdx = `
# Welcome, customer!
`
const HomePage = () => (
<MDX components={components}>
{mdx}
</MDX>
)
export async function getStaticPaths() {
const response = requestCms.get('/api/slugs/all/live')
const paths = response.data
const slugs = paths.data.map((url) => ({
params: { slug: url.split('/') }
}))
return { paths: slugs, fallback: true }
}
export async function getStaticProps({ params }) {
const { page } = await getPage({ ...params })
const source = await renderToString(page.content, { components })
return {
props: {
...data,
source,
},
revalidate: 30,
}
}
export async function getServerSideProps(req) {
const { page } = await getPage({ slug: req.query.slug })
const source = await renderToString(page.content, { components })
return {
props: {
...data,
source,
},
}
}