1
0
Fork 0

feat(fs.org): Initial import of freesewing.dev code

This commit is contained in:
Joost De Cock 2022-01-02 17:16:15 +01:00
parent 7fb05ac25d
commit ccac712133
25 changed files with 1561 additions and 9 deletions

View file

@ -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.

View file

@ -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 }) => (
<NextLink href={href}>
<a title={txt} className="hover:underline text-secondary font-bold hover:pointer">{txt}</a>
</NextLink>
)
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 }) => (
<footer className="bg-neutral">
<div className={`theme-gradient h-1 w-full relative ${app.loading ? 'loading' : ''}`}></div>
<div className="p-4 py-16 flex flex-row bg-neutral -mt-1 z-0 gap-8 flex-wrap justify-around text-neutral-content">
<div className="w-64 mt-2">
<div className="px-4 mb-4"><CreativeCommonsLogo /></div>
<div className="flex flex-row gap-2 justify-center items-center">
<div className="basis-1/4">
<CcByLogo />
</div>
<p className="text-sm text-neutral-content text-right basis-3/4">
Content on freesewing.org is licensed under
a <a className={link} href="https://creativecommons.org/licenses/by/4.0/">Creative
Commons Attribution 4.0 International license</a>
</p>
</div>
<div className="flex flex-row gap-2 justify-center items-center">
<div className="basis-1/4">
<OsiLogo />
</div>
<p className="text-sm text-neutral-content text-right basis-3/4">
Our source code and markdown is <a href="https://github.com/freesewing/freesewing"
className={link}>available
on GitHub</a> under <a href="https://opensource.org/licenses/MIT"
className={link}>the MIT license</a>
</p>
</div>
</div>
<div className="w-full sm:w-auto">
<h5 className="text-neutral-content">What is this?</h5>
<div className="theme-gradient h-1 mb-4"></div>
<ul>
<li>
<Link href="https://freesewing.org/docs/guide/what" txt="About FreeSewing" />
</li>
<li>
<Link href="https://freesewing.org/docs/faq" txt="Frequently Asked Questions" />
</li>
<li>
<Link href="https://freesewing.org/patrons/join" txt="Become a Patron" />
</li>
</ul>
</div>
<div className="w-full sm:w-auto sm:max-w-xs">
<h5 className="text-neutral-content">Where can I turn for help?</h5>
<div className="theme-gradient h-1 mb-2"></div>
<p className="text-sm text-neutral-content">
<a className={link} href={social.discord}>Our Discord server</a> is
the best place to ask questions and get help. It&apos;s where our community hangs out
so you&apos;ll get the fastest response and might even make a few new friends along the way.
</p>
<p className="text-sm text-neutral-content">
You can also <a href={social.twitter} className={link} >reach out on Twitter</a> or <a
href="https://github.com/freesewing/freesewing/issues/new/choose"
className={link}
> create an issue on Github </a> if Discord is not your jam.
</p>
</div>
<div className="w-full sm:w-auto">
<h5 className="text-neutral-content">Social Media</h5>
<div className="theme-gradient h-1 mb-4"></div>
<ul>
{Object.keys(social).map(item => <li key={item}><Link href={social[item]} txt={item}/></li>)}
</ul>
</div>
<div className="text-center">
<Logo fill='currentColor' stroke='none' size={164} className="m-auto"/>
<h5 className="text-neutral-content">FreeSewing</h5>
<p className="bold text-neutral-content text-sm">
Come for the sewing patterns
<br />
Stay for the community
</p>
</div>
</div>
<p className="text-center text-neutral-content text-sm px-2">
<span
className="px-1 text-lg font-bold block sm:inline">FreeSewing</span> is made by these <span
className="text-accent font-bold text-lg px-1 block sm:inline">wonderful contributors</span>
</p>
<div className="p-4 pb-16 flex flex-row bg-neutral -mt-2 z-0 gap-1 lg:gap-2 flex-wrap justify-around text-neutral-content lg:px-24">
{contributors.map(person => (
<a title={person.name} href={person.profile} className="m-auto" key={person.profile+person.name}>
<img
src={person.avatar_url} alt={`Avatar of ${person.name}`}
className="w-12 h-12 lg:w-16 lg:h-16 rounded-full border-2 border-secondary hover:border-accent"
/>
</a>
))}
</div>
<p className="text-center text-neutral-content text-sm px-2">
<span
className="px-1 text-lg font-bold block sm:inline">FreeSewing</span> is supported by these <span
className="text-accent font-bold text-lg px-1 block sm:inline">generous patrons</span>
</p>
<div className="p-4 pb-16 flex flex-row bg-neutral -mt-2 z-0 gap-1 lg:gap-2 flex-wrap justify-around text-neutral-content lg:px-24">
{patrons.map(person => (
<a
title={person.name}
href={`https://freesewing.org/users/${person.username}`}
className="m-auto"
key={person.username}
>
<img
src={person.img}
alt={`Avatar of ${person.name}`}
className="w-12 h-12 lg:w-16 lg:h-16 rounded-full border-2 border-secondary hover:border-accent"
/>
</a>
))}
</div>
<p className="text-center text-neutral-content text-sm px-2">
<span
className="px-1 text-lg font-bold block sm:inline">FreeSewing</span> is hosted by these <span
className="text-accent font-bold text-lg px-1 block sm:inline">awesome companies</span>
</p>
<div className="p-4 py-16 flex flex-row bg-neutral -mt-2 z-0 gap-8 flex-wrap justify-center items-center text-neutral-content">
<a title="Search powered by Algolia" href="https://www.algolia.com/">
<img src="/brands/algolia.svg" className="w-64 mx-12 sm:mx-4" alt="Search powered by Algolia"/>
</a>
<a title="Translation powered by Crowdin" href="https://www.crowdin.com/">
<img src="/brands/crowdin.svg" className="w-64 mx-12 sm:mx-4" alt="Translation powered by Crowdin" />
</a>
<a title="Deploys & hosting by Netlify" href="https://www.netlify.com/">
<img src="/brands/netlify.svg" className="w-44 mx-12 sm:mx-4" alt="Deploys & hosting by Netlify" />
</a>
<a title="Error handling by Bugsnag" href="https://www.bugsnag.com/">
<img src="/brands/bugsnag.svg" className="h-36 mx-12 sm:mx-4" alt="Error handling by bugsnag" />
</a>
</div>
</footer>
)
export default Footer

