diff --git a/packages/freesewing.org/CHANGELOG.md b/packages/freesewing.org/CHANGELOG.md
deleted file mode 100644
index ad0c3844f55..00000000000
--- a/packages/freesewing.org/CHANGELOG.md
+++ /dev/null
@@ -1,9 +0,0 @@
-# Change log for: freesewing.org
-
-
-
-This is the **initial release**, and the start of this change log.
-
-> Prior to version 2, FreeSewing was not a JavaScript project.
-> As such, that history is out of scope for this change log.
-
diff --git a/packages/freesewing.org/components/footer.js b/packages/freesewing.org/components/footer.js
new file mode 100644
index 00000000000..59b0ca7fd3c
--- /dev/null
+++ b/packages/freesewing.org/components/footer.js
@@ -0,0 +1,166 @@
+import NextLink from 'next/link'
+import Logo from 'shared/components/logos/freesewing.js'
+import contributors from 'site/prebuild/allcontributors.js'
+import patrons from 'site/prebuild/patrons.js'
+import OsiLogo from 'shared/components/logos/osi.js'
+import CreativeCommonsLogo from 'shared/components/logos/cc.js'
+import CcByLogo from 'shared/components/logos/cc-by.js'
+
+const Link = ({ href, txt }) => (
+
+ {txt}
+
+)
+const link = "text-secondary font-bold hover:pointer hover:underline px-1"
+
+const social = {
+ Discord: 'https://discord.freesewing.org/',
+ Instagram: 'https://instagram.com/freesewing_org',
+ Facebook: 'https://www.facebook.com/groups/627769821272714/',
+ Github: 'https://github.com/freesewing',
+ Reddit: 'https://www.reddit.com/r/freesewing/',
+ Twitter: 'https://twitter.com/freesewing_org',
+}
+
+const Footer = ({ app }) => (
+
+
+
+
+
+
+
+
+
Where can I turn for help?
+
+
+ Our Discord server is
+ the best place to ask questions and get help. It's where our community hangs out
+ so you'll get the fastest response and might even make a few new friends along the way.
+
+
+ You can also reach out on Twitter or create an issue on Github if Discord is not your jam.
+
+
+
+
+
Social Media
+
+
+ {Object.keys(social).map(item => )}
+
+
+
+
+
+
FreeSewing
+
+ Come for the sewing patterns
+
+ Stay for the community
+
+
+
+
+ FreeSewing is made by these wonderful contributors
+
+
+ {contributors.map(person => (
+
+
+
+ ))}
+
+
+
+ FreeSewing is supported by these generous patrons
+
+
+ {patrons.map(person => (
+
+
+
+ ))}
+
+
+
+ FreeSewing is hosted by these awesome companies
+
+
+
+
+)
+
+export default Footer
+
diff --git a/packages/freesewing.org/components/header.js b/packages/freesewing.org/components/header.js
new file mode 100644
index 00000000000..3d799e5fe76
--- /dev/null
+++ b/packages/freesewing.org/components/header.js
@@ -0,0 +1,109 @@
+import { useState, useEffect } from 'react'
+import Logo from 'shared/components/logos/freesewing.js'
+import Link from 'next/link'
+import ThemePicker from 'shared/components/theme-picker.js'
+import CloseIcon from 'shared/components/icons/close.js'
+import MenuIcon from 'shared/components/icons/menu.js'
+import SearchIcon from 'shared/components/icons/search.js'
+
+const Right = props => (
+
+
+
+)
+const Left = props => (
+
+
+
+)
+
+const Header = ({ app, setSearch }) => {
+
+ const [prevScrollPos, setPrevScrollPos] = useState(0)
+ const [show, setShow] = useState(true)
+
+ const handleScroll = () => {
+ const curScrollPos = (typeof window !== 'undefined') ? window.pageYOffset : 0
+ if (curScrollPos >= prevScrollPos) {
+ if (show && curScrollPos > 20) setShow(false)
+ }
+ else setShow(true)
+ setPrevScrollPos(curScrollPos)
+ }
+
+ useEffect(() => {
+ if (typeof window !== 'undefined') {
+ window.addEventListener('scroll', handleScroll)
+ return () => window.removeEventListener('scroll', handleScroll)
+ }
+ }, [prevScrollPos, show, handleScroll])
+
+
+ return (
+
+
+
+
+ {app.primaryMenu
+ ? <> swipe >
+ : <> swipe >
+ }
+
+
+ setSearch(true)}>
+
+
+
+
setSearch(true)}>
+
+
+ Quick Search...
+
+ Ctrl K
+
+
+
+
+
+
+
+
+
+ )
+}
+
+export default Header
diff --git a/packages/freesewing.org/components/help-us.js b/packages/freesewing.org/components/help-us.js
new file mode 100644
index 00000000000..772a9c3c829
--- /dev/null
+++ b/packages/freesewing.org/components/help-us.js
@@ -0,0 +1,49 @@
+import Popout from 'shared/components/popout.js'
+
+const HelpUs = ({ mdx=false, slug='/' }) => (
+
+ Click here to learn how you can help us improve this page
+ {mdx && (
+
+ Found a mistake?
+ You can edit this page on Github and help us improve our documentation.
+
+ )}
+
+ Does this look ok?
+
+
+ If it looks ok, great! But if not, please let me know about it.
+ Either by
+ reaching out on Discord
+ or feel free to create
+ an issue on Github .
+
+ Why do you ask?
+
+ I recently added a backend endpoint to auto-generate pretty (I hope) Open Graph images.
+ They are those little preview images you see when you paste a link in Discord (for example).
+
+
+ This idea is that it will auto-generate an image, but I am certain there are some edge cases
+ where that will not work.
+ There are hundreds of pages on this website and checking them all one by one is not something
+ I see myself doing. But since you are here on this page, perhaps you could see if the image
+ above looks ok.
+
+ Thank you, I really appreciate your help with this.
+
+
+)
+
+export default HelpUs
+
diff --git a/packages/freesewing.org/components/search.js b/packages/freesewing.org/components/search.js
new file mode 100644
index 00000000000..37c3138db3f
--- /dev/null
+++ b/packages/freesewing.org/components/search.js
@@ -0,0 +1,205 @@
+import { useState, useRef, useEffect } from 'react'
+import Link from 'next/link'
+import { useRouter } from 'next/router'
+
+import algoliasearch from 'algoliasearch/lite';
+import { useHotkeys } from 'react-hotkeys-hook'
+import { InstantSearch, connectHits, connectHighlight, connectSearchBox } from 'react-instantsearch-dom'
+import config from 'site/freesewing.config.js'
+
+const searchClient = algoliasearch(config.algolia.app, config.algolia.key)
+
+const Hits = props => {
+
+ // When we hit enter in the text field, we want to navigate to the result
+ // which means we must make the result links available in the input somehow
+ // so let's stuff them in a data attribute
+ const links = props.hits.map(hit => hit.page)
+ props.input.current.setAttribute('data-links', JSON.stringify(links))
+
+ return props.hits.map((hit, index) => (
+
+ ))
+}
+
+const CustomHits = connectHits(Hits);
+
+const Highlight = ({ highlight, attribute, hit, snippet=false }) => {
+ const parsedHit = highlight({
+ highlightProperty: snippet ? '_snippetResult' : '_highlightResult',
+ attribute,
+ hit,
+ });
+
+ return parsedHit.map((part, index) => part.isHighlighted
+ ? {part.value}
+ : {part.value}
+ )
+}
+
+const CustomHighlight = connectHighlight(Highlight);
+
+const Hit = props => (
+
+)
+
+// We use this for trapping ctrl-c
+let prev
+const handleInputKeydown = (evt, setSearch, setActive, active, router) => {
+ if (evt.key === 'Escape') setSearch(false)
+ if (evt.key === 'ArrowDown') setActive(act => act + 1)
+ if (evt.key === 'ArrowUp') setActive(act => act - 1)
+ if (evt.key === 'Enter') {
+ // Trigger navigation
+ if (evt?.target?.dataset?.links) {
+ router.push(JSON.parse(evt.target.dataset.links)[active])
+ setSearch(false)
+ }
+ }
+}
+
+const SearchBox = props => {
+
+ const input = useRef(null)
+ const router = useRouter()
+ useHotkeys('ctrl+x', () => {
+ input.current.value = ''
+ })
+ if (input.current && props.active < 0) input.current.focus()
+
+ const { currentRefinement, isSearchStalled, refine, setSearch, setActive } = props
+
+ return (
+
+
+
+
+ props.setSearch(false)}
+ >
+ Close Search
+
+
+
+
+ )
+}
+
+const CustomSearchBox = connectSearchBox(SearchBox);
+
+const Search = props => {
+
+ const [active, setActive] = useState(0)
+ useHotkeys('esc', () => props.setSearch(false))
+ useHotkeys('up', () => {
+ if (active) setActive(act => act - 1)
+ })
+ useHotkeys('down', () => {
+ setActive(act => act + 1)
+ })
+ useHotkeys('down', () => {
+ console.log('enter', active)
+ })
+
+ const stateProps = {
+ setSearch: props.setSearch,
+ setMenu: props.setMenu,
+ active, setActive
+ }
+
+ return (
+
+
+
+ )
+}
+
+export default Search
diff --git a/packages/freesewing.org/freesewing.config.js b/packages/freesewing.org/freesewing.config.js
new file mode 100644
index 00000000000..e5e3a098edd
--- /dev/null
+++ b/packages/freesewing.org/freesewing.config.js
@@ -0,0 +1,12 @@
+const config = {
+ algolia: {
+ app: 'MA0Y5A2PF0', // Application ID
+ index: 'canary_freesewing.dev',
+ key: '589c7a7e4d9c95a4f12868581259bf3a', // Search-only API key
+ },
+ strapi: 'https://posts.freesewing.org',
+ monorepo: 'https://github.com/freesewing/freesewing'
+}
+
+export default config
+
diff --git a/packages/freesewing.org/hooks/useApp.js b/packages/freesewing.org/hooks/useApp.js
new file mode 100644
index 00000000000..c36aa30fcec
--- /dev/null
+++ b/packages/freesewing.org/hooks/useApp.js
@@ -0,0 +1,99 @@
+import { useState } from 'react'
+import set from 'lodash.set'
+// Stores state in local storage
+import useLocalStorage from 'shared/hooks/useLocalStorage.js'
+// Translation
+import { en } from '@freesewing/i18n'
+// Prebuild navigation
+import prebuildNavigation from 'site/prebuild/navigation.js'
+
+function useApp(full = true) {
+
+ // User color scheme preference
+ const prefersDarkMode = (typeof window !== 'undefined' && typeof window.matchMedia === 'function')
+ ? window.matchMedia(`(prefers-color-scheme: dark`).matches
+ : null
+
+ // Persistent state
+ const [account, setAccount] = useLocalStorage('account', { username: false })
+ const [theme, setTheme] = useLocalStorage('theme', prefersDarkMode ? 'dark' : 'light')
+ const [language, setLanguage] = useLocalStorage('language', 'en')
+
+ // React State
+ const [primaryMenu, setPrimaryMenu] = useState(false)
+ const [navigation, setNavigation] = useState(prebuildNavigation[language])
+ const [slug, setSlug] = useState('/')
+ const [loading, setLoading] = useState(false)
+
+ // State methods
+ const togglePrimaryMenu = () => setPrimaryMenu(!primaryMenu)
+ const openPrimaryMenu = () => setPrimaryMenu(true)
+ const closePrimaryMenu = () => setPrimaryMenu(false)
+
+ /*
+ * Hot-update navigation method
+ */
+ const updateNavigation = (path, content) => {
+ const newNavigation = {...navigation}
+ if (typeof path === 'string') {
+ path = (path.slice(0,1) === '/')
+ ? path.slice(1).split('/')
+ : path.split('/')
+ }
+ setNavigation(set(navigation, path, content))
+ }
+
+ /*
+ * Translation method
+ *
+ * Note that freesewing.dev is only available in English
+ * however we use certain shared code/components between
+ * freesewing.dev and freesewing.org, so we still need
+ * a translation method
+ */
+ const t = (key=false, vals=false) => {
+ if (!key) return ''
+ if (!en.strings[key]) return key
+ let val = en.strings[key]
+ if (vals) {
+ for (const [search, replace] of Object.entries(vals)) {
+ val = val.replace(/search/g, replace)
+ }
+ }
+
+ return val
+ }
+
+
+ return {
+ // Static vars
+ site: 'dev',
+
+ // State
+ language,
+ loading,
+ navigation,
+ primaryMenu,
+ slug,
+ theme,
+
+ // State setters
+ setLanguage,
+ setLoading,
+ setNavigation,
+ setPrimaryMenu,
+ setSlug,
+ setTheme,
+ startLoading: () => { setLoading(true); setPrimaryMenu(false) }, // Always close menu when navigating
+ stopLoading: () => setLoading(false),
+ updateNavigation,
+
+ // State handlers
+ togglePrimaryMenu,
+
+ }
+
+}
+
+export default useApp
+
diff --git a/packages/freesewing.org/hooks/useMdx.js b/packages/freesewing.org/hooks/useMdx.js
new file mode 100644
index 00000000000..a10f3d8970b
--- /dev/null
+++ b/packages/freesewing.org/hooks/useMdx.js
@@ -0,0 +1,10 @@
+import path from 'path'
+
+const useMdx = (slug=false) => {
+ if (!slug) null
+ const file = ['markdown', 'dev', ...slug.split('/'), 'en.md'].join('/')
+ const mdx = require(file)
+ return {file}
+}
+
+export default useMdx
diff --git a/packages/freesewing.org/next.config.mjs b/packages/freesewing.org/next.config.mjs
new file mode 100644
index 00000000000..05d14efe112
--- /dev/null
+++ b/packages/freesewing.org/next.config.mjs
@@ -0,0 +1,3 @@
+import configBuilder from '../freesewing.shared/config/next.mjs'
+
+export default configBuilder('dev')
diff --git a/packages/freesewing.org/package.json b/packages/freesewing.org/package.json
new file mode 100644
index 00000000000..f354465e268
--- /dev/null
+++ b/packages/freesewing.org/package.json
@@ -0,0 +1,53 @@
+{
+ "name": "freesewing.dev",
+ "version": "2.19.6",
+ "private": true,
+ "scripts": {
+ "dev": "next dev -p 3002",
+ "develop": "next dev -p 3002",
+ "prebuild": "SITE=dev node ../freesewing.shared/prebuild/index.mjs",
+ "build": "next build",
+ "export": "yarn prebuild && next build && next export",
+ "start": "next start -p 3002",
+ "lint": "next lint",
+ "testdeploy": "next build && next export && netlify-cli deploy",
+ "deploy": "next build && next export && netlify-cli deploy --prod",
+ "serve": "pm2 start npm --name 'freesewing.dev' -- run start"
+ },
+ "dependencies": {
+ "@heroicons/react": "^1.0.5",
+ "@mdx-js/loader": "^2.0.0-rc.2",
+ "@mdx-js/mdx": "^2.0.0-rc.2",
+ "@mdx-js/react": "^2.0.0-rc.2",
+ "@mdx-js/runtime": "next",
+ "@tailwindcss/typography": "^0.5.0",
+ "algoliasearch": "^4.11.0",
+ "daisyui": "^1.16.2",
+ "lodash.get": "^4.4.2",
+ "lodash.orderby": "^4.6.0",
+ "lodash.set": "^4.3.2",
+ "netlify-cli": "^8.4.2",
+ "next": "latest",
+ "react-hotkeys-hook": "^3.4.4",
+ "react-instantsearch-dom": "^6.18.0",
+ "react-markdown": "^7.1.1",
+ "react-swipeable": "^6.2.0",
+ "react-timeago": "^6.2.1",
+ "rehype-highlight": "^5.0.1",
+ "rehype-sanitize": "^5.0.1",
+ "rehype-stringify": "^9.0.2",
+ "remark-copy-linked-files": "https://github.com/joostdecock/remark-copy-linked-files",
+ "remark-gfm": "^3.0.1",
+ "remark-jargon": "^2.19.6"
+ },
+ "devDependencies": {
+ "autoprefixer": "^10.4.0",
+ "eslint-config-next": "12.0.7",
+ "js-yaml": "^4.1.0",
+ "postcss": "^8.4.4",
+ "tailwindcss": "^3.0.1"
+ },
+ "engines": {
+ "node": ">=14.18.1"
+ }
+}
diff --git a/packages/freesewing.org/pages/[...mdxslug].js b/packages/freesewing.org/pages/[...mdxslug].js
new file mode 100644
index 00000000000..afd0c45c24a
--- /dev/null
+++ b/packages/freesewing.org/pages/[...mdxslug].js
@@ -0,0 +1,96 @@
+import Page from 'shared/components/wrappers/page.js'
+import useApp from 'site/hooks/useApp.js'
+import mdxMeta from 'site/prebuild/mdx.en.js'
+import mdxLoader from 'shared/mdx/loader'
+import MdxWrapper from 'shared/components/wrappers/mdx'
+import Head from 'next/head'
+import HelpUs from 'site/components/help-us.js'
+
+const MdxPage = props => {
+
+ // This hook is used for shared code and global state
+ const app = useApp()
+
+ /*
+ * Each page should be wrapped in the Page wrapper component
+ * You MUST pass it the result of useApp() as the `app` prop
+ * and for MDX pages you can spread the props.page props to it
+ * to automatically set the title and navigation
+ *
+ * Like breadcrumbs and updating the primary navigation with
+ * active state
+ */
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ )
+}
+
+/*
+ * Default export is the NextJS page object
+ */
+export default MdxPage
+
+
+/*
+ * getStaticProps() is used to fetch data at build-time.
+ *
+ * On this page, it is loading the mdx (markdown) content
+ * from the markdown file on disk.
+ *
+ * This, in combination with getStaticPaths() below means this
+ * page will be used to render/generate all markdown/mdx content.
+ *
+ * To learn more, see: https://nextjs.org/docs/basic-features/data-fetching
+ */
+export async function getStaticProps({ params }) {
+
+ const { mdx, intro } = await mdxLoader('en', 'dev', params.mdxslug.join('/'))
+
+ return {
+ props: {
+ mdx,
+ intro: intro.join(' '),
+ page: {
+ slug: params.mdxslug.join('/'),
+ path: '/' + params.mdxslug.join('/'),
+ slugArray: params.mdxslug,
+ ...mdxMeta[params.mdxslug.join('/')],
+ },
+ params
+ }
+ }
+}
+
+/*
+ * getStaticPaths() is used to specify for which routes (think URLs)
+ * this page should be used to generate the result.
+ *
+ * On this page, it is returning a list of routes (think URLs) for all
+ * the mdx (markdown) content.
+ * That list comes from mdxMeta, which is build in the prebuild step
+ * and contains paths, titles, and intro for all markdown.
+ *
+ * To learn more, see: https://nextjs.org/docs/basic-features/data-fetching
+ */
+export async function getStaticPaths() {
+ return {
+ paths: Object.keys(mdxMeta).map(slug => '/'+slug),
+ fallback: false
+ }
+}
diff --git a/packages/freesewing.org/pages/_app.js b/packages/freesewing.org/pages/_app.js
new file mode 100644
index 00000000000..e125a54674f
--- /dev/null
+++ b/packages/freesewing.org/pages/_app.js
@@ -0,0 +1,5 @@
+import 'shared/styles/globals.css'
+
+const FreeSewingDev = ({ Component, pageProps }) =>
+
+export default FreeSewingDev
diff --git a/packages/freesewing.org/pages/blog/[slug].js b/packages/freesewing.org/pages/blog/[slug].js
new file mode 100644
index 00000000000..8b9dab90a2c
--- /dev/null
+++ b/packages/freesewing.org/pages/blog/[slug].js
@@ -0,0 +1,133 @@
+import Page from 'shared/components/wrappers/page.js'
+import useApp from 'site/hooks/useApp.js'
+import strapiLoader from 'shared/strapi/loader'
+import { posts } from 'site/prebuild/strapi.blog.en.js'
+import TimeAgo from 'react-timeago'
+import MdxWrapper from 'shared/components/wrappers/mdx'
+import Markdown from 'react-markdown'
+import Head from 'next/head'
+import HelpUs from 'site/components/help-us.js'
+
+const strapi = "https://posts.freesewing.org"
+
+const Author = ({ author }) => (
+
+
+
+
+
+
+
+
+ {author?.displayname}
+ Wrote this
+
+
+ {author?.about}
+
+
+
+)
+
+const PostPage = ({ post, author }) => {
+ const app = useApp()
+
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ )
+
+ return (
+
+
+
+
+
+
+
+
+ )
+}
+
+export const getStaticProps = async (props) => {
+ const { post, author } = await strapiLoader('en', 'dev', 'blog', props.params.slug)
+
+ return { props: { post, author, slug: `blog/${props.params.slug}` } }
+}
+
+export const getStaticPaths = async () => {
+ const paths = []
+ for (const post of posts) paths.push({
+ params: {slug: post.slug}
+ })
+
+ return {
+ paths,
+ fallback: false,
+ }
+}
+
+export default PostPage
diff --git a/packages/freesewing.org/pages/blog/index.js b/packages/freesewing.org/pages/blog/index.js
new file mode 100644
index 00000000000..36ff2ff78d0
--- /dev/null
+++ b/packages/freesewing.org/pages/blog/index.js
@@ -0,0 +1,78 @@
+import Page from 'shared/components/wrappers/page.js'
+import useApp from 'site/hooks/useApp.js'
+import Link from 'next/link'
+import { posts } from 'site/prebuild/strapi.blog.en.js'
+import orderBy from 'lodash.orderby'
+import TimeAgo from 'react-timeago'
+import Head from 'next/head'
+import HelpUs from 'site/components/help-us.js'
+
+const strapi = "https://posts.freesewing.org"
+
+const Preview = ({ app, post }) => (
+
+)
+
+const BlogIndexPage = (props) => {
+ const app = useApp()
+
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {Object.values(orderBy(posts, ['date'], ['desc']))
+ .map(post =>
)
+ }
+
+
+
+ )
+}
+
+export default BlogIndexPage
diff --git a/packages/freesewing.org/pages/index.js b/packages/freesewing.org/pages/index.js
new file mode 100644
index 00000000000..9d8a307d199
--- /dev/null
+++ b/packages/freesewing.org/pages/index.js
@@ -0,0 +1,123 @@
+import Page from 'shared/components/wrappers/page.js'
+import useApp from 'site/hooks/useApp.js'
+import Logo from 'shared/components/logos/freesewing.js'
+import Head from 'next/head'
+import HelpUs from 'site/components/help-us.js'
+import Link from 'next/link'
+
+const HomePage = (props) => {
+ const app = useApp()
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ FreeSewing.dev hosts documentation for contributors and developers alike.
+
+ For our maker site, and to try our platform, go
+ to freesewing.org .
+
+
What is FreeSewing?
+
+
+ FreeSewing is an open source platform for made-to-measure sewing patterns.
+
+
+ @freeSewing/core is a Javascript library for 2D parametric design
+
+
+ It has a primary focus is on sewing patterns,
+ but can be utilized for a variety of similar 2D design tasks.
+
+
+
How can I try it out?
+
+
+ You can try it
+ in the browser
+ ,
+ in NodeJS
+ ,
+ or on any Javascript runtime.
+
+
+ The includes Deno, AWS Lamba, Cloudflare workers, Vercel Edge functions, Netlify functions, and so on.
+
+
+ Or save yourself the trouble, and check freesewing.org for a showcase of our software.
+
+
+
+ You son of a bitch, I'm in
+
+ *
+
+
+
+
+ We are an all-contributors project
+ and welcome all contributions.
+
+
+ Come say hi on Discord ,
+ or check out ways to contribute
+ to get inspired.
+
+
+ Last but certainly not least, you can also support FreeSewing financially:
+
+
+
+
Support FreeSewing
+
+ FreeSewing is fuelled by a voluntary subscription model
+
+
+ If you think what we do is worthwhile,
+ and if you can spare a few coins each month without hardship,
+ please support our work
+
+
Become a Patron
+
+
+
+
+ )
+}
+
+export default HomePage
diff --git a/packages/freesewing.org/pages/typography.js b/packages/freesewing.org/pages/typography.js
new file mode 100644
index 00000000000..49140ad1373
--- /dev/null
+++ b/packages/freesewing.org/pages/typography.js
@@ -0,0 +1,99 @@
+import { useEffect } from 'react'
+import Page from 'shared/components/wrappers/page.js'
+import useApp from 'site/hooks/useApp.js'
+import Popout from 'shared/components/popout.js'
+
+const TypographyPage = (props) => {
+ const app = useApp()
+ const { updateNavigation } = app
+
+ useEffect(() => {
+ updateNavigation(
+ ['typography'],
+ {
+ __title: 'Typography',
+ __linktitle: 'Typography',
+ __slug: 'typography',
+ __order: 'typography'
+ })
+ }, [updateNavigation])
+
+ const p = (
+
+ This paragraph is here to show the vertical spacing between headings and paragraphs.
+ In addition, let's make it a bit longer so we can see the line height as the text wraps.
+
+ )
+
+ return (
+
+
+
This typography page shows an overview of different elements and how they are styled.
+
It's a good starting point for theme development.
+
Headings (this is h2)
+ {p}
+
This is h3 {p}
+
This is h4 {p}
+
This is h5 {p}
+
This is h6 {p}
+
Links and buttons
+
A regular link looks like this , whereas buttons look like this:
+
Main button styles
+
+ Neutral button
+ Primary button
+ Secondary button
+ Accent button
+
+
State button styles
+
+ Info button
+ Success button
+ Warning button
+ Error button
+
+
Other button styles
+
+ Ghost button
+ Link button
+
+
Outlined button styles
+
+ Neutral button
+ Primary button
+ Secondary button
+ Accent button
+
+
Button sizes
+
+ Large
+ Normal
+ Small
+ Tiny
+ Large wide
+ Normal wide
+ Small wide
+ Tiny wide
+
+
Popouts
+
The Popout component is what powers various custom MDX components under the hood:
+ {['note', 'tip', 'warning', 'fixme', 'link', 'related', 'none'].map(type => {
+ const props = {}
+ props[type] = true
+ return (
+
+
{type}
+
+ I am the {type} title
+ {p}
+
+
+ )
+ })}
+
+
+ )
+}
+
+export default TypographyPage
+
diff --git a/packages/freesewing.org/postcss.config.js b/packages/freesewing.org/postcss.config.js
new file mode 100644
index 00000000000..85bf46214ea
--- /dev/null
+++ b/packages/freesewing.org/postcss.config.js
@@ -0,0 +1,5 @@
+// Can't seem to make this work as ESM
+const config = require('../freesewing.shared/config/postcss.config.js')
+
+module.exports = config
+
diff --git a/packages/freesewing.org/public/brands/algolia.svg b/packages/freesewing.org/public/brands/algolia.svg
new file mode 100644
index 00000000000..ab6ae5ab524
--- /dev/null
+++ b/packages/freesewing.org/public/brands/algolia.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/packages/freesewing.org/public/brands/bugsnag.svg b/packages/freesewing.org/public/brands/bugsnag.svg
new file mode 100644
index 00000000000..8abd9ff4ebf
--- /dev/null
+++ b/packages/freesewing.org/public/brands/bugsnag.svg
@@ -0,0 +1,15 @@
+
+
+
+
+
+
+
diff --git a/packages/freesewing.org/public/brands/crowdin.svg b/packages/freesewing.org/public/brands/crowdin.svg
new file mode 100644
index 00000000000..bbfe90540be
--- /dev/null
+++ b/packages/freesewing.org/public/brands/crowdin.svg
@@ -0,0 +1,61 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/packages/freesewing.org/public/brands/netlify.svg b/packages/freesewing.org/public/brands/netlify.svg
new file mode 100644
index 00000000000..8d306be6fb6
--- /dev/null
+++ b/packages/freesewing.org/public/brands/netlify.svg
@@ -0,0 +1,85 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/packages/freesewing.org/public/favicon.ico b/packages/freesewing.org/public/favicon.ico
new file mode 100644
index 00000000000..718d6fea483
Binary files /dev/null and b/packages/freesewing.org/public/favicon.ico differ
diff --git a/packages/freesewing.org/public/support.jpg b/packages/freesewing.org/public/support.jpg
new file mode 100644
index 00000000000..3c476403b0b
Binary files /dev/null and b/packages/freesewing.org/public/support.jpg differ
diff --git a/packages/freesewing.org/scripts/algolia.mjs b/packages/freesewing.org/scripts/algolia.mjs
new file mode 100644
index 00000000000..ac8255068de
--- /dev/null
+++ b/packages/freesewing.org/scripts/algolia.mjs
@@ -0,0 +1,150 @@
+/*
+ * This will update (replace really) the Algolia index with the
+ * current website contents. Or at least the markdown and Strapi
+ * content
+ *
+ * It expects the following environment vars to be set in a
+ * .env file in the 'packages/freesewing.dev' folder:
+ *
+ * ALGOLIA_APP_ID -> probably MA0Y5A2PF0
+ * ALGOLIA_API_KEY -> Needs permission to index/create/delete
+ * ALGOLIA_INDEX -> Name of the index to index to
+ *
+ */
+import dotenv from 'dotenv'
+import fs from 'fs'
+import path from 'path'
+import algoliasearch from 'algoliasearch'
+import { unified } from 'unified'
+import remarkParser from 'remark-parse'
+import remarkCompiler from 'remark-stringify'
+import remarkFrontmatter from 'remark-frontmatter'
+import remarkFrontmatterExtractor from 'remark-extract-frontmatter'
+import remarkRehype from 'remark-rehype'
+import rehypeSanitize from 'rehype-sanitize'
+import rehypeStringify from 'rehype-stringify'
+import yaml from 'yaml'
+import { getPosts } from '../../freesewing.shared/prebuild/strapi.mjs'
+import { getMdxFileList } from '../../freesewing.shared/prebuild/mdx.mjs'
+dotenv.config()
+
+/*
+ * Initialize Algolia client
+ */
+const client = algoliasearch(process.env.ALGOLIA_APP_ID, process.env.ALGOLIA_API_KEY)
+const index = client.initIndex(process.env.ALGOLIA_INDEX)
+
+/*
+ * Turn a Strapi blog post into an object ready for indexing
+ */
+const transformBlogpost = post => ({
+ objectID: `/blog/${post.slug}`,
+ page: `/blog/${post.slug}`,
+ title: post.title,
+ date: post.date,
+ slug: post.slug,
+ body: post.body,
+ author: post.author,
+ caption: post.caption,
+ type: 'blog',
+})
+
+/*
+ * Turn a Strapi author into an object ready for indexing
+ */
+const transformAuthor = author => ({
+ objectID: `/blog/authors/${author.name}`,
+ page: `/blog/authors/${author.name}`,
+ name: author.name,
+ displayname: author.displayname,
+ about: author.about,
+})
+
+/*
+ * Get and index blog posts and author info from Strapi
+ */
+const indexStrapiContent = async () => {
+
+ // Say hi
+ console.log()
+ console.log(`Indexing Strapi content to Algolia`)
+
+ const authors = {}
+ const rawPosts = await getPosts('blog', 'dev', 'en')
+ // Extract list of authors
+ for (const [slug, post] of Object.entries(rawPosts)) {
+ authors[post.author.slug] = transformAuthor(post.author)
+ rawPosts[slug].author = post.author.slug
+ }
+ // Index posts to Algolia
+ index
+ .saveObjects(Object.values(rawPosts).map(post => transformBlogpost(post)))
+ .then(({ objectIDs }) => console.log(objectIDs))
+ .catch(err => console.log(err))
+ // Index authors to Algolia
+ index
+ .saveObjects(Object.values(authors))
+ .then(({ objectIDs }) => console.log(objectIDs))
+ .catch(err => console.log(err))
+}
+
+/*
+ * Loads markdown from disk and compiles it into HTML for indexing
+ */
+const markdownLoader = async file => {
+
+ const md = await fs.promises.readFile(file, 'utf-8')
+
+ const page = await unified()
+ .use(remarkParser)
+ .use(remarkCompiler)
+ .use(remarkFrontmatter)
+ .use(remarkFrontmatterExtractor, { yaml: yaml.parse })
+ .use(remarkRehype)
+ .use(rehypeSanitize)
+ .use(rehypeStringify)
+ .process(md)
+ const id = file.split('freesewing/markdown/dev').pop().slice(0, -6)
+
+ return {
+ objectID: id,
+ page: id,
+ title: page.data.title,
+ body: page.value,
+ type: 'docs',
+ }
+}
+
+
+
+/*
+ * Get and index markdown content
+ */
+const indexMarkdownContent = async () => {
+
+ // Say hi
+ console.log()
+ console.log(`Indexing Markdown content to Algolia`)
+
+ // Setup MDX root path
+ const mdxRoot = path.resolve('..', '..', 'markdown', 'dev')
+
+ // Get list of filenames
+ const list = await getMdxFileList(mdxRoot, 'en')
+ const pages = []
+ for (const file of list) pages.push(await markdownLoader(file))
+ // Index markdown to Algolia
+ index
+ .saveObjects(pages)
+ .then(({ objectIDs }) => console.log(objectIDs))
+ .catch(err => console.log(err))
+}
+
+const run = async () => {
+ await indexMarkdownContent()
+ await indexStrapiContent()
+ console.log()
+}
+
+run()
+
diff --git a/packages/freesewing.org/tailwind.config.js b/packages/freesewing.org/tailwind.config.js
new file mode 100644
index 00000000000..3f664c97b59
--- /dev/null
+++ b/packages/freesewing.org/tailwind.config.js
@@ -0,0 +1,4 @@
+// Can't seem to make this work as ESM
+const config = require('../freesewing.shared/config/tailwind.config.js')
+
+module.exports = config