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

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