View file

@ -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 => (
<svg xmlns="http://www.w3.org/2000/svg" className="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M17 8l4 4m0 0l-4 4m4-4H3" />
</svg>
)
const Left = props => (
<svg xmlns="http://www.w3.org/2000/svg" className="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M10 19l-7-7m0 0l7-7m-7 7h18" />
</svg>
)
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 (
<header className={`
fixed top-0 left-0
bg-neutral
w-full
z-30
transition-transform
${show ? '': 'fixed top-0 left-0 -translate-y-20'}
`}>
<div className="max-w-6xl m-auto">
<div className="p-2 flex flex-row gap-2 justify-between text-neutral-content">
<button
className={`
btn btn-sm
text-neutral-content bg-transparent
border border-transparent
hover:bg-transparent hover:border-base-100
sm:hidden
h-12
`}
onClick={app.togglePrimaryMenu}>
{app.primaryMenu
? <><CloseIcon /><span className="opacity-50 pl-2 flex flex-row items-center gap-1"><Left />swipe</span></>
: <><MenuIcon /><span className="opacity-50 pl-2 flex flex-row items-center gap-1"><Right />swipe</span></>
}
</button>
<div className="flex flex-row gap-2 sm:hidden">
<button className="btn btn-sm btn h-12 px-12" onClick={() => setSearch(true)}>
<SearchIcon />
</button>
</div>
<button className={`
btn btn-sm h-12
hidden sm:flex
flex-row gap-1 mr-4 w-64 px-2
bg-base-100 text-base-content
hover:bg-base-100 hover:text-base-content
justify-between
`} onClick={() => setSearch(true)}>
<div className="flex flex-row items-center gap-2">
<SearchIcon />
<span className="normal-case font-normal">Quick Search...</span>
</div>
<span className="normal-case">Ctrl K</span>
</button>
<div className="hidden md:flex md:flex-row gap-2">
<Link href="/">
<a className="flex flex-column items-center">
<Logo size={36} fill="currentColor" stroke={false} />
</a>
</Link>
<Link href="/">
<a role="button" className="btn btn-link btn-sm text-neutral-content h-12">
freesewing.dev
</a>
</Link>
</div>
<div className="hidden sm:flex flex-row items-center">
<ThemePicker app={app} />
</div>
</div>
</div>
<div className={`theme-gradient h-1 w-full z-10 relative -mb-1 ${app.loading ? 'loading' : ''}`}></div>
</header>
)
}
export default Header

