├── .github ├── ISSUE_TEMPLATE │ └── component-request.md └── PULL_REQUEST_TEMPLATE.md ├── .gitignore ├── .storybook ├── fonts.css ├── main.js └── preview.js ├── .vscode └── settings.json ├── README.md ├── TODO-FUXT.md ├── app ├── app.vue ├── assets │ ├── css │ │ ├── main.css │ │ ├── media.css │ │ ├── transitions.css │ │ └── vars.css │ └── svgs │ │ └── logo-funkhaus.svg ├── components │ ├── global-hamburger.vue │ ├── global-header.vue │ ├── wp-block │ │ ├── core-column.vue │ │ ├── core-columns.vue │ │ ├── core-embed.vue │ │ ├── core-gallery.vue │ │ ├── core-heading.vue │ │ ├── core-image.vue │ │ ├── core-list-item.vue │ │ ├── core-list.vue │ │ ├── core-paragraph.vue │ │ ├── core-quote.vue │ │ ├── core-spacer.vue │ │ └── core-video.vue │ ├── wp-content.vue │ ├── wp-image.vue │ ├── wp-menu-item.vue │ ├── wp-menu.vue │ └── wp-seo.vue ├── composables │ └── useWpFetch.ts ├── error.vue ├── layouts │ └── default.vue ├── middleware │ └── redirect-trailing-slash.global.ts ├── pages │ ├── [slug].vue │ └── index.vue ├── plugins │ ├── browser.client.ts │ └── init.ts ├── stores │ └── site.ts ├── types │ ├── global-hamburger.d.ts │ └── index.d.ts └── utils │ ├── buildVideoEmbedUrl.ts │ ├── decodeHtmlEntities.ts │ ├── delay.ts │ ├── emdashed.ts │ ├── getCookie.ts │ ├── keysToCamelCase.ts │ ├── setCookie.ts │ └── splitAtDash.ts ├── eslint.config.mjs ├── example.env ├── nuxt.config.ts ├── package-lock.json ├── package.json ├── public └── favicon.png ├── stories └── global-header │ ├── Docs.mdx │ └── global-header.stories.ts └── tsconfig.json /.github/ISSUE_TEMPLATE/component-request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Component Request 3 | about: Use this to define a component that a developer can build. 4 | title: Component Request - {ComponentName} - {x}hrs 5 | labels: "" 6 | assignees: "" 7 | --- 8 | 9 | ## Component Description 10 | 11 | This component is used {where} to display {what}. Be sure to explain any variants or hover states. 12 | 13 | ## Design 14 | 15 | Please also see attached screenshots for quick reference. 16 | 17 | - Desktop: https://xd.adobe.com/view/1234-5678/ 18 | - Mobile: https://xd.adobe.com/view/1234-5678/ 19 | 20 | If no mobile designs provided, please use your best judgment for responsiveness. 21 | 22 | ## Slots 23 | 24 | Name and description of any slots needed. 25 | 26 | ## Props 27 | 28 | ``` 29 | props: { 30 | exampleObject: { 31 | // Mock: api.page 32 | type: Object, 33 | default: () => ({}) 34 | }, 35 | exampleArray: { 36 | // Mock: api.pages 37 | type: Array, 38 | default: () => [] 39 | }, 40 | exampleNumber: { 41 | type: Number, 42 | default: 0 43 | }, 44 | exampleString: { 45 | type: String, 46 | default: "" 47 | }, 48 | exampleBoolean: { 49 | type: Boolean, 50 | default: true 51 | } 52 | } 53 | ``` 54 | 55 | ## Developer Tips 56 | 57 | List any developer tips here 58 | 59 | 1. `--color-example` for the font color 60 | 61 | ## Events 62 | 63 | Describe any events that should be emitted by this component. 64 | 65 | 1. `menuOpened` when {something} is clicked on 66 | 67 | ## Required components 68 | 69 | List out any components that are used by this new component. For example, if you are building a grid that is made up of block components. 70 | 71 | 1. `required-component` is used for {what} 72 | 73 | ## Screenshots 74 | 75 | {attach screenshots} 76 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | **Component Created:** {path and filename}.vue from #{issue number} 2 | 3 | **Stories:** {story filename}.stories.ts 4 | 5 | **Imports:** 6 | 7 | - {list any imported components or libraries} 8 | 9 | **Notes:** 10 | 11 | {Any notes about what you built. How does it work? Anything missing?} 12 | 13 | **Time Report:** 14 | 15 | This took me {x} hours to build this. 16 | 17 | **Checklist:** 18 | 19 | - [ ] I double checked it looks like the designs 20 | - [ ] I completed any required mobile breakpoint styling 21 | - [ ] I completed any required hover state styling 22 | - [ ] I included a working Storybook file 23 | - [ ] I included a Story that showed some edge case working correctly (long text, short screen, missing image etc.) 24 | - [ ] I added notes above about how long it took to build this component 25 | - [ ] I assigned this PR to someone to review 26 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Nuxt dev/build outputs 2 | .output 3 | .data 4 | .nuxt 5 | .nitro 6 | .cache 7 | dist 8 | 9 | # Node dependencies 10 | node_modules 11 | 12 | # Logs 13 | logs 14 | *.log 15 | 16 | # Misc 17 | .DS_Store 18 | .fleet 19 | .idea 20 | 21 | # Local env files 22 | .env 23 | .env.* 24 | !.env.example 25 | -------------------------------------------------------------------------------- /.storybook/fonts.css: -------------------------------------------------------------------------------- 1 | /* 2 | Load the fonts in this file so that they are available in Storybook. 3 | 4 | Load fonts in this file using @font-face. 5 | Fonts should go in: ~/public/fonts/ 6 | Example: 7 | @font-face { 8 | font-family: 'My Font'; 9 | src: url('/public/fonts/MyFont.woff2') format('woff2'); 10 | font-style: normal; 11 | font-weight: 400; 12 | font-display: swap; 13 | } 14 | Font Weights: 15 | 100 - Thin 16 | 200 - Extra Light (Ultra Light) 17 | 300 - Light 18 | 400 - Normal 19 | 500 - Medium 20 | 600 - Semi Bold (Demi Bold) 21 | 700 - Bold 22 | 800 - Extra Bold (Ultra Bold) 23 | 900 - Black (Heavy) 24 | */ 25 | -------------------------------------------------------------------------------- /.storybook/main.js: -------------------------------------------------------------------------------- 1 | /** @type { import('storybook-vue').StorybookConfig } */ 2 | const config = { 3 | stories: [ 4 | '../stories/**/*.stories.@(js|jsx|ts|tsx|mdx)', 5 | '../stories/**/*.@(js|jsx|ts|tsx|mdx)' 6 | ], 7 | addons: [ 8 | '@storybook/addon-links', 9 | '@storybook/addon-essentials', 10 | '@chromatic-com/storybook' 11 | ], 12 | framework: { 13 | name: '@storybook-vue/nuxt', 14 | options: {} 15 | }, 16 | docs: { 17 | autodocs: 'tag' 18 | } 19 | } 20 | export default config 21 | -------------------------------------------------------------------------------- /.storybook/preview.js: -------------------------------------------------------------------------------- 1 | // Import fonts.css to load custom fonts for the Storybook preview 2 | import './fonts.css' 3 | 4 | const preview = { 5 | parameters: { 6 | layout: 'fullscreen', // Removes default Storybook padding 7 | controls: { 8 | matchers: { 9 | color: /(background|color)$/i, 10 | date: /Date$/i 11 | } 12 | } 13 | } 14 | } 15 | 16 | export default preview 17 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "prettier.enable": false, 3 | "eslint.useFlatConfig": true, 4 | "editor.codeActionsOnSave": { 5 | "source.fixAll.eslint": "explicit", 6 | "source.organizeImports": "never" 7 | }, 8 | "eslint.codeActionsOnSave.mode": "all", 9 | "eslint.codeAction.showDocumentation": { 10 | "enable": true 11 | }, 12 | "editor.formatOnSave": false, 13 | "eslint.validate": [ 14 | "javascript", 15 | "javascriptreact", 16 | "typescript", 17 | "typescriptreact", 18 | "vue", 19 | "html", 20 | "markdown", 21 | "json", 22 | "jsonc", 23 | "yaml", 24 | "toml", 25 | "css", 26 | "postcss" 27 | ], 28 | } 29 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # fuxt 2 | 3 | A complete Headless WordPress tech stack built on Nuxt 4. 4 | 5 | Works best with the [fuxt-backend](https://github.com/funkhaus/fuxt-backend) WordPress theme and included WordPress optimized components. 6 | 7 | Built by [Funkhaus](http://funkhaus.us/). We normally host on [Flywheel](https://share.getf.ly/n02x5z). 8 | 9 | PS: The name Fuxt comes from [Funkhaus](https://funkhaus.us) and Nuxt. [It's provocative](https://www.youtube.com/watch?v=_eRRab36XLI). 10 | 11 | ## Features 12 | 13 | - TODO 14 | 15 | ## Build Setup 16 | 17 | **This is just a [Nuxt site](https://nuxtjs.org), so it builds and deploys like any other Nuxt project.** 18 | 19 | Works best with the [fuxt-backend](https://github.com/funkhaus/fuxt-backend) WordPress theme as the backend. 20 | 21 | **First step:** Duplicate and rename `.example.env` to `.env`. Define any vars environment needed there. 22 | 23 | ```bash 24 | # install dependencies 25 | $ npm install 26 | 27 | # serve with hot reload at localhost:3000 28 | $ npm run dev 29 | 30 | # serve with hot reload Storybook at localhost:3003 31 | $ npm run storybook 32 | 33 | # build for production and launch server 34 | $ npm run build 35 | $ npm start 36 | 37 | # build Storybook for production 38 | $ npx nuxt storybook build 39 | 40 | # generate static project 41 | $ npm run generate 42 | 43 | ``` 44 | 45 | ## Documentation 46 | 47 | For detailed explanation on how things work, checkout [the wiki](https://github.com/funkhaus/fuxt/wiki). 48 | -------------------------------------------------------------------------------- /TODO-FUXT.md: -------------------------------------------------------------------------------- 1 | ## TODO - Fuxt 2 | - SEO defaults 3 | - Port over default CSS vars 4 | - Remove GQL features 5 | - Get GA plugin working 6 | - Make sure Preview's work 7 | - Get nuxt-fonts working one day! 8 | - Make Storybook working 9 | - Add mock-api.json for easier component testing in Storybook 10 | - add event listener for v-intersected directive (e.g. `@has-entered`) 11 | - Figure out custom gutenberg blocks 12 | - Maybe this helps with better Gutenberg support now: https://developer.wordpress.org/block-editor/reference-guides/packages/packages-block-serialization-default-parser/ 13 | - Get some sort of template and story snippit working: 14 | - https://marketplace.visualstudio.com/items?itemName=exer7um.vue-3-vscode-snippets 15 | - https://medium.com/@tbusser/vue-and-storybook-component-scaffolding-50df77b8d79d 16 | 17 | ## TODO - Fuxt backend 18 | - Update plugin manifest 19 | - Add https://wordpress.org/plugins/wp-openapi/ 20 | - Auto favicon step from WP-Easy into fuxt-backend 21 | - Hide WP Open API menu item for non-Devs 22 | 23 | ## Questions for Conrawl 24 | - See GitHub issues on fuxt repo 25 | - How to fetch more data from a watcher? $fetch vs useWpFetch()? 26 | 27 | ## TODO - Base Components 28 | - WpImage 29 | - TODO Background color 30 | - TODO Focal points and color 31 | - TODO Only load video when coming into view 32 | - TODO Has-loaded of video? 33 | - [x] WpMenu 34 | - VideoStage 35 | - Gutenberg (LLM help?) 36 | - [x] WpSeo 37 | - WpControls 38 | - [x] WpLink (No longer needed actually, nuxt-link does it all now) 39 | - SplitText 40 | 41 | ## Usage Notes 42 | - Install VS Code plugin "ESlint" 43 | - Document vueUse and lodash auto import usage 44 | -------------------------------------------------------------------------------- /app/app.vue: -------------------------------------------------------------------------------- 1 | 38 | 39 | 46 | 47 | 48 | -------------------------------------------------------------------------------- /app/assets/css/main.css: -------------------------------------------------------------------------------- 1 | /* 2 | * This file is included once, for the entire site. 3 | * It's useful for global styles. 4 | */ 5 | 6 | /* Globals */ 7 | html { 8 | font-family: var(--font-primary); 9 | font-size: 16px; 10 | color: var(--color-black); 11 | background-color: var(--color-white); 12 | margin: 0; 13 | -webkit-font-smoothing: antialiased; 14 | -moz-osx-font-smoothing: grayscale; 15 | font-feature-settings: 'kern' 1; 16 | font-kerning: normal; 17 | } 18 | body { 19 | margin: 0; 20 | } 21 | ::selection { 22 | color: var(--color-white); 23 | background: var(--color-black); 24 | } 25 | h1, 26 | h2, 27 | h3, 28 | h4, 29 | h5 { 30 | font-weight: 400; 31 | } 32 | a { 33 | text-decoration: none; 34 | color: inherit; 35 | transition: color 0.4s; 36 | } 37 | button { 38 | appearance: none; 39 | border: none; 40 | background-color: transparent; 41 | cursor: pointer; 42 | outline: none; 43 | margin: 0; 44 | } 45 | .title { 46 | .line { 47 | display: block; 48 | } 49 | } 50 | .overlay { 51 | position: absolute; 52 | top: 0; 53 | left: 0; 54 | right: 0; 55 | bottom: 0; 56 | background: rgba(0, 0, 0, 0.3); 57 | } 58 | .svg { 59 | path { 60 | transition: fill 0.4s; 61 | } 62 | } 63 | 64 | /* Z-Indexs */ 65 | .footer { 66 | z-index: -100; 67 | } 68 | .page { 69 | z-index: 100; 70 | } 71 | .header { 72 | z-index: 200; 73 | } 74 | .menu-panel { 75 | z-index: 300; 76 | } 77 | .global-hamburger { 78 | z-index: 400; 79 | } 80 | 81 | /* Page structure */ 82 | .header { 83 | } 84 | 85 | /* Gutenberg overrides */ 86 | #content { 87 | /* Margins above/below blocks */ 88 | --unit-margin-large: 40px; /* Columns, Gallerys, Blockquotes... */ 89 | --unit-margin-small: 20px; /* Everything except P, H{n} and lists. */ 90 | 91 | /* Gaps on the side of a block (between browser edge) */ 92 | --unit-gutter: var(--unit-gap, 40px); 93 | 94 | /* Gaps inbetween images in gallery */ 95 | --unit-gallery-gap: var(--unit-gap, 40px); 96 | 97 | /* Max width of blocks */ 98 | --unit-max-width-large: 1400px; /* Default, galleries, images, columns */ 99 | --unit-max-width-medium: 1080px; /* Headings (center aligned), Blockquotes */ 100 | --unit-max-width-small: 800px; /* Headings (left or right aligned), P, lists */ 101 | 102 | /* Specific block overrides */ 103 | .core-paragraph + .core-heading { 104 | margin-top: var(--unit-margin-large); 105 | } 106 | 107 | /* Gutenberg breakpoints overrides */ 108 | @media (--lt-phone) { 109 | .core-spacer { 110 | display: none; 111 | } 112 | } 113 | } 114 | 115 | /* Animations */ 116 | /* .bounce { 117 | animation: bounce 1.5s ease-in-out infinite; 118 | } 119 | @keyframes bounce { 120 | 0%, 121 | 20%, 122 | 50%, 123 | 80%, 124 | 100% { 125 | transform: translateY(0) translateX(-50%); 126 | } 127 | 40% { 128 | transform: translateY(-5px) translateX(-50%); 129 | } 130 | 60% { 131 | transform: translateY(5px) translateX(-50%); 132 | } 133 | } */ 134 | -------------------------------------------------------------------------------- /app/assets/css/media.css: -------------------------------------------------------------------------------- 1 | @custom-media --small-viewport (max-width: 600px); 2 | @custom-media --medium-viewport (min-width: 601px) and (max-width: 1200px); 3 | @custom-media --large-viewport (min-width: 1201px); 4 | 5 | @custom-media --gt-cinema only screen and (min-width: 1800px); 6 | @custom-media --lt-cinema only screen and (max-width: 1799px); 7 | @custom-media --lt-tablet only screen and (max-width: 1024px); 8 | @custom-media --lt-phone only screen and (max-width: 850px); 9 | /* @custom-media --phone-landscape: 'only screen and (max-width: 850px) and (orientation: landscape)'; */ 10 | /* @custom-media --pad: "only screen and (min-device-width : 768px) and (max-device-width : 1024px)"; */ 11 | @custom-media --has-hover (hover: hover); 12 | 13 | /* Usage example: 14 | @media (--lt-phone) { 15 | background-color: red; 16 | } 17 | */ 18 | -------------------------------------------------------------------------------- /app/assets/css/transitions.css: -------------------------------------------------------------------------------- 1 | /* Fade */ 2 | .fade-enter-from, 3 | .fade-leave-to { 4 | opacity: 0; 5 | } 6 | .fade-enter-active, 7 | .fade-leave-active { 8 | transition: opacity 0.4s var(--easing-authentic-motion); 9 | } 10 | 11 | /* Slide left & slide right */ 12 | .slide-right-enter-from, 13 | .slide-left-leave-to { 14 | transform: translateX(-100%); 15 | } 16 | .slide-left-enter-from, 17 | .slide-right-leave-to { 18 | transform: translateX(100%); 19 | } 20 | .slide-right-enter-active, 21 | .slide-right-leave-active, 22 | .slide-left-enter-active, 23 | .slide-left-leave-active { 24 | transition: transform 0.4s var(--easing-authentic-motion); 25 | } 26 | 27 | /* Slide up */ 28 | .slide-up-enter-active, 29 | .slide-up-leave-active { 30 | transition: transform 0.4s var(--easing-authentic-motion); 31 | } 32 | .slide-up-enter-from { 33 | transform: translateY(100%); 34 | } 35 | .slide-up-enter-to { 36 | transform: translateY(0%); 37 | } 38 | .slide-up-leave { 39 | transform: translateY(0%); 40 | } 41 | .slide-up-leave-to { 42 | transform: translateY(-100%); 43 | } 44 | 45 | /* Slide down */ 46 | .slide-down-enter-active, 47 | .slide-down-leave-active { 48 | transition: transform 0.4s var(--easing-authentic-motion); 49 | } 50 | .slide-down-enter-from { 51 | transform: translateY(-100%); 52 | } 53 | .slide-down-enter-to { 54 | transform: translateY(0%); 55 | } 56 | .slide-down-leave { 57 | transform: translateY(0%); 58 | } 59 | .slide-down-leave-to { 60 | transform: translateY(100%); 61 | } 62 | 63 | /* Reveal up */ 64 | .reveal-up-enter-active, 65 | .reveal-up-leave-active { 66 | transition: clip-path 0.6s var(--easing-authentic-motion); 67 | > * { 68 | transition: 69 | transform 0.6s var(--easing-authentic-motion), 70 | opacity 0.4s var(--easing-authentic-motion); 71 | } 72 | } 73 | .reveal-up-enter-active { 74 | z-index: 10; 75 | } 76 | .reveal-up-enter-from { 77 | clip-path: polygon(0 100%, 100% 100%, 100% 100%, 0% 100%); 78 | > * { 79 | transform: translateY(100px); 80 | } 81 | } 82 | .reveal-up-enter-to { 83 | clip-path: polygon(0 0, 100% 0, 100% 100%, 0% 100%); 84 | > * { 85 | transform: translateY(0px); 86 | } 87 | } 88 | .reveal-up-leave { 89 | clip-path: polygon(0 0, 100% 0, 100% 100%, 0% 100%); 90 | > * { 91 | transform: translateY(0px); 92 | } 93 | } 94 | .reveal-up-leave-to { 95 | clip-path: polygon(0 0, 100% 0, 100% 0, 0 0); 96 | > * { 97 | transform: translateY(-100px); 98 | } 99 | } 100 | 101 | /* Reveal down */ 102 | .reveal-down-enter-active, 103 | .reveal-down-leave-active { 104 | transition: clip-path 0.6s var(--easing-authentic-motion); 105 | > * { 106 | transition: 107 | transform 0.6s var(--easing-authentic-motion), 108 | opacity 0.4s var(--easing-authentic-motion); 109 | } 110 | } 111 | .reveal-down-enter-active { 112 | z-index: 10; 113 | } 114 | .reveal-down-enter-from { 115 | clip-path: polygon(0 0, 100% 0, 100% 0, 0 0); 116 | > * { 117 | transform: translateY(-100px); 118 | } 119 | } 120 | .reveal-down-enter-to { 121 | clip-path: polygon(0 0, 100% 0, 100% 100%, 0% 100%); 122 | > * { 123 | transform: translateY(0px); 124 | } 125 | } 126 | .reveal-down-leave { 127 | clip-path: polygon(0 0, 100% 0, 100% 100%, 0% 100%); 128 | > * { 129 | transform: translateY(0px); 130 | } 131 | } 132 | .reveal-down-leave-to { 133 | clip-path: polygon(0 100%, 100% 100%, 100% 100%, 0% 100%); 134 | > * { 135 | transform: translateY(100px); 136 | } 137 | } 138 | -------------------------------------------------------------------------------- /app/assets/css/vars.css: -------------------------------------------------------------------------------- 1 | :root { 2 | --color-white: #ffffff; 3 | --color-black: #000000; 4 | 5 | --unit-100vh: 100vh; 6 | --unit-gap: 40px; 7 | --unit-max-width: 1800px; 8 | 9 | --font-primary: sans-serif; 10 | --font-secondary: serif; 11 | 12 | --easing-authentic-motion: cubic-bezier(0.4, 0, 0.2, 1); 13 | 14 | /* Breakpoints */ 15 | @media (--lt-phone) { 16 | --unit-gap: 20px; 17 | } 18 | 19 | /* Handle 100vh on mobile using Dynamic View Height units */ 20 | @supports (height: 100dvh) { 21 | --unit-100vh: 100dvh; 22 | } 23 | } 24 | 25 | /* Set Breakpoint name, this is saved into Pina */ 26 | :root { 27 | --breakpoint-name: 'desktop'; 28 | @media (--gt-cinema) { 29 | --breakpoint-name: 'cinema'; 30 | } 31 | @media (--lt-cinema) { 32 | --breakpoint-name: 'desktop'; 33 | } 34 | @media (--lt-tablet) { 35 | --breakpoint-name: 'tablet'; 36 | } 37 | @media (--lt-phone) { 38 | --breakpoint-name: 'phone'; 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /app/assets/svgs/logo-funkhaus.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /app/components/global-hamburger.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | 34 | 35 | 108 | -------------------------------------------------------------------------------- /app/components/global-header.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | 12 | 13 | 23 | -------------------------------------------------------------------------------- /app/components/wp-block/core-column.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | 32 | 33 | 75 | -------------------------------------------------------------------------------- /app/components/wp-block/core-columns.vue: -------------------------------------------------------------------------------- 1 | 11 | 12 | 31 | 32 | 59 | -------------------------------------------------------------------------------- /app/components/wp-block/core-embed.vue: -------------------------------------------------------------------------------- 1 |