View file

@ -0,0 +1,49 @@
import Popout from 'shared/components/popout.js'
const HelpUs = ({ mdx=false, slug='/' }) => (
<details className="mt-4">
<summary>Click here to learn how you can help us improve this page</summary>
{mdx && (
<Popout tip className='max-w-prose'>
<h6>Found a mistake?</h6>
You can <a
href={`https://github.com/freesewing/freesewing/edit/develop/markdown/dev/${slug}/en.md`}
className="text-secondary font-bold"
>edit this page on Github</a> and help us improve our documentation.
</Popout>
)}
<Popout comment by="joost" className='max-w-prose'>
<h6>Does this look ok?</h6>
<img
className="my-4 rounded"
src={`https://canary.backend.freesewing.org/og-img/en/dev${slug}`}
/>
<p>
If it looks ok, great! But if not, please let me know about it.
Either by <a href="https://discord.freesewing.org/" className="text-secondary">
reaching out on Discord
</a> or feel free to <a
href="https://github.com/freesewing/freesewing/issues/new/choose"
className="text-secondary"
>create
an issue on Github</a>.
</p>
<h6>Why do you ask?</h6>
<p className="text-base">
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).
</p>
<p className="text-base">
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.
</p>
<p className="text-base">Thank you, I really appreciate your help with this.</p>
</Popout>
</details>
)
export default HelpUs

View file

@ -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) => (
<Hit
key={hit.page}
{...props}
hit={hit}
index={index}
len={props.hits.length}
activeLink={links[props.active]}
/>
))
}
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
? <mark className="text-base-content bg-secondary-focus bg-opacity-30" key={index}>{part.value}</mark>
: <span key={index}>{part.value}</span>
)
}
const CustomHighlight = connectHighlight(Highlight);
const Hit = props => (
<div
className={`
px-2 py-1 ounded mt-1
text-base text-base-content
sm:rounded
lg:px-4 lg:py-2
hover:bg-secondary hover:bg-opacity-10 hover:text-base-content
${props.index === props.active
? 'bg-secondary bg-opacity-30'
: 'bg-base-300 bg-opacity-10'
}
`}
>
<Link href={props.hit.page}>
<a href={props.hit.page} className="flex flex-row justify-between gap-2">
<span className="text-base sm:text-xl font-bold leading-5">
{props.hit?._highlightResult?.title
? <CustomHighlight hit={props.hit} attribute='title' />
: props.hit.title
}
</span>
<span className="text-xs pt-0.5 sm:text-base sm:pt-1 font-bold uppercase">{props.hit.page.split('/')[1]}</span>
</a>
</Link>
{props.hit?._snippetResult?.body && (
<Link href={props.hit.page}>
<a href={props.hit.page} className="text-sm sm:text-base block py-1">
<CustomHighlight hit={props.hit} attribute='body' snippet />
</a>
</Link>
)}
{props.hit?._highlightResult?.page && (
<Link href={props.hit.page}>
<a href={props.hit.page} className="text-xs sm:text-sm block opacity-70">
<CustomHighlight hit={props.hit} attribute='page' />
</a>
</Link>
)}
</div>
)
// 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 (
<div className="py-8">
<form noValidate action="" role="search" onSubmit={(evt) => evt.preventDefault()}>
<div className="form-control">
<div className="relative">
<input
ref={input}
type="search"
autoFocus={true}
value={currentRefinement}
onChange={event => refine(event.currentTarget.value)}
onKeyDown={(evt) => handleInputKeydown(evt, setSearch, setActive, props.active, router)}
className="input lg:input-lg input-bordered input-neutral w-full pr-16"
placeholder='Type to search'
/>
<button
className="absolute right-0 top-0 rounded-l-none btn btn-neutral lg:btn-lg"
onClick={() => props.setSearch(false)}
>X</button>
</div>
<label className="label hidden sm:block">
<div className="label-text flex flex-row gap-4 justify-between">
<div><b> Escape</b> to exit</div>
<div><b> Up</b> or <b>Down</b> to select</div>
<div><b> Enter</b> to navigate</div>
</div>
</label>
</div>
<div
className="overscroll-auto overflow-y-auto mt-2"
style={{maxHeight: 'calc(100vh - 10rem)'}}
>
{
input.current
&& input.current.value.length > 0
&& <CustomHits hitComponent={Hit} {...props} input={input}/>
}
</div>
</form>
<div className={`
bg-neutral text-neutral-content
z-20 w-full mx-auto
lg:bg-base-100 lg:border-base-200
fixed bottom-0 left-0 border-t-2
lg:hidden
`}>
<div className='px-4 py-0 flex flex-row w-full lg:py-2'>
<button
className={`btn btn-ghost btn-block`}
onClick={() => props.setSearch(false)}
>
<span className='px-2 pt-2 pb-2'>Close Search</span>
</button>
</div>
</div>
</div>
)
}
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 (
<InstantSearch indexName={config.algolia.index} searchClient={searchClient}>
<CustomSearchBox {...stateProps}/>
</InstantSearch>
)
}
export default Search

View file

@ -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

View file

@ -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

View file

@ -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 <p>{file}</p>
}
export default useMdx

View file

@ -0,0 +1,3 @@
import configBuilder from '../freesewing.shared/config/next.mjs'
export default configBuilder('dev')

View file

@ -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"
}
}

View file

@ -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 (
<Page app={app} {...props.page}>
<Head>
<meta property="og:title" content={props.page.title} key="title" />
<meta property="og:type" content="article" key='type' />
<meta property="og:description" content={props.intro} key='type' />
<meta property="og:article:author" content='Joost De Cock' key='author' />
<meta property="og:image" content={`https://canary.backend.freesewing.org/og-img/en/dev/${props.page.slug}`} key='image' />
<meta property="og:image:type" content="image/png" />
<meta property="og:image:width" content="1200" />
<meta property="og:image:height" content="630" />
<meta property="og:url" content={`https://freesewing.dev/${props.page.slug}`} key='url' />
<meta property="og:locale" content="en_US" key='locale' />
<meta property="og:site_name" content="freesewing.dev" key='site' />
</Head>
<MdxWrapper mdx={props.mdx} app={app}/>
<HelpUs mdx slug={`/${props.page.slug}`} />
</Page>
)
}
/*
* 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
}
}

View file

@ -0,0 +1,5 @@
import 'shared/styles/globals.css'
const FreeSewingDev = ({ Component, pageProps }) => <Component {...pageProps} />
export default FreeSewingDev

View file

@ -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 }) => (
<div id="author" className="flex flex-col lg:flex-row m-auto p-2 items-center">
<div className="theme-gradient w-40 h-40 p-2 rounded-full aspect-square hidden lg:block">
<div
className={`
w-lg bg-cover bg-center rounded-full aspect-square
hidden lg:block
`}
style={{backgroundImage: `url(${strapi}${author?.img})`}}
>
</div>
</div>
<div className="theme-gradient p-2 rounded-full aspect-square w-40 h-40 lg:hidden m-auto">
<img
className={`block w-full h-full mx-auto rounded-full`}
src={`${strapi}${author?.img}`}
alt={author?.displayname}
width={author?.picture?.width}
height={author?.picture?.height}
/>
</div>
<div className={`
text-center p-2 px-4 rounded-r-lg bg-opacity-50
lg:text-left
`}
>
<p className="text-xl">
<span className="font-semibold"> {author?.displayname}</span>
<span className="text-sm pl-2 opacity-70">Wrote this</span>
</p>
<div className="prose mdx">
<Markdown>{author?.about}</Markdown>
</div>
</div>
</div>
)
const PostPage = ({ post, author }) => {
const app = useApp()
return (
<Page app={app} title={post.title}>
<Head>
<meta property="og:title" content={post.title} key="title" />
<meta property="og:type" content="article" key='type' />
<meta property="og:description" content={post.intro || post.title} key='description' />
<meta property="og:article:author" content={author.displayname} key='author' />
<meta property="og:url" content={`https://freesewing.dev/blog/${post.slug}`} key='url' />
<meta property="og:image" content={`https://canary.backend.freesewing.org/og-img/en/dev/blog/${post.slug}`} key='image' />
<meta property="og:image:type" content="image/png" />
<meta property="og:image:width" content="1200" />
<meta property="og:image:height" content="630" />
<meta property="og:locale" content="en_US" key='locale' />
<meta property="og:site_name" content="freesewing.dev" key='site' />
</Head>
<article className="mb-12">
<div className="flex flex-row justify-between text-sm mb-1 mt-2">
<span><TimeAgo date={post.date} /> [{post.date}]</span>
<span>
By <a
href="#author"
className="text-secondary hover:text-secondary-focus"
>
{author?.displayname || 'FIXME: No displayname'}
</a>
</span>
</div>
<figure>
<img
src={`${strapi}${post.image.formats.large.url}`}
alt={post.caption}
className="shadow m-auto"
/>
<figcaption
className="text-center mb-8 prose m-auto"
dangerouslySetInnerHTML={{__html: post.caption}}
/>
</figure>
<div className="strapi prose lg:prose-lg mb-12 m-auto">
<MdxWrapper mdx={post.mdx} app={app} />
</div>
<div className="max-w-prose text-lg lg:text-xl">
<Author author={author} />
</div>
<HelpUs blog slug={`/blog/${post.slug}`} />
</article>
</Page>
)
return (
<Page app={app} title='Blog' slug='blog'>
<article className="mb-12">
<div className="strapi prose lg:prose-lg mb-12 m-auto">
<MdxWrapper mdx={props.post.mdx} />
</div>
</article>
<Author author={author} type={props.type} t={props.t}/>
</Page>
)
}
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

View file

@ -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 }) => (
<div className="theme-gradient p-1 hover:p-0 hover:mb-1 hover:pointer transition-all">
<Link href={`/blog/${post.slug}`}>
<a title={post.title} className="hover:underline">
<div className="bg-base-100 w-full aspect-video shadow flex flex-column items-end" style={{
backgroundImage: `url(${strapi}${post.img})`,
backgroundSize: 'cover',
}}>
<div className="grow"></div>
<div className="text-right">
<div className={`
bg-neutral text-neutral-content
bg-opacity-80
px-4 text-right
`}>
<h5 className={`
text-neutral-content
text-xl font-normal
md:text-2xl md:font-light
`}>
{post.title}
</h5>
<p className={`
m-0 p-1 -mt-2
text-neutral-content
opacity-50
leading-normal text-sm font-normal
`}>
<TimeAgo date={post.date} /> by {post.author}
</p>
</div>
</div>
</div>
</a>
</Link>
</div>
)
const BlogIndexPage = (props) => {
const app = useApp()
return (
<Page app={app} title='FreeSewing Development Blog' slug='blog'>
<Head>
<meta property="og:title" content="FreeSewing Developers Blog" key="title" />
<meta property="og:type" content="article" key='type' />
<meta property="og:description" content="Content for developers and contributors alike. Strictly no sewing stuff" key='type' />
<meta property="og:article:author" content='Joost De Cock' key='author' />
<meta property="og:image" content="https://canary.backend.freesewing.org/og-img/en/dev/blog" key='image' />
<meta property="og:image:type" content="image/png" />
<meta property="og:image:width" content="1200" />
<meta property="og:image:height" content="630" />
<meta property="og:url" content="https://freesewing.dev/blog" key='url' />
<meta property="og:locale" content="en_US" key='locale' />
<meta property="og:site_name" content="freesewing.dev" key='site' />
</Head>
<div className="grid grid-cols-1 gap-4 lg:grid-cols-2">
{Object.values(orderBy(posts, ['date'], ['desc']))
.map(post => <Preview app={app} post={post} key={post.slug}/>)
}
</div>
<HelpUs slug='/blog' />
</Page>
)
}
export default BlogIndexPage

View file

@ -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 (
<Page app={app} title="Welcome to FreeSewing.dev">
<Head>
<meta property="og:title" content="FreeSewing.dev" key="title" />
<meta property="og:type" content="article" key='type' />
<meta property="og:description" content="Documentation and tutorials for FreeSewing developers and contributors. Plus our Developers Blog" key='type' />
<meta property="og:article:author" content='Joost De Cock' key='author' />
<meta property="og:image" content="https://canary.backend.freesewing.org/og-img/en/dev/" key='image' />
<meta property="og:image:type" content="image/png" />
<meta property="og:image:width" content="1200" />
<meta property="og:image:height" content="630" />
<meta property="og:url" content="https://freesewing.dev/" key='url' />
<meta property="og:locale" content="en_US" key='locale' />
<meta property="og:site_name" content="freesewing.dev" key='site' />
</Head>
<div className="max-w-screen-md">
<p>
FreeSewing.dev hosts documentation for contributors and developers alike.
<br />
For our maker site, and to try our platform, go
to <a
href="https://freesewing.org/"
title="Go to FreeSewing.org"
className="text-secondary font-bold"
>freesewing.org</a>.
</p>
<h2 className="mt-8">What is FreeSewing?</h2>
<div className="theme-gradient p-1 -mt-2 mb-2 "></div>
<p className="text-2xl sm:text-3xl">
FreeSewing is an open source platform for made-to-measure sewing patterns.
</p>
<p className="text-xl sm:text-2xl">
<b>@freeSewing/core</b> is a Javascript library for 2D parametric design
</p>
<p>
It has a primary focus is on sewing patterns,
but can be utilized for a variety of similar 2D design tasks.
</p>
<h2 className='mt-8'>How can I try it out?</h2>
<div className="theme-gradient p-1 -mt-2 mb-2 "></div>
<p className="text-2xl sm:text-3xl">
You can try it <Link
href="/howtos/environments/browser">
<a className="text-secondary">in the browser</a>
</Link>, <Link
href="/howtos/environments/node">
<a className="text-secondary">in NodeJS</a>
</Link>,
or on any Javascript runtime.
</p>
<p className="text-xl sm:text-2xl">
The includes Deno, AWS Lamba, Cloudflare workers, Vercel Edge functions, Netlify functions, and so on.
</p>
<p>
Or save yourself the trouble, and check <a
href="https://freesewing.org/"
title="Go to FreeSewing.org"
className="text-secondary font-bold"
>freesewing.org</a> for a showcase of our software.
</p>
<h2 className='mt-8'>
You son of a bitch, I&apos;m in
<sup>
<a
href="https://www.youtube.com/watch?v=nKxvDYHkfSY"
className="text-secondary"
>*</a>
</sup>
</h2>
<div className="theme-gradient p-1 -mt-2 mb-2 "></div>
<p className="text-2xl sm:text-3xl">
We are an <a
href="https://allcontributors.org/"
className="text-secondary"
>all-contributors</a> project
and welcome all contributions.
</p>
<p className="text-xl sm:text-2xl">
<a
href="https://discord.freesewing.org/"
className="text-secondary"
>Come say hi on Discord</a>,
or check out <Link
href="/howtos/ways-to-contribute"><a
className="text-secondary">ways to contribute</a>
</Link> to get inspired.
</p>
<p>
Last but certainly not least, you can also support FreeSewing financially:
</p>
<div className="bg-cover bg-neutral w-full bg-center rounded-lg shadow p-4 "
style={{backgroundImage: "url(/support.jpg)"}}
>
<h2 className="text-neutral-content p-4 text-4xl font-bold sm:font-light sm:text-6xl drop-shadow">Support FreeSewing</h2>
<p className="text-neutral-content p-4 font-bold max-w-md text-lg">
FreeSewing is fuelled by a voluntary subscription model
</p>
<p className="text-neutral-content p-4 font-bold max-w-md text-lg">
If you think what we do is worthwhile,
and if you can spare a few coins each month without hardship,
please support our work
</p>
<a role="button" className="btn btn-accent btn-wide ml-4 mb-8" href="https://freesewing.org/patrons/join">Become a Patron</a>
</div>
<HelpUs slug='/' />
</div>
</Page>
)
}
export default HomePage

View file

@ -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 = (
<p>
This paragraph is here to show the vertical spacing between headings and paragraphs.
In addition, let&apos;s make it a bit longer so we can see the line height as the text wraps.
</p>
)
return (
<Page app={app} title='Typography'>
<div className="text-primary mdx max-w-prose text-base-content max-w-prose text-lg lg:text-xl">
<p>This typography page shows an overview of different elements and how they are styled.</p>
<p>It&apos;s a good starting point for theme development.</p>
<h2>Headings (this is h2)</h2>
{p}
<h3>This is h3</h3>{p}
<h4>This is h4</h4>{p}
<h5>This is h5</h5>{p}
<h6>This is h6</h6>{p}
<h2>Links and buttons</h2>
<p>A regular link <a href="#">looks like this</a>, whereas buttons look like this:</p>
<h3>Main button styles</h3>
<div className="flex flex-row gap-2 flex-wrap">
<button className="btn btn-neutral">Neutral button</button>
<button className="btn btn-primary">Primary button</button>
<button className="btn btn-secondary">Secondary button</button>
<button className="btn btn-accent">Accent button</button>
</div>
<h3>State button styles</h3>
<div className="flex flex-row gap-2 flex-wrap">
<button className="btn btn-info">Info button</button>
<button className="btn btn-success">Success button</button>
<button className="btn btn-warning">Warning button</button>
<button className="btn btn-error">Error button</button>
</div>
<h3>Other button styles</h3>
<div className="flex flex-row gap-2 flex-wrap">
<button className="btn btn-ghost">Ghost button</button>
<button className="btn btn-link">Link button</button>
</div>
<h3>Outlined button styles</h3>
<div className="flex flex-row gap-2 flex-wrap">
<button className="btn btn-outline btn-neutral">Neutral button</button>
<button className="btn btn-outline btn-primary">Primary button</button>
<button className="btn btn-outline btn-secondary">Secondary button</button>
<button className="btn btn-outline btn-accent">Accent button</button>
</div>
<h3>Button sizes</h3>
<div className="flex flex-row gap-2 flex-wrap">
<button className="btn btn-primary btn-lg">Large</button>
<button className="btn btn-primary">Normal</button>
<button className="btn btn-primary btn-sm">Small</button>
<button className="btn btn-primary btn-xs">Tiny</button>
<button className="btn btn-primary btn-lg btn-wide">Large wide</button>
<button className="btn btn-primary btn-wide">Normal wide</button>
<button className="btn btn-primary btn-sm btn-wide">Small wide</button>
<button className="btn btn-primary btn-xs bnt-wide">Tiny wide</button>
</div>
<h2>Popouts</h2>
<p>The Popout component is what powers various custom MDX components under the hood:</p>
{['note', 'tip', 'warning', 'fixme', 'link', 'related', 'none'].map(type => {
const props = {}
props[type] = true
return (
<div key={type}>
<h3 className="capitalize">{type}</h3>
<Popout {...props}>
<h5>I am the {type} title</h5>
{p}
</Popout>
</div>
)
})}
</div>
</Page>
)
}
export default TypographyPage

View file

@ -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

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 7.7 KiB

View file

@ -0,0 +1,15 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 22.1.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 900 1295.5" style="enable-background:new 0 0 900 1295.5;" xml:space="preserve">
<style type="text/css">
.st0{fill:#FF5A60;}
</style>
<circle class="st0" cx="450" cy="845.5" r="53.7"/>
<path class="st0" d="M450,1295.5c-248.1,0-450-201.9-450-450V622.3c0-18.5,15-33.5,33.5-33.5h159.6l-0.6-516.6L66.9,149.5v294.2
c0,18.5-15,33.5-33.5,33.5S0,462.2,0,443.7v-298c0-20.7,11-40.4,28.6-51.2L167.8,8.9C186.4-2.5,209.7-3,228.7,7.6
s30.8,30.7,30.8,52.5l0.6,528.8h190c141.5,0,256.6,115.1,256.6,256.6s-115.1,256.6-256.6,256.6S193.5,987,193.5,845.5l-0.2-189.7
H66.9v189.7c0,211.2,171.8,383.1,383.1,383.1s383.1-171.8,383.1-383.1S661.2,462.4,450,462.4h-58.1c-18.5,0-33.5-15-33.5-33.5
s15-33.5,33.5-33.5H450c248.1,0,450,201.9,450,450S698.1,1295.5,450,1295.5z M260.1,655.8l0.2,189.7c0,104.6,85.1,189.7,189.7,189.7
s189.7-85.1,189.7-189.7S554.6,655.8,450,655.8H260.1z"/>
</svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 9.2 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 313 KiB

View file

@ -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()

View file

@ -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