├── apps ├── storybook-ui │ ├── static │ │ └── .gitkeep │ ├── src │ │ ├── index.ts │ │ └── decorators │ │ │ └── index.ts │ ├── postcss.config.js │ ├── tsconfig.json │ ├── .storybook │ │ ├── preview.tsx │ │ └── main.ts │ ├── .eslintrc.js │ ├── package.json │ └── tailwind.config.js ├── docs │ ├── pages │ │ ├── packages │ │ │ ├── _meta.json │ │ │ ├── middleware-chain.mdx │ │ │ ├── storyblok │ │ │ │ └── storyblok-revalidate.mdx │ │ │ └── env.mdx │ │ ├── apps │ │ │ ├── _meta.json │ │ │ ├── web.mdx │ │ │ └── storybook-ui.mdx │ │ ├── best-practice.mdx │ │ ├── shared-components.mdx │ │ ├── getting-started │ │ │ ├── _meta.json │ │ │ ├── gh-actions.mdx │ │ │ ├── requirements.mdx │ │ │ └── deploy-on-vercel.mdx │ │ ├── starter-in-practice.mdx │ │ ├── apps.mdx │ │ ├── _meta.json │ │ └── project-structure.mdx │ ├── next.config.js │ ├── next-env.d.ts │ ├── components │ │ └── youtube.tsx │ ├── tsconfig.json │ ├── package.json │ └── README.md └── web │ ├── postcss.config.js │ ├── src │ ├── app │ │ ├── api │ │ │ ├── draft │ │ │ │ └── route.ts │ │ │ └── revalidate │ │ │ │ └── route.ts │ │ ├── robots.ts │ │ ├── StoryblokProvider.tsx │ │ ├── error.tsx │ │ ├── Providers.tsx │ │ ├── not-found.tsx │ │ ├── page.tsx │ │ └── [...slug] │ │ │ └── page.tsx │ └── middleware.ts │ ├── README.md │ ├── next-env.d.ts │ ├── .eslintrc.js │ ├── .env.example │ ├── tsconfig.json │ ├── package.json │ ├── next.config.mjs │ ├── tailwind.config.js │ └── getStoryblokRedirects.mjs ├── .npmrc ├── packages ├── storyblok-utils │ ├── README.md │ ├── index.ts │ ├── src │ │ ├── types │ │ │ ├── SBProps │ │ │ │ ├── index.ts │ │ │ │ └── SBProps.ts │ │ │ ├── SBTable │ │ │ │ ├── index.ts │ │ │ │ └── SBTable.ts │ │ │ ├── StoryblokAsset │ │ │ │ ├── index.ts │ │ │ │ └── StoryblokAsset.ts │ │ │ ├── StoryblokLink │ │ │ │ ├── index.ts │ │ │ │ └── StoryblokLink.ts │ │ │ └── index.ts │ │ ├── components │ │ │ ├── index.ts │ │ │ └── DynamicRender │ │ │ │ ├── index.ts │ │ │ │ └── DynamicRender.tsx │ │ ├── utils │ │ │ ├── sbEditable │ │ │ │ ├── index.ts │ │ │ │ └── sbEditable.ts │ │ │ ├── getSlugWithAppName │ │ │ │ ├── index.ts │ │ │ │ └── getSlugWithAppName.ts │ │ │ ├── resolveStoryblokStyles │ │ │ │ ├── styles │ │ │ │ │ ├── flex │ │ │ │ │ │ ├── index.ts │ │ │ │ │ │ └── flex.ts │ │ │ │ │ ├── fonts │ │ │ │ │ │ ├── index.ts │ │ │ │ │ │ └── fonts.ts │ │ │ │ │ ├── grid │ │ │ │ │ │ ├── index.ts │ │ │ │ │ │ └── grid.ts │ │ │ │ │ ├── size │ │ │ │ │ │ ├── index.ts │ │ │ │ │ │ └── size.ts │ │ │ │ │ ├── spacing │ │ │ │ │ │ └── index.ts │ │ │ │ │ ├── borderRadius │ │ │ │ │ │ ├── index.ts │ │ │ │ │ │ └── borderRadius.ts │ │ │ │ │ └── index.ts │ │ │ │ └── index.ts │ │ │ ├── getAssetFromStoryblok │ │ │ │ ├── index.ts │ │ │ │ └── getAssetFromStoryblok.ts │ │ │ ├── getSlugWithoutAppName │ │ │ │ ├── index.ts │ │ │ │ └── getSlugWithoutAppName.ts │ │ │ ├── getLinkPropsFromStoryblok │ │ │ │ └── index.ts │ │ │ ├── isSlugExcludedFromRouting │ │ │ │ ├── index.ts │ │ │ │ └── isSlugExcludedFromRouting.ts │ │ │ └── index.ts │ │ └── index.ts │ ├── tsconfig.json │ ├── .eslintrc.js │ └── package.json ├── env │ ├── index.ts │ ├── src │ │ ├── index.ts │ │ └── env │ │ │ └── env.mjs │ ├── tsconfig.json │ ├── .eslintrc.js │ ├── package.json │ └── README.md ├── fonts │ ├── index.ts │ ├── src │ │ ├── index.ts │ │ └── Poppins │ │ │ ├── index.ts │ │ │ └── Poppins.ts │ ├── tsconfig.json │ ├── README.md │ ├── .eslintrc.js │ └── package.json ├── utils │ ├── index.ts │ ├── src │ │ ├── paths │ │ │ ├── index.ts │ │ │ └── paths.ts │ │ ├── helpers │ │ │ ├── cn │ │ │ │ ├── index.ts │ │ │ │ └── cn.ts │ │ │ ├── isArrayWithLength │ │ │ │ ├── index.ts │ │ │ │ └── isArrayWithLength.ts │ │ │ ├── getNextRouteWithDomain │ │ │ │ ├── index.ts │ │ │ │ └── getNextRouteWithDomain.ts │ │ │ └── index.ts │ │ ├── index.ts │ │ └── hooks │ │ │ └── useToggle │ │ │ └── useToggle.ts │ ├── tsconfig.json │ ├── README.md │ ├── .eslintrc.js │ └── package.json ├── next-link │ ├── index.ts │ ├── src │ │ ├── index.ts │ │ └── Link │ │ │ ├── index.ts │ │ │ └── Link.tsx │ ├── tsconfig.json │ ├── .eslintrc.js │ ├── README.md │ └── package.json ├── storyblok-api │ ├── index.ts │ ├── src │ │ ├── hooks │ │ │ ├── index.ts │ │ │ └── useStoryblokSdk │ │ │ │ ├── index.ts │ │ │ │ └── useStoryblokSdk.ts │ │ ├── tags.ts │ │ ├── index.ts │ │ ├── relations.ts │ │ ├── graphql │ │ │ └── query │ │ │ │ ├── getContentNodeQuery.graphql │ │ │ │ ├── getContentNodesQuery.graphql │ │ │ │ └── getConfigItem.graphql │ │ └── api.ts │ ├── .env.example │ ├── tsconfig.json │ ├── README.md │ ├── .eslintrc.js │ ├── package.json │ └── codegen.ts ├── storyblok-seo │ ├── index.ts │ ├── src │ │ ├── components │ │ │ ├── index.ts │ │ │ └── componentsMap.ts │ │ └── index.ts │ ├── tsconfig.json │ ├── README.md │ ├── .eslintrc.js │ └── package.json ├── storyblok-ui │ ├── index.ts │ ├── src │ │ ├── components │ │ │ ├── index.ts │ │ │ ├── elements │ │ │ │ ├── SBCta │ │ │ │ │ ├── index.ts │ │ │ │ │ └── SBCta.tsx │ │ │ │ ├── SBGrid │ │ │ │ │ ├── index.ts │ │ │ │ │ └── SBGrid.tsx │ │ │ │ ├── SBImage │ │ │ │ │ └── index.ts │ │ │ │ ├── SBRow │ │ │ │ │ └── index.ts │ │ │ │ ├── SBTable │ │ │ │ │ ├── index.ts │ │ │ │ │ └── SBTable.tsx │ │ │ │ ├── SBColumn │ │ │ │ │ └── index.ts │ │ │ │ ├── SBContainer │ │ │ │ │ ├── index.ts │ │ │ │ │ └── SBContainer.tsx │ │ │ │ ├── SBRichtext │ │ │ │ │ ├── index.ts │ │ │ │ │ └── SBRichtext.tsx │ │ │ │ ├── SBTypography │ │ │ │ │ └── index.ts │ │ │ │ └── SBThemeModeSwitcher │ │ │ │ │ └── SBThemeModeSwitcher.tsx │ │ │ ├── contentTypes │ │ │ │ ├── SBPage │ │ │ │ │ ├── index.ts │ │ │ │ │ └── SBPage.tsx │ │ │ │ ├── SBFooter │ │ │ │ │ ├── index.ts │ │ │ │ │ └── SBFooter.tsx │ │ │ │ └── SBHeader │ │ │ │ │ ├── index.ts │ │ │ │ │ └── SBHeader.tsx │ │ │ └── componentsMap.ts │ │ └── index.ts │ ├── postcss.config.js │ ├── tsconfig.json │ ├── README.md │ ├── tailwind.config.js │ ├── .eslintrc.js │ └── package.json ├── storybook │ ├── index.ts │ ├── src │ │ ├── index.ts │ │ ├── main.ts │ │ └── preview.ts │ ├── tsconfig.json │ ├── README.md │ ├── .eslintrc.js │ └── package.json ├── ui │ ├── src │ │ ├── Code │ │ │ └── index.ts │ │ ├── Button │ │ │ └── index.ts │ │ ├── Layout │ │ │ ├── index.ts │ │ │ └── Layout.tsx │ │ ├── Popover │ │ │ ├── index.ts │ │ │ └── Popover.tsx │ │ ├── Table │ │ │ └── index.ts │ │ ├── Tooltip │ │ │ ├── index.ts │ │ │ ├── Tooltip.stories.tsx │ │ │ └── Tooltip.tsx │ │ ├── Richtext │ │ │ ├── index.ts │ │ │ └── Richtext.tsx │ │ ├── AspectRatio │ │ │ ├── index.ts │ │ │ └── AspectRatio.tsx │ │ ├── DropdownMenu │ │ │ └── index.ts │ │ ├── ScrollArea │ │ │ ├── index.ts │ │ │ └── ScrollArea.tsx │ │ ├── YoutubeVideo │ │ │ ├── index.ts │ │ │ ├── YoutubeVideo.css │ │ │ └── YoutubeVideo.tsx │ │ ├── ConditionalLink │ │ │ ├── index.ts │ │ │ └── ConditionalLink.tsx │ │ ├── ResponsiveImage │ │ │ ├── index.ts │ │ │ └── ResponsiveImage.tsx │ │ └── Typography │ │ │ ├── index.ts │ │ │ ├── utils │ │ │ └── getTypographyVariantStyles │ │ │ │ └── index.ts │ │ │ ├── Typography.type.ts │ │ │ ├── Typography.tsx │ │ │ └── Typography.stories.tsx │ ├── postcss.config.js │ ├── tsconfig.json │ ├── README.md │ ├── .eslintrc.js │ ├── tailwind.config.js │ ├── index.ts │ └── package.json ├── middleware-chain │ ├── index.ts │ ├── src │ │ ├── index.ts │ │ └── middlewareChain │ │ │ ├── index.ts │ │ │ └── middlewareChain.ts │ ├── tsconfig.json │ ├── README.md │ ├── .eslintrc.js │ └── package.json ├── next-api-fetcher │ ├── index.ts │ ├── src │ │ ├── index.ts │ │ └── apiFetcher │ │ │ └── apiFetcher.ts │ ├── tsconfig.json │ ├── .eslintrc.js │ ├── README.md │ └── package.json ├── storyblok-richtext │ ├── index.ts │ ├── src │ │ ├── components │ │ │ ├── NodeLi │ │ │ │ ├── index.ts │ │ │ │ └── NodeLi.tsx │ │ │ ├── MarkCode │ │ │ │ ├── index.ts │ │ │ │ └── MarkCode.tsx │ │ │ ├── MarkLink │ │ │ │ ├── index.ts │ │ │ │ └── MarkLink.tsx │ │ │ ├── NodeHeading │ │ │ │ ├── index.ts │ │ │ │ └── NodeHeading.tsx │ │ │ ├── NodeImage │ │ │ │ ├── index.ts │ │ │ │ └── NodeImage.tsx │ │ │ ├── NodeCodeblock │ │ │ │ ├── index.ts │ │ │ │ └── NodeCodeblock.tsx │ │ │ ├── StoryblokRichtext │ │ │ │ ├── index.ts │ │ │ │ ├── StoryblokRichtext.tsx │ │ │ │ └── getStoryblokRichText.tsx │ │ │ ├── DefaultBlokResolver │ │ │ │ ├── index.ts │ │ │ │ └── DefaultBlokResolver.tsx │ │ │ └── index.ts │ │ └── index.ts │ ├── postcss.config.js │ ├── tsconfig.json │ ├── README.md │ ├── tailwind.config.js │ ├── .eslintrc.js │ └── package.json ├── storyblok-revalidate │ ├── index.ts │ ├── src │ │ ├── index.ts │ │ └── revalidateHandler │ │ │ └── revalidateHandler.ts │ ├── tsconfig.json │ ├── README.md │ ├── .eslintrc.js │ └── package.json ├── next-themes │ ├── postcss.config.js │ ├── index.ts │ ├── tsconfig.json │ ├── .eslintrc.js │ ├── tailwind.config.js │ ├── README.md │ ├── package.json │ └── src │ │ ├── ThemeProvider │ │ └── ThemeProvider.tsx │ │ └── ThemeModeSwitcher │ │ └── ThemeModeSwitcher.tsx ├── storyblok-setup │ ├── .env.example │ ├── src │ │ ├── utils │ │ │ ├── capitalizeFirstLetter.ts │ │ │ ├── client.ts │ │ │ ├── endpoints.ts │ │ │ ├── color.ts │ │ │ └── checkEnv.ts │ │ ├── components │ │ │ ├── consts.ts │ │ │ ├── data │ │ │ │ ├── themeModeSwitcher.ts │ │ │ │ ├── footer.ts │ │ │ │ ├── header.ts │ │ │ │ ├── page.ts │ │ │ │ ├── seo.ts │ │ │ │ └── redirect.ts │ │ │ ├── components.ts │ │ │ └── setComponents.ts │ │ ├── componentGroup │ │ │ ├── componentsGroup.ts │ │ │ └── setComponentGroups.ts │ │ ├── stories │ │ │ ├── data │ │ │ │ ├── configRootFolders.ts │ │ │ │ └── configContent.ts │ │ │ ├── utils │ │ │ │ ├── createHomepage.ts │ │ │ │ ├── createConfig.ts │ │ │ │ ├── createConfigFolder.ts │ │ │ │ ├── createLayoutsStories.ts │ │ │ │ ├── createRootFolder.ts │ │ │ │ ├── createSpecialPagesStories.ts │ │ │ │ └── createConfigElements.ts │ │ │ └── setStories.ts │ │ └── setup.ts │ ├── tsconfig.json │ ├── README.md │ ├── .eslintrc.cjs │ └── package.json ├── tailwind-config │ ├── postcss.config.js │ ├── README.md │ ├── tailwind.config.js │ ├── package.json │ └── global.css ├── storyblok-preview │ ├── tsconfig.json │ ├── index.ts │ ├── .eslintrc.js │ ├── README.md │ ├── src │ │ └── utils │ │ │ └── isDraftMode │ │ │ └── isDraftMode.ts │ └── package.json ├── eslint-config-custom │ ├── index.js │ ├── README.md │ ├── .eslintrc.js │ ├── order-imports.js │ └── package.json └── tsconfig │ ├── README.md │ ├── package.json │ ├── nextjs.json │ ├── declaration.d.ts │ └── base.json ├── .github ├── ISSUE_TEMPLATE │ └── config.yml ├── pull_request_template.md ├── dependabot.yml ├── SECURITY.md └── workflows │ └── main.yml ├── tsconfig.json ├── .husky ├── pre-commit └── pre-push ├── .vscode └── settings.json ├── prettier.config.js ├── .prettierrc ├── .prettierignore ├── .eslintrc.js ├── .eslintignore ├── contributing.md ├── .gitignore ├── turbo.json ├── LICENSE ├── package.json └── README.md /apps/storybook-ui/static/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | auto-install-peers = true 2 | -------------------------------------------------------------------------------- /packages/storyblok-utils/README.md: -------------------------------------------------------------------------------- 1 | . 2 | -------------------------------------------------------------------------------- /packages/env/index.ts: -------------------------------------------------------------------------------- 1 | export * from './src'; 2 | -------------------------------------------------------------------------------- /packages/fonts/index.ts: -------------------------------------------------------------------------------- 1 | export * from './src'; 2 | -------------------------------------------------------------------------------- /packages/utils/index.ts: -------------------------------------------------------------------------------- 1 | export * from './src'; 2 | -------------------------------------------------------------------------------- /packages/fonts/src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './Poppins'; 2 | -------------------------------------------------------------------------------- /packages/next-link/index.ts: -------------------------------------------------------------------------------- 1 | export * from './src'; 2 | -------------------------------------------------------------------------------- /packages/storyblok-api/index.ts: -------------------------------------------------------------------------------- 1 | export * from './src'; 2 | -------------------------------------------------------------------------------- /packages/storyblok-seo/index.ts: -------------------------------------------------------------------------------- 1 | export * from './src'; 2 | -------------------------------------------------------------------------------- /packages/storyblok-ui/index.ts: -------------------------------------------------------------------------------- 1 | export * from './src'; 2 | -------------------------------------------------------------------------------- /packages/storybook/index.ts: -------------------------------------------------------------------------------- 1 | export * from './src'; 2 | -------------------------------------------------------------------------------- /packages/ui/src/Code/index.ts: -------------------------------------------------------------------------------- 1 | export * from './Code'; 2 | -------------------------------------------------------------------------------- /packages/env/src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './env/env.mjs'; 2 | -------------------------------------------------------------------------------- /packages/middleware-chain/index.ts: -------------------------------------------------------------------------------- 1 | export * from './src'; 2 | -------------------------------------------------------------------------------- /packages/next-api-fetcher/index.ts: -------------------------------------------------------------------------------- 1 | export * from './src'; 2 | -------------------------------------------------------------------------------- /packages/next-link/src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './Link'; 2 | -------------------------------------------------------------------------------- /packages/storyblok-richtext/index.ts: -------------------------------------------------------------------------------- 1 | export * from './src'; 2 | -------------------------------------------------------------------------------- /packages/storyblok-utils/index.ts: -------------------------------------------------------------------------------- 1 | export * from './src'; 2 | -------------------------------------------------------------------------------- /packages/ui/src/Button/index.ts: -------------------------------------------------------------------------------- 1 | export * from './Button'; 2 | -------------------------------------------------------------------------------- /packages/ui/src/Layout/index.ts: -------------------------------------------------------------------------------- 1 | export * from './Layout'; 2 | -------------------------------------------------------------------------------- /packages/ui/src/Popover/index.ts: -------------------------------------------------------------------------------- 1 | export * from './Popover'; 2 | -------------------------------------------------------------------------------- /packages/ui/src/Table/index.ts: -------------------------------------------------------------------------------- 1 | export * from './Table'; 2 | -------------------------------------------------------------------------------- /packages/ui/src/Tooltip/index.ts: -------------------------------------------------------------------------------- 1 | export * from './Tooltip'; 2 | -------------------------------------------------------------------------------- /packages/utils/src/paths/index.ts: -------------------------------------------------------------------------------- 1 | export * from './paths'; 2 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | blank_issues_enabled: false 2 | -------------------------------------------------------------------------------- /apps/storybook-ui/src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './decorators'; 2 | -------------------------------------------------------------------------------- /packages/fonts/src/Poppins/index.ts: -------------------------------------------------------------------------------- 1 | export * from './Poppins'; 2 | -------------------------------------------------------------------------------- /packages/next-link/src/Link/index.ts: -------------------------------------------------------------------------------- 1 | export * from './Link'; 2 | -------------------------------------------------------------------------------- /packages/storyblok-revalidate/index.ts: -------------------------------------------------------------------------------- 1 | export * from './src'; 2 | -------------------------------------------------------------------------------- /packages/ui/src/Richtext/index.ts: -------------------------------------------------------------------------------- 1 | export * from './Richtext'; 2 | -------------------------------------------------------------------------------- /packages/utils/src/helpers/cn/index.ts: -------------------------------------------------------------------------------- 1 | export * from './cn'; 2 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "tsconfig/base.json", 3 | } 4 | -------------------------------------------------------------------------------- /packages/ui/src/AspectRatio/index.ts: -------------------------------------------------------------------------------- 1 | export * from './AspectRatio'; 2 | -------------------------------------------------------------------------------- /packages/ui/src/DropdownMenu/index.ts: -------------------------------------------------------------------------------- 1 | export * from './DropdownMenu'; 2 | -------------------------------------------------------------------------------- /packages/ui/src/ScrollArea/index.ts: -------------------------------------------------------------------------------- 1 | export * from './ScrollArea'; 2 | -------------------------------------------------------------------------------- /packages/ui/src/YoutubeVideo/index.ts: -------------------------------------------------------------------------------- 1 | export * from './YoutubeVideo'; 2 | -------------------------------------------------------------------------------- /apps/docs/pages/packages/_meta.json: -------------------------------------------------------------------------------- 1 | { 2 | "storyblok": "Storyblok" 3 | } 4 | -------------------------------------------------------------------------------- /apps/storybook-ui/src/decorators/index.ts: -------------------------------------------------------------------------------- 1 | export const decorators = ''; 2 | -------------------------------------------------------------------------------- /packages/middleware-chain/src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './middlewareChain'; 2 | -------------------------------------------------------------------------------- /packages/next-api-fetcher/src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './apiFetcher/apiFetcher'; 2 | -------------------------------------------------------------------------------- /packages/storyblok-api/src/hooks/index.ts: -------------------------------------------------------------------------------- 1 | export * from './useStoryblokSdk'; 2 | -------------------------------------------------------------------------------- /packages/storyblok-seo/src/components/index.ts: -------------------------------------------------------------------------------- 1 | export * from './componentsMap'; 2 | -------------------------------------------------------------------------------- /packages/storyblok-ui/src/components/index.ts: -------------------------------------------------------------------------------- 1 | export * from './componentsMap'; 2 | -------------------------------------------------------------------------------- /packages/storyblok-ui/src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './components/componentsMap'; 2 | -------------------------------------------------------------------------------- /packages/storyblok-utils/src/types/SBProps/index.ts: -------------------------------------------------------------------------------- 1 | export * from './SBProps'; 2 | -------------------------------------------------------------------------------- /packages/storyblok-utils/src/types/SBTable/index.ts: -------------------------------------------------------------------------------- 1 | export * from './SBTable'; 2 | -------------------------------------------------------------------------------- /packages/ui/src/ConditionalLink/index.ts: -------------------------------------------------------------------------------- 1 | export * from './ConditionalLink'; 2 | -------------------------------------------------------------------------------- /packages/ui/src/ResponsiveImage/index.ts: -------------------------------------------------------------------------------- 1 | export * from './ResponsiveImage'; 2 | -------------------------------------------------------------------------------- /apps/docs/pages/apps/_meta.json: -------------------------------------------------------------------------------- 1 | { 2 | "web": "", 3 | "storybook-ui": "" 4 | } 5 | -------------------------------------------------------------------------------- /packages/storyblok-richtext/src/components/NodeLi/index.ts: -------------------------------------------------------------------------------- 1 | export * from './NodeLi'; 2 | -------------------------------------------------------------------------------- /packages/storyblok-utils/src/components/index.ts: -------------------------------------------------------------------------------- 1 | export * from './DynamicRender'; 2 | -------------------------------------------------------------------------------- /packages/storyblok-utils/src/utils/sbEditable/index.ts: -------------------------------------------------------------------------------- 1 | export * from './sbEditable'; 2 | -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | . "$(dirname "$0")/_/husky.sh" 3 | 4 | npx lint-staged 5 | -------------------------------------------------------------------------------- /.husky/pre-push: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | . "$(dirname "$0")/_/husky.sh" 3 | 4 | npx lint-staged 5 | -------------------------------------------------------------------------------- /apps/web/postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = require('tailwind-config/postcss.config'); 2 | -------------------------------------------------------------------------------- /packages/middleware-chain/src/middlewareChain/index.ts: -------------------------------------------------------------------------------- 1 | export * from './middlewareChain'; 2 | -------------------------------------------------------------------------------- /packages/storyblok-richtext/src/components/MarkCode/index.ts: -------------------------------------------------------------------------------- 1 | export * from './MarkCode'; 2 | -------------------------------------------------------------------------------- /packages/storyblok-richtext/src/components/MarkLink/index.ts: -------------------------------------------------------------------------------- 1 | export * from './MarkLink'; 2 | -------------------------------------------------------------------------------- /packages/storyblok-richtext/src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './components/StoryblokRichtext'; 2 | -------------------------------------------------------------------------------- /packages/storyblok-ui/src/components/elements/SBCta/index.ts: -------------------------------------------------------------------------------- 1 | export * from './SBCta'; 2 | -------------------------------------------------------------------------------- /packages/storyblok-ui/src/components/elements/SBGrid/index.ts: -------------------------------------------------------------------------------- 1 | export * from './SBGrid'; 2 | -------------------------------------------------------------------------------- /packages/storyblok-ui/src/components/elements/SBImage/index.ts: -------------------------------------------------------------------------------- 1 | export * from './SBImage'; 2 | -------------------------------------------------------------------------------- /packages/storyblok-ui/src/components/elements/SBRow/index.ts: -------------------------------------------------------------------------------- 1 | export * from './SBRow'; 2 | -------------------------------------------------------------------------------- /packages/storyblok-ui/src/components/elements/SBTable/index.ts: -------------------------------------------------------------------------------- 1 | export * from './SBTable'; 2 | -------------------------------------------------------------------------------- /packages/storybook/src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './main'; 2 | export * from './preview'; 3 | -------------------------------------------------------------------------------- /packages/ui/postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = require('tailwind-config/postcss.config'); 2 | -------------------------------------------------------------------------------- /packages/storyblok-api/src/hooks/useStoryblokSdk/index.ts: -------------------------------------------------------------------------------- 1 | export * from './useStoryblokSdk'; 2 | -------------------------------------------------------------------------------- /packages/storyblok-api/src/tags.ts: -------------------------------------------------------------------------------- 1 | export const TAGS = { 2 | SB_CONFIG: 'sb-config', 3 | }; 4 | -------------------------------------------------------------------------------- /packages/storyblok-richtext/src/components/NodeHeading/index.ts: -------------------------------------------------------------------------------- 1 | export * from './NodeHeading'; 2 | -------------------------------------------------------------------------------- /packages/storyblok-richtext/src/components/NodeImage/index.ts: -------------------------------------------------------------------------------- 1 | export * from './NodeImage'; 2 | -------------------------------------------------------------------------------- /packages/storyblok-ui/src/components/contentTypes/SBPage/index.ts: -------------------------------------------------------------------------------- 1 | export * from './SBPage'; 2 | -------------------------------------------------------------------------------- /packages/storyblok-ui/src/components/elements/SBColumn/index.ts: -------------------------------------------------------------------------------- 1 | export * from './SBColumn'; 2 | -------------------------------------------------------------------------------- /packages/storyblok-utils/src/types/StoryblokAsset/index.ts: -------------------------------------------------------------------------------- 1 | export * from './StoryblokAsset'; 2 | -------------------------------------------------------------------------------- /packages/storyblok-utils/src/types/StoryblokLink/index.ts: -------------------------------------------------------------------------------- 1 | export * from './StoryblokLink'; 2 | -------------------------------------------------------------------------------- /packages/utils/src/helpers/isArrayWithLength/index.ts: -------------------------------------------------------------------------------- 1 | export * from './isArrayWithLength'; 2 | -------------------------------------------------------------------------------- /packages/utils/src/paths/paths.ts: -------------------------------------------------------------------------------- 1 | export const paths = { 2 | homepage: '/', 3 | } as const; 4 | -------------------------------------------------------------------------------- /apps/storybook-ui/postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = require('tailwind-config/postcss.config'); 2 | -------------------------------------------------------------------------------- /packages/next-themes/postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = require('tailwind-config/postcss.config'); 2 | -------------------------------------------------------------------------------- /packages/storyblok-revalidate/src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './revalidateHandler/revalidateHandler'; 2 | -------------------------------------------------------------------------------- /packages/storyblok-richtext/src/components/NodeCodeblock/index.ts: -------------------------------------------------------------------------------- 1 | export * from './NodeCodeblock'; 2 | -------------------------------------------------------------------------------- /packages/storyblok-ui/postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = require('tailwind-config/postcss.config'); 2 | -------------------------------------------------------------------------------- /packages/storyblok-ui/src/components/contentTypes/SBFooter/index.ts: -------------------------------------------------------------------------------- 1 | export * from './SBFooter'; 2 | -------------------------------------------------------------------------------- /packages/storyblok-ui/src/components/contentTypes/SBHeader/index.ts: -------------------------------------------------------------------------------- 1 | export * from './SBHeader'; 2 | -------------------------------------------------------------------------------- /packages/storyblok-ui/src/components/elements/SBContainer/index.ts: -------------------------------------------------------------------------------- 1 | export * from './SBContainer'; 2 | -------------------------------------------------------------------------------- /packages/storyblok-ui/src/components/elements/SBRichtext/index.ts: -------------------------------------------------------------------------------- 1 | export * from './SBRichtext'; 2 | -------------------------------------------------------------------------------- /packages/storyblok-ui/src/components/elements/SBTypography/index.ts: -------------------------------------------------------------------------------- 1 | export * from './SBTypography'; 2 | -------------------------------------------------------------------------------- /packages/storyblok-utils/src/components/DynamicRender/index.ts: -------------------------------------------------------------------------------- 1 | export * from './DynamicRender'; 2 | -------------------------------------------------------------------------------- /packages/fonts/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "tsconfig/nextjs.json", 3 | "include": ["."] 4 | } 5 | -------------------------------------------------------------------------------- /packages/storyblok-utils/src/utils/getSlugWithAppName/index.ts: -------------------------------------------------------------------------------- 1 | export * from './getSlugWithAppName'; 2 | -------------------------------------------------------------------------------- /packages/storyblok-utils/src/utils/resolveStoryblokStyles/styles/flex/index.ts: -------------------------------------------------------------------------------- 1 | export * from './flex'; 2 | -------------------------------------------------------------------------------- /packages/storyblok-utils/src/utils/resolveStoryblokStyles/styles/fonts/index.ts: -------------------------------------------------------------------------------- 1 | export * from './fonts'; 2 | -------------------------------------------------------------------------------- /packages/storyblok-utils/src/utils/resolveStoryblokStyles/styles/grid/index.ts: -------------------------------------------------------------------------------- 1 | export * from './grid'; 2 | -------------------------------------------------------------------------------- /packages/storyblok-utils/src/utils/resolveStoryblokStyles/styles/size/index.ts: -------------------------------------------------------------------------------- 1 | export * from './size'; 2 | -------------------------------------------------------------------------------- /packages/ui/src/Typography/index.ts: -------------------------------------------------------------------------------- 1 | export * from './Typography'; 2 | export * from './Typography.type'; 3 | -------------------------------------------------------------------------------- /packages/ui/src/YoutubeVideo/YoutubeVideo.css: -------------------------------------------------------------------------------- 1 | lite-youtube { 2 | max-width: 100% !important; 3 | } 4 | -------------------------------------------------------------------------------- /packages/utils/src/helpers/getNextRouteWithDomain/index.ts: -------------------------------------------------------------------------------- 1 | export * from './getNextRouteWithDomain'; 2 | -------------------------------------------------------------------------------- /packages/storyblok-richtext/postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = require('tailwind-config/postcss.config'); 2 | -------------------------------------------------------------------------------- /packages/storyblok-richtext/src/components/StoryblokRichtext/index.ts: -------------------------------------------------------------------------------- 1 | export * from './StoryblokRichtext'; 2 | -------------------------------------------------------------------------------- /packages/storyblok-utils/src/utils/getAssetFromStoryblok/index.ts: -------------------------------------------------------------------------------- 1 | export * from './getAssetFromStoryblok'; 2 | -------------------------------------------------------------------------------- /packages/storyblok-utils/src/utils/getSlugWithoutAppName/index.ts: -------------------------------------------------------------------------------- 1 | export * from './getSlugWithoutAppName'; 2 | -------------------------------------------------------------------------------- /packages/storyblok-utils/src/utils/resolveStoryblokStyles/styles/spacing/index.ts: -------------------------------------------------------------------------------- 1 | export * from './Spacing'; 2 | -------------------------------------------------------------------------------- /packages/storyblok-richtext/src/components/DefaultBlokResolver/index.ts: -------------------------------------------------------------------------------- 1 | export * from './DefaultBlokResolver'; 2 | -------------------------------------------------------------------------------- /packages/storyblok-utils/src/utils/getLinkPropsFromStoryblok/index.ts: -------------------------------------------------------------------------------- 1 | export * from './getLinkPropsFromStoryblok'; 2 | -------------------------------------------------------------------------------- /packages/storyblok-utils/src/utils/isSlugExcludedFromRouting/index.ts: -------------------------------------------------------------------------------- 1 | export * from './isSlugExcludedFromRouting'; 2 | -------------------------------------------------------------------------------- /packages/ui/src/Typography/utils/getTypographyVariantStyles/index.ts: -------------------------------------------------------------------------------- 1 | export * from './getTypographyVariantStyles'; 2 | -------------------------------------------------------------------------------- /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | ## What? 2 | 3 | Before: 4 | 5 | After: 6 | 7 | ## Why? 8 | 9 | ## How? 10 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "editor.codeActionsOnSave": { 3 | "source.fixAll.eslint": "explicit" 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /packages/storyblok-utils/src/utils/resolveStoryblokStyles/styles/borderRadius/index.ts: -------------------------------------------------------------------------------- 1 | export * from './borderRadius'; 2 | -------------------------------------------------------------------------------- /packages/storyblok-seo/src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './components/componentsMap'; 2 | export * from './utils/getStoryblokSeoData'; 3 | -------------------------------------------------------------------------------- /packages/storyblok-utils/src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './components'; 2 | export * from './utils'; 3 | export * from './types'; 4 | -------------------------------------------------------------------------------- /apps/web/src/app/api/draft/route.ts: -------------------------------------------------------------------------------- 1 | import { draftHandler } from '@natu/storyblok-preview'; 2 | 3 | export { draftHandler as GET }; 4 | -------------------------------------------------------------------------------- /packages/utils/src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './helpers'; 2 | export * from './paths'; 3 | export * from './hooks/useToggle/useToggle'; 4 | -------------------------------------------------------------------------------- /packages/storyblok-utils/src/utils/resolveStoryblokStyles/index.ts: -------------------------------------------------------------------------------- 1 | export * from './resolveStoryblokStyles'; 2 | export * from './styles'; 3 | -------------------------------------------------------------------------------- /packages/next-themes/index.ts: -------------------------------------------------------------------------------- 1 | export * from './src/ThemeProvider/ThemeProvider'; 2 | export * from './src/ThemeModeSwitcher/ThemeModeSwitcher'; 3 | -------------------------------------------------------------------------------- /packages/utils/src/helpers/index.ts: -------------------------------------------------------------------------------- 1 | export * from './cn'; 2 | export * from './getNextRouteWithDomain'; 3 | export * from './isArrayWithLength'; 4 | -------------------------------------------------------------------------------- /packages/storyblok-setup/.env.example: -------------------------------------------------------------------------------- 1 | STORYBLOK_PERSONAL_ACCESS_TOKEN= 2 | STORYBLOK_SPACE_ID= 3 | # 'eu' | 'us' 4 | STORYBLOK_REGION="eu" 5 | 6 | 7 | -------------------------------------------------------------------------------- /packages/tailwind-config/postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | }; 7 | -------------------------------------------------------------------------------- /apps/web/src/app/api/revalidate/route.ts: -------------------------------------------------------------------------------- 1 | import { revalidateHandler } from '@natu/storyblok-revalidate'; 2 | 3 | export { revalidateHandler as POST }; 4 | -------------------------------------------------------------------------------- /packages/storyblok-api/.env.example: -------------------------------------------------------------------------------- 1 | NEXT_PUBLIC_STORYBLOK_API_URL=https://gapi.storyblok.com/v1/api 2 | NEXT_PUBLIC_STORYBLOK_PREVIEW_TOKEN=***** 3 | 4 | 5 | -------------------------------------------------------------------------------- /packages/env/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "tsconfig/nextjs.json", 3 | "include": ["."], 4 | "exclude": ["node_modules", ".next", "coverage", ".turbo"] 5 | } 6 | -------------------------------------------------------------------------------- /packages/ui/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "tsconfig/nextjs.json", 3 | "include": ["."], 4 | "exclude": ["node_modules", ".next", "coverage", ".turbo"] 5 | } 6 | -------------------------------------------------------------------------------- /apps/storybook-ui/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "tsconfig/nextjs.json", 3 | "include": ["."], 4 | "exclude": ["node_modules", ".next", "coverage", ".turbo"] 5 | } 6 | -------------------------------------------------------------------------------- /packages/utils/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "tsconfig/nextjs.json", 3 | "include": ["."], 4 | "exclude": ["node_modules", ".next", "coverage", ".turbo"] 5 | } 6 | -------------------------------------------------------------------------------- /prettier.config.js: -------------------------------------------------------------------------------- 1 | // prettier.config.js 2 | module.exports = { 3 | plugins: ['prettier-plugin-tailwindcss'], 4 | tailwindFunctions: ['clsx', 'cn', 'twMerge'], 5 | } 6 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "tabWidth": 2, 3 | "semi": true, 4 | "trailingComma": "all", 5 | "printWidth": 100, 6 | "arrowParens": "avoid", 7 | "singleQuote": true 8 | } 9 | -------------------------------------------------------------------------------- /apps/web/README.md: -------------------------------------------------------------------------------- 1 | # Web 2 | 3 | This application is the main application of the starter. 4 | 5 | [Full documentation](https://naturaily-starter-docs.vercel.app/apps/web). 6 | -------------------------------------------------------------------------------- /packages/next-link/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "tsconfig/nextjs.json", 3 | "include": ["."], 4 | "exclude": ["node_modules", ".next", "coverage", ".turbo"] 5 | } 6 | -------------------------------------------------------------------------------- /packages/next-themes/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "tsconfig/nextjs.json", 3 | "include": ["."], 4 | "exclude": ["node_modules", ".next", "coverage", ".turbo"] 5 | } 6 | -------------------------------------------------------------------------------- /packages/storyblok-api/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "tsconfig/nextjs.json", 3 | "include": ["."], 4 | "exclude": ["node_modules", ".next", "coverage", ".turbo"] 5 | } 6 | -------------------------------------------------------------------------------- /packages/storyblok-ui/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "tsconfig/nextjs.json", 3 | "include": ["."], 4 | "exclude": ["node_modules", ".next", "coverage", ".turbo"] 5 | } 6 | -------------------------------------------------------------------------------- /packages/storybook/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "tsconfig/nextjs.json", 3 | "include": ["."], 4 | "exclude": ["node_modules", ".next", "coverage", ".turbo"] 5 | } 6 | -------------------------------------------------------------------------------- /packages/middleware-chain/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "tsconfig/nextjs.json", 3 | "include": ["."], 4 | "exclude": ["node_modules", ".next", "coverage", ".turbo"] 5 | } 6 | -------------------------------------------------------------------------------- /packages/next-api-fetcher/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "tsconfig/nextjs.json", 3 | "include": ["."], 4 | "exclude": ["node_modules", ".next", "coverage", ".turbo"], 5 | } 6 | -------------------------------------------------------------------------------- /packages/storyblok-preview/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "tsconfig/nextjs.json", 3 | "include": ["."], 4 | "exclude": ["node_modules", ".next", "coverage", ".turbo"] 5 | } 6 | -------------------------------------------------------------------------------- /packages/storyblok-richtext/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "tsconfig/nextjs.json", 3 | "include": ["."], 4 | "exclude": ["node_modules", ".next", "coverage", ".turbo"] 5 | } 6 | -------------------------------------------------------------------------------- /packages/storyblok-seo/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "tsconfig/nextjs.json", 3 | "include": ["."], 4 | "exclude": ["node_modules", ".next", "coverage", ".turbo"], 5 | } 6 | -------------------------------------------------------------------------------- /packages/storyblok-utils/src/types/index.ts: -------------------------------------------------------------------------------- 1 | export * from './SBProps'; 2 | export * from './StoryblokLink'; 3 | export * from './StoryblokAsset'; 4 | export * from './SBTable'; 5 | -------------------------------------------------------------------------------- /packages/storyblok-utils/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "tsconfig/nextjs.json", 3 | "include": ["."], 4 | "exclude": ["node_modules", ".next", "coverage", ".turbo"] 5 | } 6 | -------------------------------------------------------------------------------- /apps/docs/next.config.js: -------------------------------------------------------------------------------- 1 | const withNextra = require('nextra')({ 2 | theme: 'nextra-theme-docs', 3 | themeConfig: './theme.config.tsx', 4 | }) 5 | 6 | module.exports = withNextra() 7 | -------------------------------------------------------------------------------- /packages/storyblok-revalidate/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "tsconfig/nextjs.json", 3 | "include": ["."], 4 | "exclude": ["node_modules", ".next", "coverage", ".turbo"] 5 | } 6 | -------------------------------------------------------------------------------- /apps/docs/pages/best-practice.mdx: -------------------------------------------------------------------------------- 1 | import { Callout } from 'nextra/components'; 2 | 3 | # Best practice 4 | 5 | 6 | Under construction 7 | 8 | -------------------------------------------------------------------------------- /packages/storyblok-api/src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './generated/graphql'; 2 | export * from './api'; 3 | export * from './hooks'; 4 | export * from './relations'; 5 | export * from './tags'; 6 | -------------------------------------------------------------------------------- /packages/storyblok-setup/src/utils/capitalizeFirstLetter.ts: -------------------------------------------------------------------------------- 1 | export const capitalizeFirstLetter = (string: T) => 2 | string.charAt(0).toUpperCase() + string.slice(1); 3 | -------------------------------------------------------------------------------- /packages/storyblok-richtext/src/components/NodeLi/NodeLi.tsx: -------------------------------------------------------------------------------- 1 | import { ReactNode } from 'react'; 2 | 3 | export const NodeLi = (children: ReactNode) =>
  • {children}
  • ; 4 | -------------------------------------------------------------------------------- /apps/docs/pages/shared-components.mdx: -------------------------------------------------------------------------------- 1 | import { Callout } from 'nextra/components'; 2 | 3 | # Shared components 4 | 5 | 6 | Under construction 7 | 8 | -------------------------------------------------------------------------------- /packages/storyblok-richtext/src/components/index.ts: -------------------------------------------------------------------------------- 1 | export * from './StoryblokRichtext'; 2 | export * from './DefaultBlokResolver'; 3 | export * from './MarkLink'; 4 | export * from './NodeImage'; 5 | -------------------------------------------------------------------------------- /packages/storyblok-ui/README.md: -------------------------------------------------------------------------------- 1 | # @natu/storyblok-ui 2 | 3 | Basic ready-to-use components. 4 | 5 | [Full documentation](https://naturaily-starter-docs.vercel.app/packages/storyblok/storyblok-ui). 6 | -------------------------------------------------------------------------------- /packages/storyblok-seo/src/components/componentsMap.ts: -------------------------------------------------------------------------------- 1 | // Add JSON-LD components here 2 | const elements = {}; 3 | 4 | export const seoComponentsMap: Record = { 5 | ...elements, 6 | }; 7 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: 'npm' 4 | directory: '/' 5 | open-pull-requests-limit: 10 6 | schedule: 7 | interval: daily 8 | time: '12:00' 9 | -------------------------------------------------------------------------------- /apps/docs/pages/getting-started/_meta.json: -------------------------------------------------------------------------------- 1 | { 2 | "requirements": "Requirements", 3 | "installation": "Installation", 4 | "deploy-on-vercel": "Deploy on Vercel", 5 | "gh-actions": "GH Actions" 6 | } 7 | -------------------------------------------------------------------------------- /packages/storyblok-preview/index.ts: -------------------------------------------------------------------------------- 1 | export * from './src/actions/actions'; 2 | export * from './src/components/DraftModeProvider/DraftModeProvider'; 3 | export * from './src/utils/isDraftMode/isDraftMode'; 4 | -------------------------------------------------------------------------------- /packages/ui/README.md: -------------------------------------------------------------------------------- 1 | # @natu/ui 2 | 3 | This package contains all pure React components to be used within the project. 4 | 5 | [Full documentation](https://naturaily-starter-docs.vercel.app/packages/ui). 6 | -------------------------------------------------------------------------------- /apps/docs/pages/starter-in-practice.mdx: -------------------------------------------------------------------------------- 1 | import { Callout } from 'nextra/components'; 2 | 3 | # Naturaily Starter in practice 4 | 5 | 6 | Under construction 7 | 8 | -------------------------------------------------------------------------------- /packages/fonts/README.md: -------------------------------------------------------------------------------- 1 | # @natu/fonts 2 | 3 | This package is using next/font to handle and optimize custom fonts usage. 4 | 5 | [Full documentation](https://naturaily-starter-docs.vercel.app/packages/fonts). 6 | -------------------------------------------------------------------------------- /packages/ui/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | parserOptions: { 3 | tsconfigRootDir: __dirname, 4 | project: './tsconfig.json', 5 | }, 6 | extends: ['eslint-config-custom/eslint-next.js'], 7 | }; 8 | -------------------------------------------------------------------------------- /packages/eslint-config-custom/index.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: ['next', 'turbo', 'prettier'], 3 | rules: { 4 | '@next/next/no-html-link-for-pages': 'off', 5 | 'react/jsx-key': 'off', 6 | }, 7 | }; 8 | -------------------------------------------------------------------------------- /packages/next-themes/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | parserOptions: { 3 | tsconfigRootDir: __dirname, 4 | project: './tsconfig.json', 5 | }, 6 | extends: ['eslint-config-custom/eslint-next.js'], 7 | }; 8 | -------------------------------------------------------------------------------- /packages/utils/README.md: -------------------------------------------------------------------------------- 1 | # @natu/utils 2 | 3 | This package contains utils - small, pure, separate and reusable pieces of code. 4 | 5 | [Full documentation](https://naturaily-starter-docs.vercel.app/packages/utils). 6 | -------------------------------------------------------------------------------- /packages/next-api-fetcher/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | parserOptions: { 3 | tsconfigRootDir: __dirname, 4 | project: './tsconfig.json', 5 | }, 6 | extends: ['eslint-config-custom/eslint-next.js'], 7 | }; 8 | -------------------------------------------------------------------------------- /packages/storyblok-utils/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | parserOptions: { 3 | tsconfigRootDir: __dirname, 4 | project: './tsconfig.json', 5 | }, 6 | extends: ['eslint-config-custom/eslint-next.js'], 7 | }; 8 | -------------------------------------------------------------------------------- /packages/storyblok-api/README.md: -------------------------------------------------------------------------------- 1 | # @natu/storyblok-api 2 | 3 | A package to connect to Storyblok CMS API and fetch data. 4 | 5 | [Full documentation](https://naturaily-starter-docs.vercel.app/packages/storyblok/storyblok-api). 6 | -------------------------------------------------------------------------------- /packages/storyblok-preview/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | parserOptions: { 3 | tsconfigRootDir: __dirname, 4 | project: './tsconfig.json', 5 | }, 6 | extends: ['eslint-config-custom/eslint-next.js'], 7 | }; 8 | -------------------------------------------------------------------------------- /packages/storyblok-richtext/README.md: -------------------------------------------------------------------------------- 1 | # @natu/storyblok-richtext 2 | 3 | Handling the Storyblok-specific rich text. 4 | 5 | [Full documentation](https://naturaily-starter-docs.vercel.app/packages/storyblok/storyblok-richtext). 6 | -------------------------------------------------------------------------------- /packages/ui/src/AspectRatio/AspectRatio.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import * as AspectRatioPrimitive from '@radix-ui/react-aspect-ratio'; 4 | 5 | const AspectRatio = AspectRatioPrimitive.Root; 6 | 7 | export { AspectRatio }; 8 | -------------------------------------------------------------------------------- /packages/utils/src/helpers/getNextRouteWithDomain/getNextRouteWithDomain.ts: -------------------------------------------------------------------------------- 1 | import { env } from '@natu/env'; 2 | 3 | export const getNextRouteWithDomain = (routePath: string) => 4 | `${env.NEXT_PUBLIC_APP_URL}${routePath}`; 5 | -------------------------------------------------------------------------------- /packages/storyblok-preview/README.md: -------------------------------------------------------------------------------- 1 | # @natu/storyblok-preview 2 | 3 | This package is handling Storyblok preview mode. 4 | 5 | [Full documentation](https://naturaily-starter-docs.vercel.app/packages/storyblok/storyblok-preview). 6 | -------------------------------------------------------------------------------- /packages/tailwind-config/README.md: -------------------------------------------------------------------------------- 1 | # tailwind-config 2 | 3 | This package contains default Tailwind config and necessary packages. 4 | 5 | [Full documentation](https://naturaily-starter-docs.vercel.app/packages/tailwind-config). 6 | -------------------------------------------------------------------------------- /apps/web/next-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | 4 | // NOTE: This file should not be edited 5 | // see https://nextjs.org/docs/basic-features/typescript for more information. 6 | -------------------------------------------------------------------------------- /packages/storybook/README.md: -------------------------------------------------------------------------------- 1 | # @natu/storybook 2 | 3 | This package contains basic configuration of Storybook which can be used to create an app. 4 | 5 | [Full documentation](https://naturaily-starter-docs.vercel.app/packages/storybook). 6 | -------------------------------------------------------------------------------- /apps/docs/next-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | 4 | // NOTE: This file should not be edited 5 | // see https://nextjs.org/docs/basic-features/typescript for more information. 6 | -------------------------------------------------------------------------------- /packages/ui/tailwind.config.js: -------------------------------------------------------------------------------- 1 | const defaultConfig = require('tailwind-config/tailwind.config'); 2 | 3 | /** @type {import("tailwindcss").Config} */ 4 | const config = { 5 | presets: [defaultConfig], 6 | }; 7 | 8 | module.exports = config; 9 | -------------------------------------------------------------------------------- /packages/middleware-chain/README.md: -------------------------------------------------------------------------------- 1 | # @natu/middleware-chain 2 | 3 | This package is chaining middlewares one after another if using more of them. 4 | 5 | [Full documentation](https://naturaily-starter-docs.vercel.app/packages/middleware-chain). 6 | -------------------------------------------------------------------------------- /packages/storyblok-utils/src/utils/resolveStoryblokStyles/styles/index.ts: -------------------------------------------------------------------------------- 1 | export * from './fonts'; 2 | export * from './flex'; 3 | export * from './grid'; 4 | export * from './size'; 5 | export * from './spacing'; 6 | export * from './borderRadius'; 7 | -------------------------------------------------------------------------------- /apps/web/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | parserOptions: { 3 | tsconfigRootDir: __dirname, 4 | project: './tsconfig.json', 5 | }, 6 | ignorePatterns: ['next.config.mjs'], 7 | extends: ['eslint-config-custom/eslint-next.js'], 8 | }; 9 | -------------------------------------------------------------------------------- /packages/fonts/src/Poppins/Poppins.ts: -------------------------------------------------------------------------------- 1 | import { Poppins } from 'next/font/google'; 2 | 3 | export const poppinsFont = Poppins({ 4 | weight: ['400', '700'], 5 | subsets: ['latin-ext'], 6 | display: 'swap', 7 | variable: '--font-primary', 8 | }); 9 | -------------------------------------------------------------------------------- /packages/next-themes/tailwind.config.js: -------------------------------------------------------------------------------- 1 | const defaultConfig = require('tailwind-config/tailwind.config'); 2 | 3 | /** @type {import("tailwindcss").Config} */ 4 | const config = { 5 | presets: [defaultConfig], 6 | }; 7 | 8 | module.exports = config; 9 | -------------------------------------------------------------------------------- /packages/storyblok-ui/tailwind.config.js: -------------------------------------------------------------------------------- 1 | const defaultConfig = require('tailwind-config/tailwind.config'); 2 | 3 | /** @type {import("tailwindcss").Config} */ 4 | const config = { 5 | presets: [defaultConfig], 6 | }; 7 | 8 | module.exports = config; 9 | -------------------------------------------------------------------------------- /packages/eslint-config-custom/README.md: -------------------------------------------------------------------------------- 1 | # @natu/eslint-config-custom 2 | 3 | This package provides ESlint configuration which can be extended if needed. 4 | 5 | [Full documentation](https://naturaily-starter-docs.vercel.app/packages/eslint-config-custom). 6 | -------------------------------------------------------------------------------- /packages/storyblok-revalidate/README.md: -------------------------------------------------------------------------------- 1 | # @natu/storyblok-revalidate 2 | 3 | Allows pages update without the need to rebuild the entire project. 4 | 5 | [Full documentation](https://naturaily-starter-docs.vercel.app/packages/storyblok/storyblok-revalidate). 6 | -------------------------------------------------------------------------------- /packages/storyblok-richtext/tailwind.config.js: -------------------------------------------------------------------------------- 1 | const defaultConfig = require('tailwind-config/tailwind.config'); 2 | 3 | /** @type {import("tailwindcss").Config} */ 4 | const config = { 5 | presets: [defaultConfig], 6 | }; 7 | 8 | module.exports = config; 9 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .next 3 | dist 4 | yarn.lock 5 | public 6 | coverage 7 | .gitkeep 8 | *.hbs 9 | storybook-static 10 | .env.example 11 | migrations 12 | favicon.ico 13 | packages/fonts/src/Bariol/files 14 | packages/fonts/src/BariolSerif/files -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | // This tells ESLint to load the config from the package `eslint-config-custom` 4 | extends: ['custom'], 5 | settings: { 6 | next: { 7 | rootDir: ['apps/*/'], 8 | }, 9 | }, 10 | }; 11 | -------------------------------------------------------------------------------- /packages/storyblok-api/src/relations.ts: -------------------------------------------------------------------------------- 1 | const configFooter = 'config.footer'; 2 | const configHeader = 'config.header'; 3 | const configNotFoundPage = 'config.notFoundPage'; 4 | 5 | export const relations = [configFooter, configHeader, configNotFoundPage].join(','); 6 | -------------------------------------------------------------------------------- /packages/storyblok-seo/README.md: -------------------------------------------------------------------------------- 1 | # @natu/storyblok-seo 2 | 3 | Gets the SEO data from Storyblok and generates metadata object which can be used in the Next page. 4 | 5 | [Full documentation](https://naturaily-starter-docs.vercel.app/packages/storyblok/storyblok-seo). 6 | -------------------------------------------------------------------------------- /packages/storyblok-setup/src/utils/client.ts: -------------------------------------------------------------------------------- 1 | import StoryblokClient from 'storyblok-js-client'; 2 | 3 | export const storyblok = new StoryblokClient({ 4 | oauthToken: process.env.STORYBLOK_PERSONAL_ACCESS_TOKEN, 5 | region: process.env.STORYBLOK_REGION, 6 | }); 7 | -------------------------------------------------------------------------------- /packages/env/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | parserOptions: { 3 | tsconfigRootDir: __dirname, 4 | project: './tsconfig.json', 5 | }, 6 | rules: { 7 | 'import/export': 0, 8 | }, 9 | extends: ['eslint-config-custom/eslint-next.js'], 10 | }; 11 | -------------------------------------------------------------------------------- /packages/eslint-config-custom/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | parserOptions: { 3 | tsconfigRootDir: __dirname, 4 | project: './tsconfig.json', 5 | }, 6 | ignorePatterns: ['next.config.mjs'], 7 | extends: ['eslint-config-custom/eslint-next.js'], 8 | }; 9 | -------------------------------------------------------------------------------- /packages/storyblok-setup/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "tsconfig/nextjs.json", 3 | "include": [".", ".eslintrc.cjs"], 4 | "exclude": ["node_modules", ".next", "coverage", ".turbo"], 5 | "compilerOptions": { 6 | "allowImportingTsExtensions": true, 7 | }, 8 | } 9 | -------------------------------------------------------------------------------- /packages/next-api-fetcher/README.md: -------------------------------------------------------------------------------- 1 | # @natu/next-api-fetcher 2 | 3 | This package exposes GraphQL-focused fetcher that can be used to easily retrieve data from GraphQL API. 4 | 5 | [Full documentation ↗](https://naturaily-starter-docs.vercel.app/packages/next-api-fetcher). 6 | -------------------------------------------------------------------------------- /packages/storyblok-preview/src/utils/isDraftMode/isDraftMode.ts: -------------------------------------------------------------------------------- 1 | import { env } from '@natu/env'; 2 | 3 | export const isDraftMode = (draftMode: boolean) => 4 | draftMode || 5 | process.env.NODE_ENV !== 'production' || 6 | env.NEXT_PUBLIC_STORYBLOK_TOKEN_VERSION === 'draft'; 7 | -------------------------------------------------------------------------------- /packages/ui/src/ResponsiveImage/ResponsiveImage.tsx: -------------------------------------------------------------------------------- 1 | import Image, { ImageProps } from 'next/image'; 2 | 3 | export type ResponsiveImageProps = ImageProps; 4 | 5 | export const ResponsiveImage = ({ src, ...rest }: ResponsiveImageProps) => ( 6 | 7 | ); 8 | -------------------------------------------------------------------------------- /packages/storyblok-utils/src/types/StoryblokAsset/StoryblokAsset.ts: -------------------------------------------------------------------------------- 1 | export type StoryblokAsset = { 2 | alt: string; 3 | copyright: string; 4 | fieldtype: string; 5 | filename: string; 6 | focus: string; 7 | id: number; 8 | name: string; 9 | title: string; 10 | }; 11 | -------------------------------------------------------------------------------- /packages/tsconfig/README.md: -------------------------------------------------------------------------------- 1 | # tsconfig 2 | 3 | This package contains default TypeScript config consists of a base configuration extended 4 | with recommended settings for the Next.js application. 5 | 6 | [Full documentation](https://naturaily-starter-docs.vercel.app/packages/tsconfig). 7 | -------------------------------------------------------------------------------- /packages/utils/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | parserOptions: { 3 | tsconfigRootDir: __dirname, 4 | project: './tsconfig.json', 5 | }, 6 | rules: { 7 | 'import/no-extraneous-dependencies': 0, 8 | }, 9 | extends: ['eslint-config-custom/eslint-next.js'], 10 | }; 11 | -------------------------------------------------------------------------------- /apps/storybook-ui/.storybook/preview.tsx: -------------------------------------------------------------------------------- 1 | import { preview } from '@natu/storybook'; 2 | 3 | import 'tailwind-config/global.css'; // Import CSS 4 | 5 | export default preview; 6 | 7 | // This is the place responsible for grouping all decorators from the storybook app 8 | export const decorators = []; 9 | -------------------------------------------------------------------------------- /packages/next-themes/README.md: -------------------------------------------------------------------------------- 1 | # @natu/next-themes 2 | 3 | Themes' (dark, mode, and custom) support for Next.js done using the external [next-themes ↗](https://www.npmjs.com/package/next-themes) package. 4 | 5 | [Full documentation ↗](https://naturaily-starter-docs.vercel.app/packages/next-themes). 6 | -------------------------------------------------------------------------------- /packages/storyblok-api/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | parserOptions: { 3 | tsconfigRootDir: __dirname, 4 | project: './tsconfig.json', 5 | }, 6 | rules: { 7 | 'import/no-extraneous-dependencies': 0, 8 | }, 9 | extends: ['eslint-config-custom/eslint-next.js'], 10 | }; 11 | -------------------------------------------------------------------------------- /apps/storybook-ui/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | parserOptions: { 3 | tsconfigRootDir: __dirname, 4 | project: './tsconfig.json', 5 | }, 6 | rules: { 7 | 'import/no-extraneous-dependencies': 0, 8 | }, 9 | 10 | extends: ['eslint-config-custom/eslint-next.js'], 11 | }; 12 | -------------------------------------------------------------------------------- /packages/middleware-chain/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | parserOptions: { 3 | tsconfigRootDir: __dirname, 4 | project: './tsconfig.json', 5 | }, 6 | rules: { 7 | 'import/no-extraneous-dependencies': 0, 8 | }, 9 | extends: ['eslint-config-custom/eslint-next.js'], 10 | }; 11 | -------------------------------------------------------------------------------- /packages/storybook/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | parserOptions: { 3 | tsconfigRootDir: __dirname, 4 | project: './tsconfig.json', 5 | }, 6 | rules: { 7 | 'import/no-extraneous-dependencies': 0, 8 | }, 9 | 10 | extends: ['eslint-config-custom/eslint-next.js'], 11 | }; 12 | -------------------------------------------------------------------------------- /packages/storyblok-revalidate/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | parserOptions: { 3 | tsconfigRootDir: __dirname, 4 | project: './tsconfig.json', 5 | }, 6 | rules: { 7 | 'import/no-extraneous-dependencies': 0, 8 | }, 9 | extends: ['eslint-config-custom/eslint-next.js'], 10 | }; 11 | -------------------------------------------------------------------------------- /packages/storyblok-setup/README.md: -------------------------------------------------------------------------------- 1 | # @natu/storyblok-setup 2 | 3 | Script setting up predefined: 4 | 5 | - datasources, 6 | - blocks, 7 | - stories 8 | 9 | in Storyblok connected to the project. 10 | 11 | [Full documentation](https://naturaily-starter-docs.vercel.app/packages/storyblok/storyblok-setup). 12 | -------------------------------------------------------------------------------- /apps/storybook-ui/.storybook/main.ts: -------------------------------------------------------------------------------- 1 | import type { StorybookConfig } from '@storybook/nextjs'; 2 | 3 | import { rootConfig } from '@natu/storybook'; 4 | 5 | const config: StorybookConfig = { 6 | ...rootConfig, 7 | stories: ['../../../packages/ui/src/**/*.stories.@(js|jsx|ts|tsx)'], 8 | }; 9 | 10 | export default config; 11 | -------------------------------------------------------------------------------- /packages/next-link/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | parserOptions: { 3 | tsconfigRootDir: __dirname, 4 | project: './tsconfig.json', 5 | }, 6 | rules: { 7 | 'import/no-extraneous-dependencies': 0, 8 | 'no-restricted-imports': 0, 9 | }, 10 | extends: ['eslint-config-custom/eslint-next.js'], 11 | }; 12 | -------------------------------------------------------------------------------- /packages/storyblok-ui/src/components/elements/SBThemeModeSwitcher/SBThemeModeSwitcher.tsx: -------------------------------------------------------------------------------- 1 | import { ThemeModeSwitcher } from '@natu/next-themes'; 2 | import { SBProps, sbEditable } from '@natu/storyblok-utils'; 3 | 4 | export const SBThemeModeSwitcher = ({ blok }: SBProps) => ( 5 | 6 | ); 7 | -------------------------------------------------------------------------------- /packages/tsconfig/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "tsconfig", 3 | "version": "0.0.0", 4 | "private": true, 5 | "files": [ 6 | "base.json", 7 | "nextjs.json" 8 | ], 9 | "devDependencies": { 10 | "@tsconfig/node18": "^18.2.4", 11 | "tsconfig-paths": "4.2.0", 12 | "typescript": "5.4.3" 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /apps/web/src/app/robots.ts: -------------------------------------------------------------------------------- 1 | import { MetadataRoute } from 'next'; 2 | 3 | import { env } from '@natu/env'; 4 | 5 | const Robots = (): MetadataRoute.Robots => ({ 6 | rules: { 7 | userAgent: '*', 8 | allow: '/', 9 | }, 10 | sitemap: `${env.NEXT_PUBLIC_APP_URL}/sitemap.xml`, 11 | }); 12 | 13 | export default Robots; 14 | -------------------------------------------------------------------------------- /packages/storyblok-richtext/src/components/MarkCode/MarkCode.tsx: -------------------------------------------------------------------------------- 1 | import { ReactNode } from 'react'; 2 | 3 | export const MarkCode = (children: ReactNode) => ( 4 | 5 | {children} 6 | 7 | ); 8 | -------------------------------------------------------------------------------- /packages/storyblok-utils/src/utils/index.ts: -------------------------------------------------------------------------------- 1 | export * from './sbEditable'; 2 | export * from './getSlugWithAppName'; 3 | export * from './isSlugExcludedFromRouting'; 4 | export * from './getLinkPropsFromStoryblok'; 5 | export * from './getAssetFromStoryblok'; 6 | export * from './getSlugWithoutAppName'; 7 | export * from './resolveStoryblokStyles'; 8 | -------------------------------------------------------------------------------- /packages/storyblok-seo/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | parserOptions: { 3 | tsconfigRootDir: __dirname, 4 | project: './tsconfig.json', 5 | }, 6 | rules: { 7 | 'import/no-extraneous-dependencies': 0, 8 | '@typescript-eslint/naming-convention': 0, 9 | }, 10 | extends: ['eslint-config-custom/eslint-next.js'], 11 | }; 12 | -------------------------------------------------------------------------------- /packages/storyblok-setup/src/components/consts.ts: -------------------------------------------------------------------------------- 1 | export const TEMPLATE_COLOR = '#fbce41'; 2 | export const ORGANISMS_COLOR = '#2db47d'; 3 | export const MOLECULES_COLOR = '#ff6159'; 4 | export const ATOMS_COLOR = '#ffac00'; 5 | export const LAYOUT_COLOR = '#00b3b0'; 6 | export const SEO_COLOR = '#395ece'; 7 | export const CONTENT_TYPE_COLOR = '#1b243f'; 8 | -------------------------------------------------------------------------------- /packages/storyblok-ui/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | parserOptions: { 3 | tsconfigRootDir: __dirname, 4 | project: './tsconfig.json', 5 | }, 6 | rules: { 7 | 'import/no-extraneous-dependencies': 0, 8 | '@typescript-eslint/naming-convention': 0, 9 | }, 10 | extends: ['eslint-config-custom/eslint-next.js'], 11 | }; 12 | -------------------------------------------------------------------------------- /apps/web/.env.example: -------------------------------------------------------------------------------- 1 | NEXT_PUBLIC_APP_URL=http://localhost:3000 2 | 3 | # storyblok 4 | NEXT_PUBLIC_STORYBLOK_API_URL=https://gapi.storyblok.com/v1/api 5 | NEXT_PUBLIC_STORYBLOK_PREVIEW_TOKEN= 6 | NEXT_PUBLIC_STORYBLOK_MAIN_APP_FOLDER=app 7 | NEXT_PUBLIC_STORYBLOK_EXCLUDED_FOLDERS_FROM_ROUTING=configuration-a93cfcb3 8 | # NEXT_PUBLIC_STORYBLOK_TOKEN_VERSION 9 | -------------------------------------------------------------------------------- /packages/storyblok-richtext/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | parserOptions: { 3 | tsconfigRootDir: __dirname, 4 | project: './tsconfig.json', 5 | }, 6 | rules: { 7 | 'import/no-extraneous-dependencies': 0, 8 | '@typescript-eslint/naming-convention': 0, 9 | }, 10 | extends: ['eslint-config-custom/eslint-next.js'], 11 | }; 12 | -------------------------------------------------------------------------------- /packages/utils/src/helpers/isArrayWithLength/isArrayWithLength.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * If items is an array with a length greater than zero, return true, otherwise return false. 3 | * @param {T[]} [items] - The array to check 4 | */ 5 | export const isArrayWithLength = (items?: T[] | null): items is T[] => 6 | !!items && Array.isArray(items) && items.length > 0; 7 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | **/node_modules/* 2 | node_modules/* 3 | apps/docs/* 4 | **/out/* 5 | **/.next/* 6 | coverage 7 | **/*.js 8 | **/*.json 9 | **/*.css 10 | public 11 | styles 12 | .next 13 | dist 14 | .turbo 15 | README.md 16 | storybook-static 17 | schema.gql 18 | favicon.ico 19 | global.css 20 | styles.module.css 21 | **/*.graphql 22 | getStoryblokRedirects.mjs -------------------------------------------------------------------------------- /apps/docs/components/youtube.tsx: -------------------------------------------------------------------------------- 1 | type YoutubeProps = { 2 | id: string; 3 | }; 4 | 5 | const Youtube = ({ id }: YoutubeProps) => ( 6 | 10 | ); 11 | 12 | export default function MyApp({ id }) { 13 | return ; 14 | } 15 | -------------------------------------------------------------------------------- /packages/fonts/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | parserOptions: { 3 | tsconfigRootDir: __dirname, 4 | project: './tsconfig.json', 5 | }, 6 | rules: { 7 | 'import/no-extraneous-dependencies': 0, 8 | 'import/export': 0, 9 | }, 10 | extends: ['eslint-config-custom/eslint-next.js'], 11 | ignorePatterns: ['src/Bariol/files/*', 'src/BariolSerif/files/*'], 12 | }; 13 | -------------------------------------------------------------------------------- /packages/next-link/README.md: -------------------------------------------------------------------------------- 1 | # @natu/next-link 2 | 3 | The package contains a wrapper for the `next.js` link that handles following external links out of the box: 4 | 5 | - external websites (starting with `http`), 6 | - emails (starting with `mailto`), 7 | - phones (starting with `tel`). 8 | 9 | [Full documentation ↗](https://naturaily-starter-docs.vercel.app/packages/next-link). 10 | -------------------------------------------------------------------------------- /packages/storyblok-utils/src/utils/resolveStoryblokStyles/styles/borderRadius/borderRadius.ts: -------------------------------------------------------------------------------- 1 | export type BorderRadius = 'lg' | 'md' | 'sm' | 'full'; 2 | 3 | // check shadcn preset & global styles variable 4 | export const borderRadiusVariants: Record = { 5 | sm: 'rounded-sm', 6 | md: 'rounded-md', 7 | lg: 'rounded-lg', 8 | full: 'rounded-full', 9 | }; 10 | -------------------------------------------------------------------------------- /packages/storyblok-api/src/graphql/query/getContentNodeQuery.graphql: -------------------------------------------------------------------------------- 1 | query getContentNode($slug: ID!, $relations: String = "", $skipContent: Boolean = false) { 2 | ContentNode(id: $slug, resolve_relations: $relations) { 3 | id 4 | first_published_at 5 | full_slug 6 | name 7 | published_at 8 | slug 9 | uuid 10 | content @skip(if: $skipContent) 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /packages/tsconfig/nextjs.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json.schemastore.org/tsconfig", 3 | "display": "Next.js", 4 | "extends": "./base.json", 5 | "compilerOptions": { 6 | "jsx": "preserve", 7 | "noEmit": true, 8 | "target": "es5" 9 | }, 10 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"], 11 | "exclude": ["node_modules", ".next", ".turbo", "coverage"] 12 | } 13 | -------------------------------------------------------------------------------- /packages/fonts/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@natu/fonts", 3 | "version": "0.0.0", 4 | "private": true, 5 | "main": "./index.ts", 6 | "types": "./index.ts", 7 | "license": "MIT", 8 | "scripts": { 9 | "eslint:format": "eslint src --fix" 10 | }, 11 | "devDependencies": { 12 | "autoprefixer": "10.4.19", 13 | "eslint-config-custom": "*", 14 | "tsconfig": "*" 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /.github/SECURITY.md: -------------------------------------------------------------------------------- 1 | # Reporting Security Vulnerabilities 2 | 3 | Any potential security vulnerabilities can be reported directly at mateusz.hadrys@naturaily.com. The Naturaily Team communicates privately and will work on resolving the issues depending on severity. At the moment there is no bug bounty program. 4 | 5 | Thank you for disclosing your findings responsibly and helping to make Naturaily safe for everyone 🙏. 6 | -------------------------------------------------------------------------------- /apps/docs/pages/apps.mdx: -------------------------------------------------------------------------------- 1 | import { Cards, Card } from 'nextra/components'; 2 | import { LayoutPanelLeft } from 'lucide-react'; 3 | 4 | # Apps 5 | 6 | `Apps` are reusable applications. Each of them is deployable. 7 | 8 | 9 | } title="Web" href="/apps/web" arrow /> 10 | } title="Storybook UI" href="/apps/storybook-ui" arrow /> 11 | 12 | -------------------------------------------------------------------------------- /packages/storyblok-utils/src/utils/resolveStoryblokStyles/styles/size/size.ts: -------------------------------------------------------------------------------- 1 | export type Size = 'full' | 'screen-md' | 'screen-xl' | 'screen-2xl'; 2 | 3 | export const sizeVariants: Record = { 4 | full: 'w-full', 5 | 'screen-md': 'max-w-screen-md w-full mx-auto', // 768px 6 | 'screen-xl': 'max-w-screen-xl w-full mx-auto', // 1280px 7 | 'screen-2xl': 'max-w-screen-2xl w-full mx-auto', // 1536px 8 | }; 9 | -------------------------------------------------------------------------------- /packages/next-link/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@natu/next-link", 3 | "version": "0.0.0", 4 | "private": true, 5 | "main": "./index.ts", 6 | "types": "./index.ts", 7 | "license": "MIT", 8 | "files": [ 9 | "index.ts" 10 | ], 11 | "scripts": { 12 | "eslint:format": "eslint src --fix" 13 | }, 14 | "devDependencies": { 15 | "eslint-config-custom": "*", 16 | "tsconfig": "*" 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /packages/storyblok-setup/.eslintrc.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | parserOptions: { 3 | tsconfigRootDir: __dirname, 4 | project: './tsconfig.json', 5 | }, 6 | rules: { 7 | 'import/export': 0, 8 | 'import/extensions': 0, 9 | 'no-console': 0, 10 | 'no-restricted-syntax': 0, 11 | '@typescript-eslint/ban-ts-comment': 0, 12 | }, 13 | extends: ['eslint-config-custom/eslint-next.js'], 14 | }; 15 | -------------------------------------------------------------------------------- /apps/web/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "baseUrl": ".", 4 | "noEmit": true, 5 | "plugins": [ 6 | { 7 | "name": "next", 8 | }, 9 | ], 10 | "moduleResolution": "node", 11 | }, 12 | "extends": "tsconfig/nextjs.json", 13 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], 14 | "exclude": ["node_modules", ".next", "coverage", ".turbo"], 15 | } 16 | -------------------------------------------------------------------------------- /packages/storyblok-utils/src/types/StoryblokLink/StoryblokLink.ts: -------------------------------------------------------------------------------- 1 | export type StoryblokLinkType = 'url' | 'story' | 'email'; 2 | export type TargetLinkOptions = '_blank'; 3 | 4 | export interface StoryblokLink { 5 | cached_url: string; 6 | email: string | null; 7 | fieldtype: string; 8 | id: string; 9 | linktype: StoryblokLinkType; 10 | url: string | null; 11 | anchor?: string; 12 | target?: TargetLinkOptions; 13 | } 14 | -------------------------------------------------------------------------------- /contributing.md: -------------------------------------------------------------------------------- 1 | # Contributing to Naturaily Storyblok Starter 2 | 3 | We love your input! We want to make contributing to this project as easy and transparent as possible, whether it's: 4 | 5 | - Reporting a bug 6 | - Discussing the current state of the code 7 | - Submitting a fix 8 | - Proposing new features 9 | - Becoming a maintainer 10 | 11 | [Read more in our documentation](https://naturaily-starter-docs.vercel.app/contribution-guide) 12 | -------------------------------------------------------------------------------- /packages/storyblok-ui/src/components/contentTypes/SBPage/SBPage.tsx: -------------------------------------------------------------------------------- 1 | import { BlokItem, DynamicRender, SBProps, sbEditable } from '@natu/storyblok-utils'; 2 | 3 | interface SBPageProps { 4 | body?: BlokItem[]; 5 | } 6 | 7 | export const SBPage = ({ blok }: SBProps) => { 8 | const { body } = blok; 9 | 10 | return ( 11 |
    12 | 13 |
    14 | ); 15 | }; 16 | -------------------------------------------------------------------------------- /packages/ui/src/Typography/Typography.type.ts: -------------------------------------------------------------------------------- 1 | export type TypographyVariant = 2 | | 'text-9xl' 3 | | 'text-8xl' 4 | | 'text-7xl' 5 | | 'text-6xl' 6 | | 'text-5xl' 7 | | 'text-4xl' 8 | | 'text-3xl' 9 | | 'text-2xl' 10 | | 'text-xl' 11 | | 'text-lg' 12 | | 'text-base' 13 | | 'text-sm' 14 | | 'text-xs'; 15 | 16 | export type TypographyVariantProp = 17 | | TypographyVariant 18 | | [TypographyVariant, TypographyVariant?, TypographyVariant?]; 19 | -------------------------------------------------------------------------------- /apps/storybook-ui/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@natu/storybook-ui", 3 | "version": "0.0.0", 4 | "private": true, 5 | "license": "MIT", 6 | "scripts": { 7 | "eslint:format": "eslint src --fix", 8 | "storybook": "storybook dev -p 6006", 9 | "storybook:build": "storybook build" 10 | }, 11 | "devDependencies": { 12 | "eslint-config-custom": "*", 13 | "tailwind-config": "*", 14 | "tsconfig": "*", 15 | "@natu/storybook": "*" 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /packages/storyblok-setup/src/utils/endpoints.ts: -------------------------------------------------------------------------------- 1 | export const SPACE_ENDPOINT = `spaces/${process.env.STORYBLOK_SPACE_ID}`; 2 | 3 | export const STORIES_ENDPOINT = `${SPACE_ENDPOINT}/stories/`; 4 | export const COMPONENTS_ENDPOINT = `${SPACE_ENDPOINT}/components/`; 5 | export const COMPONENTS_GROUP_ENDPOINT = `${SPACE_ENDPOINT}/component_groups/`; 6 | export const DATASOURCES_ENDPOINT = `${SPACE_ENDPOINT}/datasources/`; 7 | export const DATASOURCES_ENTRY_ENDPOINT = `${SPACE_ENDPOINT}/datasource_entries/`; 8 | -------------------------------------------------------------------------------- /packages/storyblok-setup/src/components/data/themeModeSwitcher.ts: -------------------------------------------------------------------------------- 1 | import { ComponentSchema } from '../components.types.ts'; 2 | import { ATOMS_COLOR } from '../consts.ts'; 3 | 4 | export const themeModeSwitcher: ComponentSchema = { 5 | componentGroup: 'atoms', 6 | data: { 7 | name: 'themeModeSwitcher', 8 | display_name: 'Theme mode switcher', 9 | is_nestable: true, 10 | is_root: false, 11 | color: ATOMS_COLOR, 12 | icon: 'block-sticker', 13 | schema: {}, 14 | }, 15 | }; 16 | -------------------------------------------------------------------------------- /packages/storyblok-ui/src/components/contentTypes/SBHeader/SBHeader.tsx: -------------------------------------------------------------------------------- 1 | import { BlokItem, DynamicRender, SBProps, sbEditable } from '@natu/storyblok-utils'; 2 | 3 | interface SBHeaderProps { 4 | body?: BlokItem[]; 5 | } 6 | 7 | export const SBHeader = ({ blok }: SBProps) => { 8 | const { body } = blok; 9 | 10 | return ( 11 |
    12 | 13 |
    14 | ); 15 | }; 16 | -------------------------------------------------------------------------------- /packages/storyblok-richtext/src/components/DefaultBlokResolver/DefaultBlokResolver.tsx: -------------------------------------------------------------------------------- 1 | // @ts-ignore 2 | import { StoryblokComponent } from '@storyblok/react/rsc'; 3 | 4 | type ComponentProps = Record & { _uid: string }; 5 | 6 | export const DefaultBlokResolver = (component: string, props: ComponentProps) => { 7 | const blok = { ...props, component }; 8 | 9 | // eslint-disable-next-line react/destructuring-assignment 10 | return ; 11 | }; 12 | -------------------------------------------------------------------------------- /packages/storyblok-ui/src/components/contentTypes/SBFooter/SBFooter.tsx: -------------------------------------------------------------------------------- 1 | import { BlokItem, DynamicRender, SBProps, sbEditable } from '@natu/storyblok-utils'; 2 | 3 | interface SBFooterProps { 4 | body?: BlokItem[]; 5 | } 6 | 7 | export const SBFooter = ({ blok }: SBProps) => { 8 | const { body } = blok; 9 | 10 | return ( 11 |
    12 | 13 |
    14 | ); 15 | }; 16 | -------------------------------------------------------------------------------- /packages/ui/index.ts: -------------------------------------------------------------------------------- 1 | export * from './src/AspectRatio'; 2 | export * from './src/Button'; 3 | export * from './src/Code'; 4 | export * from './src/ConditionalLink'; 5 | export * from './src/DropdownMenu'; 6 | export * from './src/Layout'; 7 | export * from './src/Popover'; 8 | export * from './src/ResponsiveImage'; 9 | export * from './src/Richtext'; 10 | export * from './src/ScrollArea'; 11 | export * from './src/Table'; 12 | export * from './src/Tooltip'; 13 | export * from './src/Typography'; 14 | export * from './src/YoutubeVideo'; 15 | -------------------------------------------------------------------------------- /packages/utils/src/helpers/cn/cn.ts: -------------------------------------------------------------------------------- 1 | import { clsx, type ClassValue } from 'clsx'; 2 | import { twMerge } from 'tailwind-merge'; 3 | 4 | /** 5 | * The function `cn` is a TypeScript function that merges multiple class lists into a single class 6 | * list. 7 | * @param {ClassValue[]} classLists - classLists is a rest parameter that allows you to pass in 8 | * multiple class lists as arguments. Each class list is represented by the ClassValue type. 9 | */ 10 | export const cn = (...classLists: ClassValue[]) => twMerge(clsx(classLists)); 11 | -------------------------------------------------------------------------------- /apps/storybook-ui/tailwind.config.js: -------------------------------------------------------------------------------- 1 | const defaultConfig = require('tailwind-config/tailwind.config'); 2 | 3 | /** @type {import("tailwindcss").Config} */ 4 | const config = { 5 | content: [ 6 | // ui 7 | '../../packages/ui/src/**/*.{js,ts,jsx,tsx}', 8 | // responsive image 9 | '../../packages/next-responsive-image/src/ResponsiveImage/ResponsiveImage.tsx', 10 | // storybook 11 | './src/decorators/decorators/**/*.{js,ts,jsx,tsx}', 12 | ], 13 | presets: [defaultConfig], 14 | }; 15 | 16 | module.exports = config; 17 | -------------------------------------------------------------------------------- /packages/env/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@natu/env", 3 | "private": true, 4 | "version": "0.0.0", 5 | "main": "./index.ts", 6 | "types": "./index.ts", 7 | "license": "MIT", 8 | "files": [ 9 | "index.ts" 10 | ], 11 | "publishConfig": { 12 | "access": "public" 13 | }, 14 | "scripts": { 15 | "eslint:format": "eslint src --fix" 16 | }, 17 | "devDependencies": { 18 | "eslint-config-custom": "*", 19 | "tsconfig": "*" 20 | }, 21 | "dependencies": { 22 | "@t3-oss/env-nextjs": "0.9.2" 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /packages/next-themes/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@natu/next-themes", 3 | "version": "0.0.0", 4 | "private": true, 5 | "main": "./index.ts", 6 | "types": "./index.ts", 7 | "license": "MIT", 8 | "files": [ 9 | "index.ts" 10 | ], 11 | "scripts": { 12 | "eslint:format": "eslint src --fix" 13 | }, 14 | "devDependencies": { 15 | "eslint-config-custom": "*", 16 | "tsconfig": "*", 17 | "tailwind-config": "*" 18 | }, 19 | "dependencies": { 20 | "@natu/ui": "*", 21 | "next-themes": "0.3.0" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /packages/middleware-chain/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@natu/middleware-chain", 3 | "version": "0.0.0", 4 | "private": true, 5 | "main": "./index.ts", 6 | "types": "./index.ts", 7 | "license": "MIT", 8 | "files": [ 9 | "index.ts" 10 | ], 11 | "publishConfig": { 12 | "access": "public" 13 | }, 14 | "scripts": { 15 | "eslint:format": "eslint src --fix" 16 | }, 17 | "devDependencies": { 18 | "eslint-config-custom": "*", 19 | "tsconfig": "*" 20 | }, 21 | "dependencies": { 22 | "@natu/env": "*" 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /packages/storybook/src/main.ts: -------------------------------------------------------------------------------- 1 | import type { StorybookConfig } from '@storybook/nextjs'; 2 | 3 | export const rootConfig: Omit = { 4 | addons: [ 5 | '@storybook/addon-links', 6 | '@storybook/addon-essentials', 7 | '@storybook/addon-interactions', 8 | '@storybook/addon-a11y', // Additional addon for accessibility 9 | ], 10 | framework: { 11 | name: '@storybook/nextjs', 12 | options: {}, 13 | }, 14 | docs: { 15 | autodocs: false, // Generate docs automatically for each story 16 | }, 17 | }; 18 | -------------------------------------------------------------------------------- /apps/docs/pages/getting-started/gh-actions.mdx: -------------------------------------------------------------------------------- 1 | # Setup GH actions 2 | 3 | We use GitHub Actions for code validation. If you want to use it, add an environmental variable to your repository. 4 | 5 | ```env copy 6 | SKIP_ENV_VALIDATION="true" 7 | ``` 8 | 9 | ![Vercel](https://a.storyblok.com/f/218794/2568x1984/ce98fad700/screenshot-2024-02-19-at-3-57-55-pm.png) 10 | 11 | It disables the validation of environments during GitHub Actions runtime. 12 | 13 | You can edit the GitHub Action in this file: 14 | 15 | ```bash copy 16 | .github/workflows/main.yml 17 | ``` 18 | -------------------------------------------------------------------------------- /packages/next-themes/src/ThemeProvider/ThemeProvider.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import { ThemeProvider as NextThemesProvider } from 'next-themes'; 4 | import { type ThemeProviderProps } from 'next-themes/dist/types'; 5 | 6 | export const ThemeProvider = ({ 7 | children, 8 | defaultTheme, 9 | forcedTheme, 10 | ...props 11 | }: ThemeProviderProps) => ( 12 | 17 | {children} 18 | 19 | ); 20 | -------------------------------------------------------------------------------- /packages/storyblok-setup/src/componentGroup/componentsGroup.ts: -------------------------------------------------------------------------------- 1 | export type ComponentGroupName = 2 | | 'atoms' 3 | | 'molecules' 4 | | 'organisms' 5 | | 'content type' 6 | | 'layout' 7 | | 'seo' 8 | | 'templates'; 9 | 10 | interface ComponentGroupElement { 11 | name: ComponentGroupName; 12 | } 13 | export const componentGroups: ComponentGroupElement[] = [ 14 | { name: 'atoms' }, 15 | { name: 'molecules' }, 16 | { name: 'organisms' }, 17 | { name: 'content type' }, 18 | { name: 'layout' }, 19 | { name: 'seo' }, 20 | { name: 'templates' }, 21 | ]; 22 | -------------------------------------------------------------------------------- /packages/tailwind-config/tailwind.config.js: -------------------------------------------------------------------------------- 1 | const shadcnPreset = require('./shadcd-preset'); 2 | 3 | /** @type {import('tailwindcss').Config} */ 4 | const tailwindConfig = { 5 | mode: 'jit', 6 | darkMode: ['class'], 7 | presets: [shadcnPreset], 8 | theme: { 9 | extend: { 10 | fontFamily: { 11 | primary: ['var(--font-primary)'], 12 | }, 13 | }, 14 | }, 15 | variants: { 16 | extend: {}, 17 | }, 18 | plugins: [require('@tailwindcss/typography'), require('tailwindcss-animate')], 19 | }; 20 | 21 | module.exports = tailwindConfig; 22 | -------------------------------------------------------------------------------- /packages/ui/src/YoutubeVideo/YoutubeVideo.tsx: -------------------------------------------------------------------------------- 1 | // @ts-ignore 2 | import { YouTubeEmbed } from '@next/third-parties/google'; 3 | 4 | import './YoutubeVideo.css'; 5 | 6 | export interface YoutubeVideoProps { 7 | videoid?: string; 8 | params?: string; 9 | className?: string; 10 | playLabel?: string; 11 | } 12 | 13 | export const YoutubeVideo = ({ videoid, params, playLabel, ...rest }: YoutubeVideoProps) => { 14 | if (!videoid) { 15 | return null; 16 | } 17 | 18 | return ; 19 | }; 20 | -------------------------------------------------------------------------------- /packages/storyblok-setup/src/components/data/footer.ts: -------------------------------------------------------------------------------- 1 | import { ComponentSchema } from '../components.types.ts'; 2 | import { LAYOUT_COLOR } from '../consts.ts'; 3 | 4 | export const footer: ComponentSchema = { 5 | componentGroup: 'layout', 6 | data: { 7 | name: 'footer', 8 | display_name: 'Footer', 9 | is_nestable: true, 10 | is_root: true, 11 | color: LAYOUT_COLOR, 12 | icon: 'block-monitor', 13 | schema: { 14 | body: { 15 | type: 'bloks', 16 | pos: 0, 17 | required: false, 18 | }, 19 | }, 20 | }, 21 | }; 22 | -------------------------------------------------------------------------------- /packages/storyblok-setup/src/components/data/header.ts: -------------------------------------------------------------------------------- 1 | import { ComponentSchema } from '../components.types.ts'; 2 | import { LAYOUT_COLOR } from '../consts.ts'; 3 | 4 | export const header: ComponentSchema = { 5 | componentGroup: 'layout', 6 | data: { 7 | name: 'header', 8 | display_name: 'Header', 9 | is_nestable: true, 10 | is_root: true, 11 | color: LAYOUT_COLOR, 12 | icon: 'block-monitor', 13 | schema: { 14 | body: { 15 | type: 'bloks', 16 | pos: 0, 17 | required: false, 18 | }, 19 | }, 20 | }, 21 | }; 22 | -------------------------------------------------------------------------------- /packages/storyblok-preview/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@natu/storyblok-preview", 3 | "version": "0.0.0", 4 | "private": true, 5 | "main": "./index.ts", 6 | "types": "./index.ts", 7 | "license": "MIT", 8 | "files": [ 9 | "index.ts" 10 | ], 11 | "publishConfig": { 12 | "access": "public" 13 | }, 14 | "scripts": { 15 | "eslint:format": "eslint src --fix" 16 | }, 17 | "devDependencies": { 18 | "eslint-config-custom": "*", 19 | "tsconfig": "*" 20 | }, 21 | "dependencies": { 22 | "@natu/env": "*", 23 | "@natu/utils": "*" 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /packages/storyblok-utils/src/types/SBTable/SBTable.ts: -------------------------------------------------------------------------------- 1 | import { SbComponentType } from '../SBProps'; 2 | 3 | interface StoryblokTableColumn extends SbComponentType<'_table_col'> { 4 | value?: string; 5 | } 6 | 7 | interface StoryblokTableBody extends SbComponentType<'_table_row'> { 8 | body?: StoryblokTableColumn[]; 9 | } 10 | 11 | interface StoryblokTableHead extends SbComponentType<'_table_head'> { 12 | value?: string; 13 | } 14 | 15 | export interface StoryblokTable { 16 | fieldtype: 'table'; 17 | thead?: StoryblokTableHead[]; 18 | tbody?: StoryblokTableBody[]; 19 | } 20 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | node_modules 5 | .pnp 6 | .pnp.js 7 | 8 | # testing 9 | coverage 10 | 11 | # next.js 12 | .next/ 13 | out/ 14 | build 15 | 16 | # misc 17 | .DS_Store 18 | *.pem 19 | 20 | # debug 21 | npm-debug.log* 22 | yarn-debug.log* 23 | yarn-error.log* 24 | 25 | # local env files 26 | .env 27 | .env.local 28 | .env.development.local 29 | .env.test.local 30 | .env.production.local 31 | 32 | # turbo 33 | .turbo 34 | 35 | # vercel 36 | .vercel 37 | 38 | # storybook 39 | storybook-static -------------------------------------------------------------------------------- /apps/web/src/middleware.ts: -------------------------------------------------------------------------------- 1 | import { middlewareChain } from '@natu/middleware-chain'; 2 | import { withStoryblokPreviewMiddleware } from '@natu/storyblok-preview'; 3 | 4 | const middlewares = [withStoryblokPreviewMiddleware]; 5 | 6 | export default middlewareChain(middlewares); 7 | 8 | export const config = { 9 | /* 10 | * Match all request paths except for the ones starting with: 11 | * - _next 12 | * - api (API routes) 13 | * - static (static files) 14 | * - favicon.ico (favicon file) 15 | */ 16 | matcher: ['/((?!api|_next/static|_next/image|favicon.ico).*)'], 17 | }; 18 | -------------------------------------------------------------------------------- /packages/middleware-chain/src/middlewareChain/middlewareChain.ts: -------------------------------------------------------------------------------- 1 | 'use server'; 2 | 3 | import { NextResponse } from 'next/server'; 4 | import type { NextMiddleware } from 'next/server'; 5 | 6 | type MiddlewareFactory = (middleware: NextMiddleware) => NextMiddleware; 7 | 8 | export const middlewareChain = (functions: MiddlewareFactory[], index = 0): NextMiddleware => { 9 | const current = functions[index]; 10 | 11 | if (current) { 12 | const next = middlewareChain(functions, index + 1); 13 | 14 | return current(next); 15 | } 16 | 17 | return () => NextResponse.next(); 18 | }; 19 | -------------------------------------------------------------------------------- /apps/docs/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": ["dom", "dom.iterable", "esnext"], 5 | "allowJs": true, 6 | "skipLibCheck": true, 7 | "strict": false, 8 | "forceConsistentCasingInFileNames": true, 9 | "noEmit": true, 10 | "incremental": true, 11 | "esModuleInterop": true, 12 | "module": "esnext", 13 | "moduleResolution": "node", 14 | "resolveJsonModule": true, 15 | "isolatedModules": true, 16 | "jsx": "preserve" 17 | }, 18 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"], 19 | "exclude": ["node_modules"] 20 | } 21 | -------------------------------------------------------------------------------- /packages/storyblok-utils/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@natu/storyblok-utils", 3 | "version": "0.0.0", 4 | "private": true, 5 | "main": "./index.ts", 6 | "types": "./index.ts", 7 | "license": "MIT", 8 | "files": [ 9 | "index.ts" 10 | ], 11 | "publishConfig": { 12 | "access": "public" 13 | }, 14 | "scripts": { 15 | "eslint:format": "eslint src --fix" 16 | }, 17 | "devDependencies": { 18 | "eslint-config-custom": "*", 19 | "tsconfig": "*" 20 | }, 21 | "dependencies": { 22 | "@natu/env": "*", 23 | "@natu/utils": "*", 24 | "@storyblok/react": "3.0.9" 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /packages/utils/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@natu/utils", 3 | "version": "0.0.0", 4 | "private": true, 5 | "main": "./index.ts", 6 | "types": "./index.ts", 7 | "license": "MIT", 8 | "files": [ 9 | "index.ts" 10 | ], 11 | "publishConfig": { 12 | "access": "public" 13 | }, 14 | "scripts": { 15 | "eslint:format": "eslint src --fix" 16 | }, 17 | "devDependencies": { 18 | "eslint-config-custom": "*", 19 | "tsconfig": "*" 20 | }, 21 | "dependencies": { 22 | "@natu/env": "*", 23 | "encoding": "0.1.13", 24 | "tailwind-merge": "2.2.2", 25 | "zod": "3.22.4" 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /packages/storyblok-seo/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@natu/storyblok-seo", 3 | "version": "0.0.0", 4 | "main": "./index.ts", 5 | "types": "./index.ts", 6 | "license": "MIT", 7 | "files": [ 8 | "index.ts" 9 | ], 10 | "publishConfig": { 11 | "access": "public" 12 | }, 13 | "scripts": { 14 | "eslint:format": "eslint src --fix" 15 | }, 16 | "dependencies": { 17 | "@natu/storyblok-api": "*", 18 | "@natu/storyblok-utils": "*", 19 | "@natu/ui": "*", 20 | "@natu/utils": "*" 21 | }, 22 | "devDependencies": { 23 | "eslint-config-custom": "*", 24 | "tsconfig": "*" 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /packages/tailwind-config/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "tailwind-config", 3 | "version": "0.0.0", 4 | "license": "MIT", 5 | "files": [ 6 | "postcss.config.js", 7 | "tailwind.config.js", 8 | "global.css" 9 | ], 10 | "dependencies": {}, 11 | "devDependencies": { 12 | "@tailwindcss/line-clamp": "0.4.4", 13 | "@tailwindcss/typography": "0.5.10", 14 | "autoprefixer": "10.4.19", 15 | "postcss": "8.4.36", 16 | "prettier-plugin-tailwindcss": "0.5.12", 17 | "tailwindcss": "3.4.4", 18 | "tailwindcss-animate": "1.0.7" 19 | }, 20 | "publishConfig": { 21 | "access": "public" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /packages/storyblok-revalidate/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@natu/storyblok-revalidate", 3 | "version": "0.0.0", 4 | "private": true, 5 | "main": "./index.ts", 6 | "types": "./index.ts", 7 | "license": "MIT", 8 | "files": [ 9 | "index.ts" 10 | ], 11 | "publishConfig": { 12 | "access": "public" 13 | }, 14 | "scripts": { 15 | "eslint:format": "eslint src --fix" 16 | }, 17 | "devDependencies": { 18 | "eslint-config-custom": "*", 19 | "tsconfig": "*" 20 | }, 21 | "dependencies": { 22 | "@natu/env": "*", 23 | "@natu/storyblok-utils": "*", 24 | "@natu/storyblok-api": "*" 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /packages/storyblok-utils/src/utils/getSlugWithAppName/getSlugWithAppName.ts: -------------------------------------------------------------------------------- 1 | import { env } from '@natu/env'; 2 | 3 | interface GetSlugWithAppNameInput { 4 | slug?: string; 5 | } 6 | 7 | /** 8 | * The function `getSlugWithAppName` returns a slug with the app name if it exists, otherwise it 9 | * returns just the app name. 10 | * @param {GetSlugWithAppNameInput} - - `slug`: A string representing the slug value. 11 | */ 12 | export const getSlugWithAppName = ({ slug }: GetSlugWithAppNameInput) => 13 | slug 14 | ? `${env.NEXT_PUBLIC_STORYBLOK_MAIN_APP_FOLDER}/${slug.replace('index', '')}` 15 | : env.NEXT_PUBLIC_STORYBLOK_MAIN_APP_FOLDER; 16 | -------------------------------------------------------------------------------- /packages/storybook/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@natu/storybook", 3 | "version": "0.0.0", 4 | "private": true, 5 | "main": "./index.ts", 6 | "types": "./index.ts", 7 | "license": "MIT", 8 | "devDependencies": { 9 | "@storybook/addon-a11y": "8.0.0", 10 | "@storybook/addon-essentials": "8.0.4", 11 | "@storybook/addon-interactions": "8.0.0", 12 | "@storybook/addon-links": "8.0.0", 13 | "@storybook/blocks": "8.0.0", 14 | "@storybook/nextjs": "8.0.0", 15 | "@storybook/react": "8.0.4", 16 | "eslint-config-custom": "*", 17 | "storybook": "8.0.2", 18 | "tailwind-config": "*", 19 | "tsconfig": "*" 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /packages/storyblok-setup/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@natu/storyblok-setup", 3 | "private": true, 4 | "version": "0.0.0", 5 | "type": "module", 6 | "license": "MIT", 7 | "scripts": { 8 | "setup": "ts-node-esm -r dotenv/config ./src/setup.ts && cd ../../ && yarn dev", 9 | "eslint:format": "eslint src --fix" 10 | }, 11 | "dependencies": { 12 | "dotenv": "16.4.5", 13 | "prompts": "2.4.2", 14 | "slugify": "1.6.6", 15 | "storyblok-js-client": "^6.7.1", 16 | "ts-node": "^10.9.2" 17 | }, 18 | "devDependencies": { 19 | "@types/prompts": "2.4.9", 20 | "eslint-config-custom": "*", 21 | "tsconfig": "*" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /packages/tsconfig/declaration.d.ts: -------------------------------------------------------------------------------- 1 | declare module '*.css' { 2 | const content: Record; 3 | export default content; 4 | } 5 | 6 | declare global { 7 | interface Window {} 8 | } 9 | 10 | declare namespace NodeJS { 11 | interface ProcessEnv { 12 | readonly NODE_ENV: 'development' | 'production' | 'test'; 13 | } 14 | 15 | interface RequestInit extends globalThis.RequestInit { 16 | next?: NextFetchRequestConfig | undefined; 17 | } 18 | } 19 | 20 | interface NextFetchRequestConfig { 21 | revalidate?: number | false; 22 | tags?: string[]; 23 | } 24 | 25 | interface RequestInit { 26 | next?: NextFetchRequestConfig | undefined; 27 | } 28 | -------------------------------------------------------------------------------- /packages/ui/src/Richtext/Richtext.tsx: -------------------------------------------------------------------------------- 1 | import { ComponentProps, ReactNode } from 'react'; 2 | 3 | import { cn } from '@natu/utils'; 4 | 5 | export interface RichtextProps extends ComponentProps<'div'> { 6 | children?: ReactNode; 7 | html?: string | TrustedHTML; 8 | } 9 | 10 | export const Richtext = ({ className, children, html, ...rest }: RichtextProps) => { 11 | const styles = cn('prose max-w-none dark:prose-invert', className); 12 | 13 | if (html) { 14 | return
    ; 15 | } 16 | 17 | return ( 18 |
    19 | {children} 20 |
    21 | ); 22 | }; 23 | -------------------------------------------------------------------------------- /packages/storyblok-richtext/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@natu/storyblok-richtext", 3 | "version": "0.0.0", 4 | "main": "./index.ts", 5 | "types": "./index.ts", 6 | "license": "MIT", 7 | "files": [ 8 | "index.ts" 9 | ], 10 | "publishConfig": { 11 | "access": "public" 12 | }, 13 | "scripts": { 14 | "eslint:format": "eslint src --fix" 15 | }, 16 | "dependencies": { 17 | "@natu/next-link": "*", 18 | "@natu/storyblok-utils": "*", 19 | "@natu/ui": "*", 20 | "storyblok-rich-text-react-renderer": "2.9.2" 21 | }, 22 | "devDependencies": { 23 | "eslint-config-custom": "*", 24 | "tailwind-config": "*", 25 | "tsconfig": "*" 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /packages/ui/src/Layout/Layout.tsx: -------------------------------------------------------------------------------- 1 | import { ComponentProps, ReactNode } from 'react'; 2 | 3 | import { cn } from '@natu/utils'; 4 | 5 | export interface LayoutProps extends ComponentProps<'div'> { 6 | children?: ReactNode; 7 | header?: ReactNode; 8 | footer?: ReactNode; 9 | } 10 | 11 | export const Layout = ({ 12 | children, 13 | className, 14 | header = null, 15 | footer = null, 16 | ...rest 17 | }: LayoutProps) => { 18 | const wrapperStyles = cn('flex flex-col min-h-screen', className); 19 | 20 | return ( 21 |
    22 | {header} 23 |
    {children}
    24 | {footer} 25 |
    26 | ); 27 | }; 28 | -------------------------------------------------------------------------------- /apps/docs/pages/apps/web.mdx: -------------------------------------------------------------------------------- 1 | # Web 2 | 3 | This application is the main application of the starter, which: 4 | 5 | - is the web app created using imported packages of this starter, 6 | - has a minimal amount of logic directly in it. 7 | 8 | Ready app can be deployed, see [how to deploy apps on Vercel](/getting-started/deploy-on-vercel). 9 | 10 | ## Available scripts 11 | 12 | Build ready-to-deploy, optimized production version of an app: 13 | 14 | ```bash copy 15 | yarn build 16 | ``` 17 | 18 | Run app locally: 19 | 20 | 1. in **development mode** 21 | 22 | ```bash copy 23 | yarn dev 24 | ``` 25 | 26 | 2. in **production mode** (needs building an app first) 27 | 28 | ```bash copy 29 | yarn start 30 | ``` 31 | -------------------------------------------------------------------------------- /packages/storyblok-api/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@natu/storyblok-api", 3 | "version": "0.0.0", 4 | "private": true, 5 | "main": "./index.ts", 6 | "types": "./index.ts", 7 | "license": "MIT", 8 | "files": [ 9 | "index.ts" 10 | ], 11 | "publishConfig": { 12 | "access": "public" 13 | }, 14 | "scripts": { 15 | "eslint:format": "eslint src --fix", 16 | "codegen": "yarn graphql-code-generator --config ./codegen.ts -r dotenv/config" 17 | }, 18 | "devDependencies": { 19 | "eslint-config-custom": "*", 20 | "tsconfig": "*" 21 | }, 22 | "dependencies": { 23 | "@natu/env": "*", 24 | "@natu/next-api-fetcher": "*", 25 | "@natu/storyblok-preview": "*" 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /apps/docs/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "docs", 3 | "version": "0.0.1", 4 | "description": "Nextra - Naturaily & storyblok docs.", 5 | "scripts": { 6 | "dev:docs": "next dev", 7 | "build": "next build", 8 | "start": "next start" 9 | }, 10 | "author": "Shu Ding ", 11 | "license": "MIT", 12 | "dependencies": { 13 | "lucide-react": "0.359.0", 14 | "next": "^14.2.4", 15 | "nextra": "latest", 16 | "nextra-theme-docs": "latest", 17 | "react": "^18.2.0", 18 | "react-dom": "^18.2.0" 19 | }, 20 | "devDependencies": { 21 | "@types/node": "20.11.30", 22 | "eslint-config-custom": "*", 23 | "eslint-plugin-mdx": "3.1.5", 24 | "tsconfig": "*" 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /packages/storyblok-richtext/src/components/StoryblokRichtext/StoryblokRichtext.tsx: -------------------------------------------------------------------------------- 1 | import { ISbRichtext } from '@storyblok/react'; 2 | import { ComponentProps } from 'react'; 3 | 4 | import { Richtext } from '@natu/ui'; 5 | 6 | import { getStoryblokRichText } from './getStoryblokRichText'; 7 | 8 | export type StoryblokRichTextData = ISbRichtext; 9 | 10 | interface StoryblokRichtextProps extends ComponentProps<'div'> { 11 | data?: StoryblokRichTextData; 12 | } 13 | 14 | export const StoryblokRichtext = ({ data, ...rest }: StoryblokRichtextProps) => { 15 | if (!data) { 16 | return null; 17 | } 18 | 19 | const content = getStoryblokRichText(data); 20 | 21 | return {content}; 22 | }; 23 | -------------------------------------------------------------------------------- /apps/web/src/app/StoryblokProvider.tsx: -------------------------------------------------------------------------------- 1 | // @ts-ignore 2 | import StoryblokBridgeLoader from '@storyblok/react/bridge-loader'; 3 | // @ts-ignore 4 | import { storyblokInit } from '@storyblok/react/rsc'; 5 | import React, { ReactNode } from 'react'; 6 | 7 | import { env } from '@natu/env'; 8 | import { componentsMap } from '@natu/storyblok-ui'; 9 | 10 | storyblokInit({ 11 | // @ts-ignore 12 | accessToken: env.NEXT_PUBLIC_STORYBLOK_PREVIEW_TOKEN, 13 | components: componentsMap, 14 | }); 15 | 16 | interface StoryblokProviderProps { 17 | children?: ReactNode; 18 | } 19 | 20 | export const StoryblokProvider = ({ children }: StoryblokProviderProps) => ( 21 | <> 22 | {children} 23 | 24 | 25 | ); 26 | -------------------------------------------------------------------------------- /packages/storyblok-ui/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@natu/storyblok-ui", 3 | "version": "0.0.0", 4 | "main": "./index.ts", 5 | "types": "./index.ts", 6 | "license": "MIT", 7 | "files": [ 8 | "index.ts" 9 | ], 10 | "publishConfig": { 11 | "access": "public" 12 | }, 13 | "scripts": { 14 | "eslint:format": "eslint src --fix" 15 | }, 16 | "dependencies": { 17 | "@natu/storyblok-api": "*", 18 | "@natu/storyblok-richtext": "*", 19 | "@natu/storyblok-utils": "*", 20 | "@natu/ui": "*", 21 | "@natu/next-themes": "*", 22 | "@natu/utils": "*" 23 | }, 24 | "devDependencies": { 25 | "eslint-config-custom": "*", 26 | "tailwind-config": "*", 27 | "tsconfig": "*" 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /apps/web/src/app/error.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import { Button } from '@natu/ui'; 4 | 5 | interface GlobalErrorProps { 6 | reset: () => void; 7 | } 8 | 9 | const GlobalError = ({ reset }: GlobalErrorProps) => ( 10 |
    11 |

    Oh no!

    12 |

    13 | There was an issue with our app. This could be a temporary issue, please try your action 14 | again. 15 |

    16 | 19 |
    20 | ); 21 | 22 | export default GlobalError; 23 | -------------------------------------------------------------------------------- /packages/eslint-config-custom/order-imports.js: -------------------------------------------------------------------------------- 1 | const appPrefix = '@natu'; 2 | 3 | module.exports = [ 4 | `/^${appPrefix}/ui/`, 5 | `/^${appPrefix}/next-link/`, 6 | `/^${appPrefix}/utils/`, 7 | `/^${appPrefix}/react-query-gql/`, 8 | `/^${appPrefix}/next-api-fetcher/`, 9 | `/^${appPrefix}/storyblok-preview/`, 10 | `/^${appPrefix}/storyblok-utils/`, 11 | `/^${appPrefix}/storyblok-api/`, 12 | `/^${appPrefix}/storyblok-ui/`, 13 | `/^${appPrefix}/storyblok-richtext/`, 14 | `/^${appPrefix}/env/`, 15 | `/^${appPrefix}/middleware-chain/`, 16 | `/^${appPrefix}/storybook/`, 17 | `/^${appPrefix}/fonts/`, 18 | `/^${appPrefix}/next-themes/`, 19 | `/^${appPrefix}/storyblok-revalidate/`, 20 | `/^${appPrefix}/storyblok-seo/`, 21 | ]; 22 | -------------------------------------------------------------------------------- /packages/storyblok-api/src/hooks/useStoryblokSdk/useStoryblokSdk.ts: -------------------------------------------------------------------------------- 1 | import { useDraftModeContext } from '@natu/storyblok-preview'; 2 | 3 | import { getStoryblokApi } from '../../api'; 4 | import { Sdk } from '../../sdk'; 5 | 6 | /** 7 | * This function returns the Storyblok SDK with draft mode enabled or disabled based on the current 8 | * context. 9 | * @returns The `useStoryblokSdk` function returns an instance of the `Sdk` class, which is obtained by 10 | * calling the `getStoryblokApi` function with the `draftMode` value obtained from the `useDraftModeContext` 11 | * hook. 12 | */ 13 | export const useStoryblokSdk = (): Sdk => { 14 | const { draftMode } = useDraftModeContext(); 15 | 16 | return getStoryblokApi({ draftMode }); 17 | }; 18 | -------------------------------------------------------------------------------- /packages/storyblok-richtext/src/components/NodeImage/NodeImage.tsx: -------------------------------------------------------------------------------- 1 | import { ReactNode } from 'react'; 2 | 3 | import { StoryblokAsset, getAssetFromStoryblok } from '@natu/storyblok-utils'; 4 | import { ResponsiveImage } from '@natu/ui'; 5 | 6 | interface NodeImageProps { 7 | alt?: string; 8 | title?: string; 9 | src?: string; 10 | } 11 | 12 | export const NodeImage = (_: ReactNode, { alt, src: imageSrc, title }: NodeImageProps) => { 13 | const storyblokImage = { 14 | alt, 15 | filename: imageSrc, 16 | title, 17 | } as StoryblokAsset; 18 | 19 | const image = getAssetFromStoryblok(storyblokImage, { type: 'image' }); 20 | 21 | if (!image.src) { 22 | return null; 23 | } 24 | 25 | return ; 26 | }; 27 | -------------------------------------------------------------------------------- /packages/ui/src/ConditionalLink/ConditionalLink.tsx: -------------------------------------------------------------------------------- 1 | import { AnchorHTMLAttributes, Children, cloneElement, ReactElement } from 'react'; 2 | 3 | import { Link } from '@natu/next-link'; 4 | import { cn } from '@natu/utils'; 5 | 6 | export interface ConditionalLinkProps extends AnchorHTMLAttributes {} 7 | 8 | export const ConditionalLink = ({ href, children, className, ...rest }: ConditionalLinkProps) => { 9 | if (href) { 10 | return ( 11 | 12 | {children} 13 | 14 | ); 15 | } 16 | 17 | const child = Children.only(children); 18 | 19 | return cloneElement(child as ReactElement, { 20 | className, 21 | ...rest, 22 | }); 23 | }; 24 | -------------------------------------------------------------------------------- /packages/storyblok-setup/src/utils/color.ts: -------------------------------------------------------------------------------- 1 | const styles = { 2 | // got these from playing around with what I found from: 3 | // https://github.com/istanbuljs/istanbuljs/blob/0f328fd0896417ccb2085f4b7888dd8e167ba3fa/packages/istanbul-lib-report/lib/file-writer.js#L84-L96 4 | // they're the best I could find that works well for light or dark terminals 5 | success: { open: '\u001b[32;1m', close: '\u001b[0m' }, 6 | danger: { open: '\u001b[31;1m', close: '\u001b[0m' }, 7 | info: { open: '\u001b[36;1m', close: '\u001b[0m' }, 8 | subtitle: { open: '\u001b[2;1m', close: '\u001b[0m' }, 9 | }; 10 | 11 | type Color = keyof typeof styles; 12 | 13 | export const color = (modifier: Color, string: string) => 14 | styles[modifier].open + string + styles[modifier].close; 15 | -------------------------------------------------------------------------------- /packages/next-api-fetcher/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@natu/next-api-fetcher", 3 | "version": "0.0.0", 4 | "private": true, 5 | "main": "./index.ts", 6 | "types": "./index.ts", 7 | "license": "MIT", 8 | "files": [ 9 | "index.ts" 10 | ], 11 | "scripts": { 12 | "eslint:format": "eslint src --fix" 13 | }, 14 | "devDependencies": { 15 | "@graphql-codegen/add": "5.0.2", 16 | "@graphql-codegen/cli": "5.0.2", 17 | "@graphql-codegen/typescript": "4.0.6", 18 | "@graphql-codegen/typescript-document-nodes": "4.0.6", 19 | "@graphql-codegen/typescript-operations": "4.2.0", 20 | "cross-env": "7.0.3", 21 | "eslint-config-custom": "*", 22 | "tsconfig": "*" 23 | }, 24 | "dependencies": { 25 | "graphql": "16.8.1" 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /packages/storyblok-setup/src/stories/data/configRootFolders.ts: -------------------------------------------------------------------------------- 1 | interface FolderSchema { 2 | name: string; 3 | slug: string; 4 | is_folder?: boolean; 5 | // @ts-ignore 6 | default_root: 'page' | 'redirect' | 'footer' | 'header'; 7 | parent_id?: number; 8 | disble_fe_editor?: boolean; 9 | } 10 | 11 | export const configRootFolders: FolderSchema[] = [ 12 | { 13 | name: 'Redirects', 14 | slug: 'redirects', 15 | is_folder: true, 16 | default_root: 'redirect', 17 | disble_fe_editor: true, 18 | }, 19 | { 20 | name: 'Special pages', 21 | slug: 'special-pages', 22 | default_root: 'page', 23 | is_folder: true, 24 | }, 25 | { 26 | name: 'Layout', 27 | slug: 'layout', 28 | default_root: 'page', 29 | is_folder: true, 30 | }, 31 | ]; 32 | -------------------------------------------------------------------------------- /apps/docs/README.md: -------------------------------------------------------------------------------- 1 | # Nextra Docs Template 2 | 3 | This is a template for creating documentation with [Nextra](https://nextra.site). 4 | 5 | [**Live Demo →**](https://nextra-docs-template.vercel.app) 6 | 7 | [![](.github/screenshot.png)](https://nextra-docs-template.vercel.app) 8 | 9 | ## Quick Start 10 | 11 | Click the button to clone this repository and deploy it on Vercel: 12 | 13 | [![](https://vercel.com/button)](https://vercel.com/new/clone?s=https%3A%2F%2Fgithub.com%2Fshuding%2Fnextra-docs-template&showOptionalTeamCreation=false) 14 | 15 | ## Local Development 16 | 17 | First, run `pnpm i` to install the dependencies. 18 | 19 | Then, run `pnpm dev` to start the development server and visit localhost:3000. 20 | 21 | ## License 22 | 23 | This project is licensed under the MIT License. 24 | -------------------------------------------------------------------------------- /packages/next-link/src/Link/Link.tsx: -------------------------------------------------------------------------------- 1 | import NextLink, { LinkProps as NextLinkProps } from 'next/link'; 2 | import { HTMLAttributes, forwardRef } from 'react'; 3 | 4 | export type LinkProps = NextLinkProps & HTMLAttributes; 5 | 6 | const externalLinkPrefix = ['http', 'mailto', 'tel']; 7 | 8 | export const Link = forwardRef(({ children, href, ...rest }, ref) => { 9 | const isExternalLink = 10 | typeof href === 'string' && externalLinkPrefix.some(prefix => href.startsWith(prefix)); 11 | 12 | if (isExternalLink) { 13 | return ( 14 | 15 | {children} 16 | 17 | ); 18 | } 19 | 20 | return ( 21 | 22 | {children} 23 | 24 | ); 25 | }); 26 | -------------------------------------------------------------------------------- /packages/tsconfig/base.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json.schemastore.org/tsconfig", 3 | "display": "Default", 4 | "compilerOptions": { 5 | "allowJs": true, 6 | "composite": false, 7 | "declaration": true, 8 | "declarationMap": true, 9 | "esModuleInterop": true, 10 | "forceConsistentCasingInFileNames": true, 11 | "incremental": true, 12 | "inlineSources": false, 13 | "isolatedModules": true, 14 | "lib": ["ES2021", "DOM", "DOM.Iterable"], 15 | "module": "esnext", 16 | "moduleResolution": "node", 17 | "noUnusedLocals": false, 18 | "noUnusedParameters": false, 19 | "preserveWatchOutput": true, 20 | "resolveJsonModule": true, 21 | "skipLibCheck": true, 22 | "strict": true, 23 | "typeRoots": ["../../node_modules/@types"] 24 | }, 25 | "files": ["declaration.d.ts"] 26 | } 27 | -------------------------------------------------------------------------------- /apps/web/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "web", 3 | "version": "1.0.0", 4 | "private": true, 5 | "scripts": { 6 | "dev": "next dev", 7 | "build": "next build", 8 | "start": "next start", 9 | "eslint:format": "eslint src --fix" 10 | }, 11 | "dependencies": { 12 | "@natu/env": "*", 13 | "@natu/fonts": "*", 14 | "@natu/middleware-chain": "*", 15 | "@natu/next-themes": "*", 16 | "@natu/storyblok-api": "*", 17 | "@natu/storyblok-preview": "*", 18 | "@natu/storyblok-revalidate": "*", 19 | "@natu/storyblok-seo": "*", 20 | "@natu/storyblok-ui": "*", 21 | "@natu/storyblok-utils": "*", 22 | "@natu/ui": "*", 23 | "@natu/utils": "*", 24 | "next": "^14.2.4" 25 | }, 26 | "devDependencies": { 27 | "eslint-config-custom": "*", 28 | "tailwind-config": "*", 29 | "tsconfig": "*" 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /packages/storyblok-utils/src/utils/getSlugWithoutAppName/getSlugWithoutAppName.ts: -------------------------------------------------------------------------------- 1 | import { env } from '@natu/env'; 2 | 3 | /** 4 | * This TypeScript function removes the app name from a given slug string. 5 | * @param {string} slug - The `slug` parameter is a string representing a URL slug. It is likely used 6 | * in a Next.js or Storyblok project to determine the content to display on a page. 7 | * @returns The `getSlugWithoutAppName` function returns a modified version of the `slug` string with 8 | * any occurrence of the `NEXT_PUBLIC_STORYBLOK_APP_NAME` environment variable removed. The modified 9 | * string is returned as the output of the function. 10 | */ 11 | export const getSlugWithoutAppName = (slug?: string | null) => { 12 | if (!slug) { 13 | return ''; 14 | } 15 | 16 | return slug.replace(env.NEXT_PUBLIC_STORYBLOK_MAIN_APP_FOLDER, ''); 17 | }; 18 | -------------------------------------------------------------------------------- /packages/storyblok-utils/src/utils/getAssetFromStoryblok/getAssetFromStoryblok.ts: -------------------------------------------------------------------------------- 1 | import { StoryblokAsset } from '../../types'; 2 | 3 | interface Config { 4 | type: 'image' | 'video'; 5 | } 6 | 7 | export const getAssetFromStoryblok = ( 8 | asset?: StoryblokAsset, 9 | config: Config = { type: 'image' }, 10 | ) => { 11 | if (!asset || !asset?.filename) { 12 | return { 13 | src: null, 14 | alt: null, 15 | title: null, 16 | }; 17 | } 18 | 19 | if (config.type === 'image') { 20 | return { 21 | src: asset.filename, 22 | alt: asset.alt, 23 | title: asset.title, 24 | width: Number(asset.filename.split('/')[5].split('x')[0]), 25 | height: Number(asset.filename.split('/')[5].split('x')[1]), 26 | }; 27 | } 28 | 29 | return { 30 | src: asset.filename, 31 | alt: asset.alt, 32 | title: asset.title, 33 | }; 34 | }; 35 | -------------------------------------------------------------------------------- /packages/storyblok-richtext/src/components/NodeCodeblock/NodeCodeblock.tsx: -------------------------------------------------------------------------------- 1 | import { ReactNode } from 'react'; 2 | 3 | import { Code, CodeProps } from '@natu/ui'; 4 | 5 | interface NodeCodeblockOptions { 6 | class: string; 7 | } 8 | 9 | const validCodeComponentLanguage = [ 10 | { sb: 'typescript', valid: 'tsx' }, 11 | { sb: 'javascript', valid: 'jsx' }, 12 | { sb: 'reasonml', valid: 'reason' }, 13 | ]; 14 | 15 | export const NodeCodeblock = (children: ReactNode, options: NodeCodeblockOptions) => { 16 | if (!children) { 17 | return null; 18 | } 19 | 20 | const codeData = `${children}`; 21 | 22 | const sbLang = options?.class?.replace('language-', ''); 23 | const lang = 24 | validCodeComponentLanguage.find(item => item.sb === sbLang)?.valid || sbLang || 'tsx'; 25 | 26 | return ; 27 | }; 28 | -------------------------------------------------------------------------------- /packages/ui/src/Tooltip/Tooltip.stories.tsx: -------------------------------------------------------------------------------- 1 | import type { Meta, StoryObj } from '@storybook/react'; 2 | 3 | import { Button } from '../Button'; 4 | import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from './Tooltip'; 5 | 6 | const meta: Meta = { 7 | title: 'Components/Tooltip', 8 | component: Tooltip, 9 | args: { 10 | delayDuration: 300, 11 | }, 12 | }; 13 | 14 | export default meta; 15 | 16 | type Story = StoryObj; 17 | 18 | export const Default: Story = { 19 | render: args => ( 20 | 21 | 22 | 23 | 24 | 25 | 26 |

    Any data as children

    27 |
    28 |
    29 |
    30 | ), 31 | }; 32 | -------------------------------------------------------------------------------- /apps/docs/pages/_meta.json: -------------------------------------------------------------------------------- 1 | { 2 | "index": { 3 | "display": "hidden", 4 | "theme": { 5 | "breadcrumb": false 6 | } 7 | }, 8 | "getting-started": "Getting Started", 9 | "project-structure": "", 10 | "folder-structure": { 11 | "title": "Package and application architecture", 12 | "type": "separator" 13 | }, 14 | "apps": "Apps", 15 | "packages": "Packages", 16 | "naturaily": { 17 | "title": "Naturaily.com ↗", 18 | "type": "page", 19 | "href": "https://naturaily.com/", 20 | "newWindow": true 21 | }, 22 | "guides": { 23 | "title": "Guides", 24 | "type": "separator" 25 | }, 26 | "best-practice": "Best Practice", 27 | "starter-in-practice": "Starter In Practice", 28 | "shared-components": "Shared Components", 29 | "community": { 30 | "title": "Community", 31 | "type": "separator" 32 | }, 33 | "contribution-guide": "Contribution Guide" 34 | } 35 | -------------------------------------------------------------------------------- /packages/utils/src/hooks/useToggle/useToggle.ts: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import { MouseEvent, useCallback, useState } from 'react'; 4 | 5 | type UseToggleValue = boolean; 6 | export type ToggleFunc = (value?: UseToggleValue | MouseEvent) => void; 7 | 8 | /** 9 | * It returns a tuple of the current value and a function that toggles the value 10 | * @param [initialOn=false] - The initial value of the toggle. 11 | * @returns An array with two values. The first value is the current state of the toggle. The second 12 | * value is a function that can be used to toggle the state. 13 | */ 14 | export const useToggle = (initialOn = false): [UseToggleValue, ToggleFunc] => { 15 | const [on, setOn] = useState(initialOn); 16 | 17 | const toggle = useCallback( 18 | value => setOn(currentValue => (typeof value === 'boolean' ? value : !currentValue)), 19 | [], 20 | ); 21 | 22 | return [on, toggle]; 23 | }; 24 | -------------------------------------------------------------------------------- /apps/web/src/app/Providers.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import { ReactNode } from 'react'; 4 | 5 | import { ThemeProvider } from '@natu/next-themes'; 6 | import { DraftModeProvider } from '@natu/storyblok-preview'; 7 | 8 | interface DarkModeOptions { 9 | defaultTheme?: 'light' | 'dark' | 'system' | null | string; 10 | forcedTheme?: 'light' | 'dark' | null | string; 11 | } 12 | 13 | interface ProvidersProps { 14 | children?: ReactNode; 15 | draftMode?: boolean; 16 | darkModeOptions?: DarkModeOptions; 17 | } 18 | 19 | export const Providers = ({ children, draftMode, darkModeOptions }: ProvidersProps) => ( 20 | 27 | {children} 28 | 29 | ); 30 | -------------------------------------------------------------------------------- /packages/storyblok-setup/src/components/data/page.ts: -------------------------------------------------------------------------------- 1 | import { ComponentSchema } from '../components.types'; 2 | import { CONTENT_TYPE_COLOR } from '../consts.ts'; 3 | 4 | export const page: ComponentSchema = { 5 | componentGroup: 'content type', 6 | data: { 7 | name: 'page', 8 | display_name: 'Page', 9 | is_root: true, 10 | is_nestable: false, 11 | color: CONTENT_TYPE_COLOR, 12 | icon: 'block-wallet', 13 | schema: { 14 | body: { 15 | type: 'bloks', 16 | pos: 0, 17 | }, 18 | seo: { 19 | type: 'bloks', 20 | pos: 0, 21 | maximum: 1, 22 | description: 'By filling in this field you will overwrite global seo settings', 23 | restrict_components: true, 24 | component_whitelist: ['seo'], 25 | }, 26 | tab: { 27 | display_name: 'Seo', 28 | type: 'tab', 29 | pos: 2, 30 | keys: ['seo'], 31 | }, 32 | }, 33 | }, 34 | }; 35 | -------------------------------------------------------------------------------- /apps/web/src/app/not-found.tsx: -------------------------------------------------------------------------------- 1 | import { draftMode } from 'next/headers'; 2 | 3 | import { env } from '@natu/env'; 4 | import { TAGS, getStoryblokApi, relations } from '@natu/storyblok-api'; 5 | import { DynamicRender, getSlugWithAppName } from '@natu/storyblok-utils'; 6 | 7 | const NotFound = async () => { 8 | const { isEnabled } = draftMode(); 9 | const { getConfigNode } = getStoryblokApi({ draftMode: isEnabled }); 10 | 11 | const slug = getSlugWithAppName({ 12 | slug: env.NEXT_PUBLIC_STORYBLOK_EXCLUDED_FOLDERS_FROM_ROUTING, 13 | }); 14 | 15 | const configData = await getConfigNode( 16 | { 17 | slug, 18 | skipFooter: true, 19 | skipHeader: true, 20 | skipSeo: true, 21 | relations, 22 | }, 23 | { 24 | next: { 25 | tags: [TAGS.SB_CONFIG], 26 | }, 27 | }, 28 | ); 29 | 30 | return ; 31 | }; 32 | 33 | export default NotFound; 34 | -------------------------------------------------------------------------------- /packages/storyblok-api/src/graphql/query/getContentNodesQuery.graphql: -------------------------------------------------------------------------------- 1 | query getContentNodes( 2 | $relations: String = "" 3 | $perPage: Int = 12 4 | $page: Int = 1 5 | $startsWith: String = "" 6 | $excludingSlugs: String = "" 7 | $withTag: String = "" 8 | $searchTerm: String = "" 9 | $filterQuery: JsonScalar = {} 10 | $sortBy: String = "" 11 | $skipContent: Boolean = false 12 | ) { 13 | ContentNodes( 14 | page: $page 15 | resolve_relations: $relations 16 | per_page: $perPage 17 | starts_with: $startsWith 18 | excluding_slugs: $excludingSlugs 19 | search_term: $searchTerm 20 | with_tag: $withTag 21 | filter_query: $filterQuery 22 | sort_by: $sortBy 23 | ) { 24 | total 25 | items { 26 | content @skip(if: $skipContent) 27 | created_at 28 | first_published_at 29 | full_slug 30 | id 31 | name 32 | path 33 | published_at 34 | slug 35 | uuid 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /packages/eslint-config-custom/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "eslint-config-custom", 3 | "version": "0.0.0", 4 | "private": true, 5 | "main": "index.js", 6 | "license": "MIT", 7 | "files": [ 8 | "eslint-next.js" 9 | ], 10 | "devDependencies": { 11 | "@typescript-eslint/eslint-plugin": "7.3.1", 12 | "@typescript-eslint/parser": "7.2.0", 13 | "eslint": "8.57.0", 14 | "eslint-config-airbnb": "19.0.4", 15 | "eslint-config-airbnb-base": "15.0.0", 16 | "eslint-config-airbnb-typescript": "18.0.0", 17 | "eslint-config-next": "14.1.4", 18 | "eslint-config-prettier": "9.1.0", 19 | "eslint-config-turbo": "1.12.5", 20 | "eslint-mdx": "3.1.5", 21 | "eslint-plugin-import": "2.29.1", 22 | "eslint-plugin-import-helpers": "1.3.1", 23 | "eslint-plugin-react": "7.34.1", 24 | "eslint-plugin-security": "2.1.1", 25 | "tsconfig": "*" 26 | }, 27 | "publishConfig": { 28 | "access": "public" 29 | }, 30 | "dependencies": {} 31 | } 32 | -------------------------------------------------------------------------------- /apps/web/next.config.mjs: -------------------------------------------------------------------------------- 1 | import '@natu/env/src/env/env.mjs'; 2 | const { getStoryblokRedirects } = await import('./getStoryblokRedirects.mjs'); 3 | 4 | /** @type {import("next").NextConfig} */ 5 | const config = { 6 | images: { 7 | formats: ['image/avif', 'image/webp'], 8 | remotePatterns: [ 9 | { 10 | protocol: 'https', 11 | hostname: 'a.storyblok.com', 12 | }, 13 | ], 14 | }, 15 | reactStrictMode: true, 16 | transpilePackages: ['@natu/env', '@natu/ui'], 17 | async redirects() { 18 | const storyblokRedirectsItems = await getStoryblokRedirects(); 19 | 20 | return [...storyblokRedirectsItems]; 21 | }, 22 | async rewrites() { 23 | return [ 24 | { 25 | source: `/${process.env.NEXT_PUBLIC_STORYBLOK_MAIN_APP_FOLDER}/:path*`, 26 | destination: '/:path*', // The :path parameter is used here so will not be automatically passed in the query 27 | }, 28 | ]; 29 | }, 30 | }; 31 | 32 | export default config; 33 | -------------------------------------------------------------------------------- /apps/docs/pages/getting-started/requirements.mdx: -------------------------------------------------------------------------------- 1 | # 👌 Requirements to get started 2 | 3 | ```bash 4 | node - v18.18.2 5 | yarn - v1.22.17 6 | ``` 7 | 8 | ### ✅ Node 9 | 10 | To install Node.js, you can follow the nvm documentation available at [https://github.com/nvm-sh/nvm](https://github.com/nvm-sh/nvm). 11 | 12 | ```bash copy 13 | nvm use 18.18.2 14 | ``` 15 | 16 | Once installed, you can check the version using the following command: 17 | 18 | ```bash copy 19 | node --version 20 | ``` 21 | 22 | Ensure that the command returns `v18.18.2` to confirm that you have the correct version installed. 23 | 24 | ### ✅ Yarn 25 | 26 | The Turbo Repo utilizes Yarn package manager. 27 | 28 | To install Yarn, you can follow the official documentation available at [https://yarnpkg.com](https://yarnpkg.com). Once installed, you can check the version using the following command: 29 | 30 | ```bash copy 31 | yarn --version 32 | ``` 33 | 34 | Ensure that the command returns `1.22.17` to confirm that you have the correct version installed. 35 | -------------------------------------------------------------------------------- /packages/storyblok-utils/src/components/DynamicRender/DynamicRender.tsx: -------------------------------------------------------------------------------- 1 | // @ts-ignore 2 | import { StoryblokComponent } from '@storyblok/react/rsc'; 3 | 4 | import { BlokItem } from '../../types'; 5 | 6 | export interface StoryblokComponentsProps { 7 | data?: BlokItem | BlokItem[]; 8 | asListItem?: boolean; 9 | parentProps?: Record; 10 | } 11 | 12 | export const DynamicRender = ({ 13 | data, 14 | asListItem = false, 15 | parentProps, 16 | }: StoryblokComponentsProps) => { 17 | if (!data) { 18 | return null; 19 | } 20 | 21 | if (Array.isArray(data)) { 22 | if (asListItem) { 23 | return data.map(blok => ( 24 |
  • 25 | 26 |
  • 27 | )); 28 | } 29 | 30 | return data.map(blok => ); 31 | } 32 | 33 | return ; 34 | }; 35 | -------------------------------------------------------------------------------- /packages/storyblok-api/src/graphql/query/getConfigItem.graphql: -------------------------------------------------------------------------------- 1 | query getConfigNode( 2 | $slug: ID! 3 | $relations: String = "" 4 | $skipHeader: Boolean = false 5 | $skipFooter: Boolean = false 6 | $skipNotFoundPage: Boolean = false 7 | $skipSeo: Boolean = false 8 | ) { 9 | ConfigItem(id: $slug, resolve_relations: $relations) { 10 | content { 11 | _editable 12 | _uid 13 | header @skip(if: $skipHeader) { 14 | content 15 | } 16 | footer @skip(if: $skipFooter) { 17 | content 18 | } 19 | notFoundPage @skip(if: $skipNotFoundPage) { 20 | content 21 | } 22 | defaultSeo @skip(if: $skipSeo) 23 | twitterCreator @skip(if: $skipSeo) 24 | googleVerificationId @skip(if: $skipSeo) 25 | siteName @skip(if: $skipSeo) 26 | defaultTheme 27 | forcedTheme 28 | } 29 | created_at 30 | first_published_at 31 | full_slug 32 | id 33 | name 34 | path 35 | published_at 36 | release_id 37 | uuid 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /apps/web/tailwind.config.js: -------------------------------------------------------------------------------- 1 | const defaultConfig = require('tailwind-config/tailwind.config'); 2 | 3 | /** @type {import("tailwindcss").Config} */ 4 | const config = { 5 | presets: [defaultConfig], 6 | content: [ 7 | './src/app/**/*.{js,ts,jsx,tsx}', 8 | // ui 9 | '../../packages/ui/src/**/*.{js,ts,jsx,tsx}', 10 | // storyblok-preview 11 | '../../packages/storyblok-preview/src/components/**/*.{js,ts,jsx,tsx}', 12 | // storyblok-utils - resolve storyblok styles func 13 | '../../packages/storyblok-utils/src/utils/resolveStoryblokStyles/styles/**/*.{js,ts,jsx,tsx}', 14 | // storyblok-richtext 15 | '../../packages/storyblok-richtext/src/components/**/*.{js,ts,jsx,tsx}', 16 | // storyblok-ui 17 | '../../packages/storyblok-ui/src/components/**/*.{js,ts,jsx,tsx}', 18 | // next-theme 19 | '../../packages/next-themes/src/**/*.{js,ts,jsx,tsx}', 20 | // theme-customizer 21 | '../../packages/theme-customizer/src/components/**/*.{js,ts,jsx,tsx}', 22 | ], 23 | }; 24 | 25 | module.exports = config; 26 | -------------------------------------------------------------------------------- /packages/storyblok-utils/src/utils/isSlugExcludedFromRouting/isSlugExcludedFromRouting.ts: -------------------------------------------------------------------------------- 1 | import { env } from '@natu/env'; 2 | 3 | const excludingSlugsFromRouting = 4 | env.NEXT_PUBLIC_STORYBLOK_EXCLUDED_FOLDERS_FROM_ROUTING?.split(','); 5 | 6 | /** 7 | * The function checks if a given slug is excluded from routing based on a list of excluded slugs. 8 | * @param {string} slug - The `slug` parameter is a string that represents a URL slug. A slug is a 9 | * user-friendly URL component that typically represents a page or resource on a website. It is often 10 | * used in routing to determine which page or resource to display based on the URL. 11 | * @returns a boolean value. It returns true if the given slug is included in the array of 12 | * excludingSlugsFromRouting, and false otherwise. 13 | */ 14 | export const isSlugExcludedFromRouting = (slug: string) => { 15 | if (!excludingSlugsFromRouting) { 16 | return false; 17 | } 18 | 19 | return excludingSlugsFromRouting.some((excludedSlug: string) => slug.includes(excludedSlug)); 20 | }; 21 | -------------------------------------------------------------------------------- /turbo.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://turbo.build/schema.json", 3 | "globalDependencies": ["**/.env.*local"], 4 | "pipeline": { 5 | "build": { 6 | "dependsOn": ["^build"], 7 | "outputs": [".next/**", "!.next/cache/**"], 8 | "env": [ 9 | "NODE_ENV", 10 | "SKIP_ENV_VALIDATION", 11 | "NEXT_PUBLIC_APP_URL", 12 | "NEXT_PUBLIC_STORYBLOK_TOKEN_VERSION", 13 | "NEXT_PUBLIC_STORYBLOK_PREVIEW_TOKEN", 14 | "NEXT_PUBLIC_STORYBLOK_MAIN_APP_FOLDER", 15 | "NEXT_PUBLIC_STORYBLOK_EXCLUDED_FOLDERS_FROM_ROUTING", 16 | "NEXT_PUBLIC_STORYBLOK_API_URL", 17 | "STORYBLOK_MANAGEMENT_API_URL", 18 | "STORYBLOK_PERSONAL_ACCESS_TOKEN", 19 | "STORYBLOK_SPACE_ID", 20 | "STORYBLOK_REGION" 21 | ] 22 | }, 23 | "eslint:format": { 24 | "cache": false, 25 | "persistent": true 26 | }, 27 | "dev": { 28 | "cache": false, 29 | "persistent": true 30 | }, 31 | "codegen": { 32 | "cache": false 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /packages/ui/src/Typography/Typography.tsx: -------------------------------------------------------------------------------- 1 | import { Slot } from '@radix-ui/react-slot'; 2 | import { ComponentProps, ElementType, ReactNode, forwardRef } from 'react'; 3 | 4 | import { cn } from '@natu/utils'; 5 | 6 | import { TypographyVariantProp } from './Typography.type'; 7 | import { getTypographyVariantStyles } from './utils/getTypographyVariantStyles'; 8 | 9 | export interface TypographyProps extends ComponentProps<'p'> { 10 | children?: ReactNode; 11 | variant?: TypographyVariantProp; 12 | component?: ElementType; 13 | asChild?: boolean; 14 | } 15 | 16 | export const Typography = forwardRef( 17 | ({ component, children, className, variant, asChild = false, ...rest }, ref) => { 18 | const typographyVariant = getTypographyVariantStyles(variant); 19 | 20 | const Component = asChild ? Slot : component || 'p'; 21 | const styles = cn(typographyVariant, className); 22 | 23 | return ( 24 | 25 | {children} 26 | 27 | ); 28 | }, 29 | ); 30 | -------------------------------------------------------------------------------- /packages/storyblok-utils/src/utils/resolveStoryblokStyles/styles/grid/grid.ts: -------------------------------------------------------------------------------- 1 | export type Grid = '1-col' | '2-col' | '3-col' | '4-col' | '5-col' | '1/2' | '2/1'; 2 | 3 | export const gridMobileVariants: Record = { 4 | '1-col': 'grid-cols-1', 5 | '2-col': 'grid-cols-2', 6 | '3-col': 'grid-cols-3', 7 | '4-col': 'grid-cols-4', 8 | '5-col': 'grid-cols-5', 9 | '1/2': 'grid-cols-[1fr_2fr]', 10 | '2/1': 'grid-cols-[2fr_1fr]', 11 | }; 12 | 13 | export const gridTabletVariants: Record = { 14 | '1-col': 'md:grid-cols-1', 15 | '2-col': 'md:grid-cols-2', 16 | '3-col': 'md:grid-cols-3', 17 | '4-col': 'md:grid-cols-4', 18 | '5-col': 'md:grid-cols-5', 19 | '1/2': 'md:grid-cols-[1fr_2fr]', 20 | '2/1': 'md:grid-cols-[2fr_1fr]', 21 | }; 22 | 23 | export const gridDesktopVariants: Record = { 24 | '1-col': 'lg:grid-cols-1', 25 | '2-col': 'lg:grid-cols-2', 26 | '3-col': 'lg:grid-cols-3', 27 | '4-col': 'lg:grid-cols-4', 28 | '5-col': 'lg:grid-cols-5', 29 | '1/2': 'lg:grid-cols-[1fr_2fr]', 30 | '2/1': 'lg:grid-cols-[2fr_1fr]', 31 | }; 32 | -------------------------------------------------------------------------------- /packages/storyblok-richtext/src/components/StoryblokRichtext/getStoryblokRichText.tsx: -------------------------------------------------------------------------------- 1 | import { ISbRichtext } from '@storyblok/react'; 2 | import { 3 | render, 4 | MARK_LINK, 5 | NODE_IMAGE, 6 | NODE_HEADING, 7 | MARK_CODE, 8 | NODE_CODEBLOCK, 9 | NODE_LI, 10 | } from 'storyblok-rich-text-react-renderer'; 11 | 12 | import { DefaultBlokResolver } from '../DefaultBlokResolver'; 13 | import { MarkCode } from '../MarkCode'; 14 | import { MarkLink } from '../MarkLink'; 15 | import { NodeCodeblock } from '../NodeCodeblock'; 16 | import { NodeHeading } from '../NodeHeading'; 17 | import { NodeImage } from '../NodeImage'; 18 | import { NodeLi } from '../NodeLi'; 19 | 20 | export const getStoryblokRichText = (data: ISbRichtext) => 21 | render(data, { 22 | markResolvers: { 23 | [MARK_LINK]: MarkLink, 24 | [MARK_CODE]: MarkCode, 25 | }, 26 | nodeResolvers: { 27 | [NODE_HEADING]: NodeHeading, 28 | [NODE_IMAGE]: NodeImage, 29 | [NODE_CODEBLOCK]: NodeCodeblock, 30 | [NODE_LI]: NodeLi, 31 | }, 32 | defaultBlokResolver: DefaultBlokResolver, 33 | }); 34 | -------------------------------------------------------------------------------- /packages/storyblok-setup/src/stories/utils/createHomepage.ts: -------------------------------------------------------------------------------- 1 | import { storyblok } from '../../utils/client.ts'; 2 | import { color } from '../../utils/color.ts'; 3 | import { STORIES_ENDPOINT } from '../../utils/endpoints.ts'; 4 | import { homepageContent } from '../data/homepageContent.ts'; 5 | 6 | interface CreateHomepageInput { 7 | parentFolderID?: number; 8 | } 9 | 10 | export const createHomepage = async ({ parentFolderID }: CreateHomepageInput) => { 11 | let homepageUUID; 12 | 13 | try { 14 | const res = await storyblok.post(STORIES_ENDPOINT, { 15 | story: { 16 | name: 'Homepage', 17 | slug: '/', 18 | is_startpage: true, 19 | is_folder: false, 20 | parent_id: `${parentFolderID}`, 21 | content: { 22 | ...homepageContent, 23 | }, 24 | }, 25 | publish: 1, 26 | }); 27 | // @ts-ignore 28 | homepageUUID = res.data.story.uuid; 29 | } catch (err) { 30 | console.error(color('danger', `🚨 CreateHomepage - ${JSON.stringify(err)}`)); 31 | } 32 | 33 | return { 34 | homepageUUID, 35 | }; 36 | }; 37 | -------------------------------------------------------------------------------- /packages/storyblok-setup/src/components/data/seo.ts: -------------------------------------------------------------------------------- 1 | import { ComponentSchema } from '../components.types.ts'; 2 | import { SEO_COLOR } from '../consts.ts'; 3 | 4 | export const seo: ComponentSchema = { 5 | componentGroup: 'seo', 6 | data: { 7 | name: 'seo', 8 | display_name: 'Seo', 9 | is_nestable: true, 10 | is_root: false, 11 | color: SEO_COLOR, 12 | icon: 'block-buildin', 13 | schema: { 14 | metaTitle: { 15 | type: 'text', 16 | pos: 0, 17 | required: false, 18 | }, 19 | metaDescription: { 20 | type: 'textarea', 21 | pos: 1, 22 | required: false, 23 | }, 24 | metaImage: { 25 | type: 'asset', 26 | pos: 2, 27 | required: false, 28 | filetypes: ['images'], 29 | }, 30 | noIndex: { 31 | type: 'boolean', 32 | pos: 3, 33 | required: false, 34 | default_value: false, 35 | }, 36 | noFollow: { 37 | type: 'boolean', 38 | pos: 4, 39 | required: false, 40 | default_value: false, 41 | }, 42 | }, 43 | }, 44 | }; 45 | -------------------------------------------------------------------------------- /packages/storyblok-utils/src/utils/sbEditable/sbEditable.ts: -------------------------------------------------------------------------------- 1 | // @ts-ignore 2 | import { storyblokEditable, SbBlokData } from '@storyblok/react/rsc'; 3 | 4 | import { SbComponentType } from '../../types'; 5 | 6 | /** 7 | * This TypeScript function checks if a given object has an "_ed itable" property and returns a 8 | * corresponding object if it does. 9 | * @param [blok] - The `blok` parameter is an optional argument of type `SbComponentType`. It 10 | * is used to check if the `_editable` property exists in the `blok` object. If it does, the function 11 | * returns the result of calling `storyblokEditable` function with the 12 | * @returns The `sbEditable` function returns an object that contains the editable properties of a 13 | * Storyblok component. If the `blok` parameter is not provided or does not have an `_editable` 14 | * property, an empty object is returned. 15 | */ 16 | export const sbEditable = (blok?: SbComponentType) => { 17 | // eslint-disable-next-line no-underscore-dangle 18 | if (!blok || !blok?._editable) { 19 | return {}; 20 | } 21 | 22 | return storyblokEditable(blok as SbBlokData); 23 | }; 24 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Naturaily 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /packages/storyblok-setup/src/setup.ts: -------------------------------------------------------------------------------- 1 | import { setComponentGroups } from './componentGroup/setComponentGroups.ts'; 2 | import { setComponents } from './components/setComponents.ts'; 3 | import { setDatasources } from './datasource/setDatasource.ts'; 4 | import { createAppEnv } from './env/createAppEnv.ts'; 5 | import { setStories } from './stories/setStories.ts'; 6 | import { checkEnv } from './utils/checkEnv.ts'; 7 | import { color } from './utils/color.ts'; 8 | 9 | const setup = async () => { 10 | console.log(color('info', '⏱️ Starting setup... (~1m)')); 11 | 12 | if (!checkEnv()) { 13 | return; 14 | } 15 | 16 | await setDatasources(); 17 | const componentGroups = await setComponentGroups(); 18 | await setComponents({ componentGroups }); 19 | const { 20 | NEXT_PUBLIC_STORYBLOK_EXCLUDED_FOLDERS_FROM_ROUTING, 21 | NEXT_PUBLIC_STORYBLOK_MAIN_APP_FOLDER, 22 | } = await setStories(); 23 | 24 | await createAppEnv({ 25 | NEXT_PUBLIC_STORYBLOK_EXCLUDED_FOLDERS_FROM_ROUTING, 26 | NEXT_PUBLIC_STORYBLOK_MAIN_APP_FOLDER, 27 | }); 28 | 29 | console.log(color('success', '🔥 Setup complete!')); 30 | }; 31 | 32 | setup(); 33 | -------------------------------------------------------------------------------- /packages/storyblok-setup/src/components/components.ts: -------------------------------------------------------------------------------- 1 | import { ComponentSchema } from './components.types.ts'; 2 | import { column } from './data/column.ts'; 3 | import { config } from './data/config.ts'; 4 | import { container } from './data/container.ts'; 5 | import { cta } from './data/cta.ts'; 6 | import { footer } from './data/footer.ts'; 7 | import { grid } from './data/grid.ts'; 8 | import { header } from './data/header.ts'; 9 | import { image } from './data/image.ts'; 10 | import { page } from './data/page.ts'; 11 | import { redirect } from './data/redirect.ts'; 12 | import { richtext } from './data/richtext.ts'; 13 | import { row } from './data/row.ts'; 14 | import { seo } from './data/seo.ts'; 15 | import { table } from './data/table.ts'; 16 | import { themeModeSwitcher } from './data/themeModeSwitcher.ts'; 17 | import { typography } from './data/typography.ts'; 18 | 19 | export const components: ComponentSchema[] = [ 20 | container, 21 | grid, 22 | row, 23 | column, 24 | typography, 25 | themeModeSwitcher, 26 | table, 27 | richtext, 28 | image, 29 | cta, 30 | footer, 31 | header, 32 | seo, 33 | redirect, 34 | page, 35 | config, 36 | ]; 37 | -------------------------------------------------------------------------------- /packages/storyblok-setup/src/stories/utils/createConfig.ts: -------------------------------------------------------------------------------- 1 | import { storyblok } from '../../utils/client.ts'; 2 | import { STORIES_ENDPOINT } from '../../utils/endpoints.ts'; 3 | import { configContent } from '../data/configContent.ts'; 4 | import { LayoutStoryData } from './createLayoutsStories.ts'; 5 | 6 | interface CreateConfigInput { 7 | parentFolderID?: number; 8 | layoutsUUID?: LayoutStoryData[]; 9 | specialPagesUUID?: LayoutStoryData[]; 10 | } 11 | 12 | export const createConfig = async ({ 13 | parentFolderID, 14 | layoutsUUID, 15 | specialPagesUUID, 16 | }: CreateConfigInput) => { 17 | const footerUUID = layoutsUUID?.find(layout => layout.slug === 'footer')?.uuid; 18 | const headerUUID = layoutsUUID?.find(layout => layout.slug === 'header')?.uuid; 19 | const notFoundUUID = specialPagesUUID?.find(item => item.slug === 'not-found-404')?.uuid; 20 | 21 | try { 22 | await storyblok.post(STORIES_ENDPOINT, { 23 | story: { 24 | ...configContent({ footerUUID, headerUUID, notFoundUUID }), 25 | parent_id: `${parentFolderID}`, 26 | }, 27 | publish: 1, 28 | }); 29 | } catch (err) { 30 | console.log(err); 31 | } 32 | }; 33 | -------------------------------------------------------------------------------- /packages/storyblok-utils/src/utils/resolveStoryblokStyles/styles/fonts/fonts.ts: -------------------------------------------------------------------------------- 1 | import { cn } from '@natu/utils'; 2 | 3 | export const fontFamilyVariants = { 4 | primary: cn('font-primary'), 5 | secondary: cn('font-secondary'), 6 | }; 7 | 8 | export type FontFamily = keyof typeof fontFamilyVariants; 9 | 10 | export const fontWeightVariants = { 11 | normal: 'font-normal', // 400 12 | bold: 'font-bold', // 700 13 | }; 14 | 15 | export type FontWeight = keyof typeof fontWeightVariants; 16 | 17 | export type TextAlign = 'left' | 'center' | 'right' | 'justify'; 18 | 19 | export const textAlignVariants: Record = { 20 | left: 'text-left', 21 | center: 'text-center', 22 | right: 'text-right', 23 | justify: 'text-justify', 24 | }; 25 | 26 | export const textAlignTabletVariants: Record = { 27 | left: 'md:text-left', 28 | center: 'md:text-center', 29 | right: 'md:text-right', 30 | justify: 'md:text-justify', 31 | }; 32 | 33 | export const textAlignDesktopVariants: Record = { 34 | left: 'lg:text-left', 35 | center: 'lg:text-center', 36 | right: 'lg:text-right', 37 | justify: 'lg:text-justify', 38 | }; 39 | -------------------------------------------------------------------------------- /apps/docs/pages/packages/middleware-chain.mdx: -------------------------------------------------------------------------------- 1 | # @natu/middleware-chain 2 | 3 | ## 💻 Middlewares chaining 4 | 5 | [Middlewares](https://nextjs.org/docs/app/building-your-application/routing/middleware) are functions that 6 | allow you to intersept the request and perform some action before sending the response to the user. 7 | This package is chaining them one after another if using more of them. 8 | 9 | It was described in details in the video below: 10 | 11 | import Youtube from '../../components/youtube'; 12 | 13 | 14 | 15 | ## 🤓 Usage 16 | 17 | To use this package in the app import this package as a dependency in its individual `package.json` file: 18 | 19 | ```json 20 | "dependencies": { 21 | "@natu/middleware-chain": "*", 22 | } 23 | ``` 24 | 25 | Then use it like in the example below: 26 | 27 | ```ts copy 28 | import { middlewareChain } from '@natu/middleware-chain'; 29 | import { withStoryblokPreviewMiddleware } from '@natu/storyblok-preview'; 30 | import { withAuthMiddleware } from '@natu/auth'; 31 | 32 | const middlewares = [withStoryblokPreviewMiddleware, withAuthMiddleware]; 33 | 34 | export default middlewareChain(middlewares); 35 | ``` 36 | -------------------------------------------------------------------------------- /packages/storyblok-setup/src/stories/data/configContent.ts: -------------------------------------------------------------------------------- 1 | interface ConfigContentInput { 2 | footerUUID?: string; 3 | headerUUID?: string; 4 | notFoundUUID?: string; 5 | } 6 | 7 | export const configContent = ({ footerUUID, headerUUID, notFoundUUID }: ConfigContentInput) => ({ 8 | name: 'Config', 9 | slug: '/', 10 | is_startpage: true, 11 | is_folder: false, 12 | content: { 13 | _uid: 'e65982e5-ba09-4c88-87ed-45c47b8962c4', 14 | footer: footerUUID, 15 | header: headerUUID, 16 | notFoundPage: notFoundUUID, 17 | siteName: 'Naturaily', 18 | component: 'config', 19 | defaultSeo: [ 20 | { 21 | _uid: 'fa094dc4-dc93-4b7d-afbc-454941497d1d', 22 | noIndex: false, 23 | noFollow: false, 24 | component: 'seo', 25 | metaTitle: 'Naturaily: Jamstack & headless e-commerce agency', 26 | metaDescription: 27 | "We offer Jamstack websites, headless e-commerce, and custom web development for the utmost flexibility and performance. Let's talk!", 28 | }, 29 | ], 30 | forcedTheme: '', 31 | defaultTheme: 'system', 32 | twitterCreator: '@naturaily', 33 | googleVerificationId: '', 34 | }, 35 | }); 36 | -------------------------------------------------------------------------------- /packages/storybook/src/preview.ts: -------------------------------------------------------------------------------- 1 | import { MINIMAL_VIEWPORTS } from '@storybook/addon-viewport'; 2 | import type { Preview } from '@storybook/react'; 3 | 4 | // Custom viewports definition. Extends the MINIMAL_VIEWPORTS 5 | const customViewports = { 6 | iphone7: { 7 | name: 'iPhone 7', 8 | styles: { 9 | width: '375px', 10 | height: '667px', 11 | }, 12 | }, 13 | samsungGalaxyS21Ultra: { 14 | name: 'Samsung Galaxy S21 Ultra', 15 | styles: { 16 | width: '384px', 17 | height: '854px', 18 | }, 19 | }, 20 | }; 21 | 22 | export const preview: Preview = { 23 | parameters: { 24 | backgrounds: { 25 | default: 'light', 26 | values: [ 27 | { name: 'light', value: '#ffffff' }, 28 | { name: 'gray', value: '#808080' }, 29 | { name: 'dark', value: '#212b31' }, 30 | ], 31 | }, 32 | actions: { argTypesRegex: '^on[A-Z].*' }, 33 | controls: { 34 | matchers: { 35 | color: /(background|color)$/i, 36 | date: /Date$/, 37 | }, 38 | }, 39 | viewport: { 40 | viewports: { 41 | ...MINIMAL_VIEWPORTS, 42 | ...customViewports, 43 | }, 44 | }, 45 | }, 46 | }; 47 | -------------------------------------------------------------------------------- /packages/storyblok-api/codegen.ts: -------------------------------------------------------------------------------- 1 | import { CodegenConfig } from '@graphql-codegen/cli'; 2 | 3 | const config: CodegenConfig = { 4 | schema: [ 5 | { 6 | [process.env.NEXT_PUBLIC_STORYBLOK_API_URL!]: { 7 | headers: { 8 | Token: process.env.NEXT_PUBLIC_STORYBLOK_PREVIEW_TOKEN!, 9 | Version: 'draft', 10 | }, 11 | }, 12 | }, 13 | ], 14 | overwrite: true, 15 | config: { 16 | namingConvention: { 17 | typeNames: 'change-case-all#pascalCase', 18 | }, 19 | }, 20 | documents: ['./src/**/*.graphql'], 21 | generates: { 22 | 'src/generated/graphql.ts': { 23 | plugins: [ 24 | { 25 | add: { 26 | content: '/* eslint-disable @typescript-eslint/no-unused-vars */', 27 | }, 28 | }, 29 | { 30 | add: { 31 | content: '/* eslint-disable @typescript-eslint/no-explicit-any */', 32 | }, 33 | }, 34 | 'typescript', 35 | 'typescript-operations', 36 | 'typescript-document-nodes', 37 | ], 38 | hooks: { 39 | afterOneFileWrite: ['prettier --write', 'eslint --fix'], 40 | }, 41 | }, 42 | }, 43 | }; 44 | 45 | export default config; 46 | -------------------------------------------------------------------------------- /packages/storyblok-richtext/src/components/NodeHeading/NodeHeading.tsx: -------------------------------------------------------------------------------- 1 | import { ReactNode, ElementType } from 'react'; 2 | 3 | import { Typography, TypographyVariantProp } from '@natu/ui'; 4 | 5 | interface NodeHeadingProps { 6 | level: 1 | 2 | 3 | 4 | 5 | 6; 7 | } 8 | 9 | interface Heading { 10 | tag: ElementType; 11 | variant: TypographyVariantProp; 12 | } 13 | 14 | const headings: Heading[] = [ 15 | { 16 | tag: 'h1', 17 | variant: ['text-4xl', 'text-5xl', 'text-6xl'], 18 | }, 19 | { 20 | tag: 'h2', 21 | variant: ['text-3xl', 'text-4xl', 'text-5xl'], 22 | }, 23 | { 24 | tag: 'h3', 25 | variant: ['text-2xl', 'text-3xl', 'text-4xl'], 26 | }, 27 | { 28 | tag: 'h4', 29 | variant: ['text-xl', 'text-2xl', 'text-3xl'], 30 | }, 31 | { 32 | tag: 'h5', 33 | variant: ['text-lg', 'text-xl', 'text-2xl'], 34 | }, 35 | { 36 | tag: 'h6', 37 | variant: ['text-base', 'text-lg', 'text-xl'], 38 | }, 39 | ]; 40 | 41 | export const NodeHeading = (children: ReactNode, { level }: NodeHeadingProps) => { 42 | const { tag, variant } = headings[level - 1]; 43 | 44 | return ( 45 | 46 | {children} 47 | 48 | ); 49 | }; 50 | -------------------------------------------------------------------------------- /packages/ui/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@natu/ui", 3 | "version": "0.0.0", 4 | "private": true, 5 | "main": "./index.ts", 6 | "types": "./index.ts", 7 | "license": "MIT", 8 | "files": [ 9 | "index.ts" 10 | ], 11 | "scripts": { 12 | "eslint:format": "eslint src --fix" 13 | }, 14 | "devDependencies": { 15 | "@babel/core": "7.24.3", 16 | "@types/node": "20.11.30", 17 | "@types/react": "18.2.72", 18 | "@types/react-dom": "^18.2.22", 19 | "eslint-config-custom": "*", 20 | "tailwind-config": "*", 21 | "tsconfig": "*" 22 | }, 23 | "dependencies": { 24 | "@natu/next-link": "*", 25 | "@natu/utils": "*", 26 | "@next/third-parties": "14.1.4", 27 | "@radix-ui/react-aspect-ratio": "1.0.3", 28 | "@radix-ui/react-dropdown-menu": "2.0.6", 29 | "@radix-ui/react-label": "2.0.2", 30 | "@radix-ui/react-popover": "1.0.7", 31 | "@radix-ui/react-scroll-area": "1.0.5", 32 | "@radix-ui/react-slot": "1.0.2", 33 | "@radix-ui/react-tooltip": "1.0.7", 34 | "class-variance-authority": "0.7.0", 35 | "framer-motion": "11.2.12", 36 | "lucide-react": "0.359.0", 37 | "prism-react-renderer": "2.3.1", 38 | "react": "18.2.0", 39 | "react-dom": "18.2.0" 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /apps/docs/pages/packages/storyblok/storyblok-revalidate.mdx: -------------------------------------------------------------------------------- 1 | import { Steps, Callout } from 'nextra/components'; 2 | 3 | # @natu/storyblok-revalidate 4 | 5 | Allows pages update without the need to rebuild the entire project. 6 | 7 | 8 | Used in the [web app](/apps/web). 9 | 10 | 11 | ## Usage 12 | 13 | ### Import package in `package.json` 14 | 15 | ```json 16 | "dependencies": { 17 | "@natu/storyblok-revalidate": "*", 18 | } 19 | ``` 20 | 21 | ### Use `revalidateHandler` in api route 22 | 23 | ```tsx filename="apps/web/src/app/api/revalidate/route.ts" 24 | import { revalidateHandler } from '@natu/storyblok-revalidate'; 25 | 26 | export { revalidateHandler as POST }; 27 | ``` 28 | 29 | ### Setup revalidate webhook in Storyblok CMS 30 | 31 | As the endpoint URL, pass: 32 | 33 | `https:///api/revalidate?token=NEXT_PUBLIC_STORYBLOK_PREVIEW_TOKEN` 34 | 35 | You will find the token under the variable `NEXT_PUBLIC_STORYBLOK_PREVIEW_TOKEN` in the file `apps/web/.env` 36 | 37 | ![Vercel success](https://a.storyblok.com/f/218794/4074x2210/0826083bdc/screenshot-2024-02-07-at-1-21-22-pm.png) 38 | 39 | 40 | Remember to check all the checkboxes in the `Story` accordion 41 | 42 | -------------------------------------------------------------------------------- /packages/storyblok-ui/src/components/componentsMap.ts: -------------------------------------------------------------------------------- 1 | import { SBFooter } from './contentTypes/SBFooter'; 2 | import { SBHeader } from './contentTypes/SBHeader'; 3 | import { SBPage } from './contentTypes/SBPage'; 4 | import { SBColumn } from './elements/SBColumn'; 5 | import { SBContainer } from './elements/SBContainer'; 6 | import { SBCta } from './elements/SBCta'; 7 | import { SBGrid } from './elements/SBGrid'; 8 | import { SBImage } from './elements/SBImage'; 9 | import { SBRichtext } from './elements/SBRichtext'; 10 | import { SBRow } from './elements/SBRow'; 11 | import { SBTable } from './elements/SBTable'; 12 | import { SBThemeModeSwitcher } from './elements/SBThemeModeSwitcher/SBThemeModeSwitcher'; 13 | import { SBTypography } from './elements/SBTypography'; 14 | 15 | const elements = { 16 | cta: SBCta, 17 | image: SBImage, 18 | richtext: SBRichtext, 19 | table: SBTable, 20 | container: SBContainer, 21 | themeModeSwitcher: SBThemeModeSwitcher, 22 | typography: SBTypography, 23 | grid: SBGrid, 24 | column: SBColumn, 25 | row: SBRow, 26 | }; 27 | 28 | const contentTypes = { 29 | page: SBPage, 30 | footer: SBFooter, 31 | header: SBHeader, 32 | }; 33 | 34 | export const componentsMap: Record = { 35 | ...elements, 36 | ...contentTypes, 37 | }; 38 | -------------------------------------------------------------------------------- /packages/next-themes/src/ThemeModeSwitcher/ThemeModeSwitcher.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import { Moon, Sun } from 'lucide-react'; 4 | import { useTheme } from 'next-themes'; 5 | 6 | import { 7 | Button, 8 | DropdownMenu, 9 | DropdownMenuContent, 10 | DropdownMenuItem, 11 | DropdownMenuTrigger, 12 | } from '@natu/ui'; 13 | 14 | export const ThemeModeSwitcher = ({ ...rest }) => { 15 | const { setTheme } = useTheme(); 16 | 17 | return ( 18 | 19 | 20 | 25 | 26 | 27 | setTheme('light')}>Light 28 | setTheme('dark')}>Dark 29 | setTheme('system')}>System 30 | 31 | 32 | ); 33 | }; 34 | -------------------------------------------------------------------------------- /packages/storyblok-setup/src/components/setComponents.ts: -------------------------------------------------------------------------------- 1 | import { ComponentGroup } from '../componentGroup/setComponentGroups.ts'; 2 | import { storyblok } from '../utils/client.ts'; 3 | import { color } from '../utils/color.ts'; 4 | import { COMPONENTS_ENDPOINT } from '../utils/endpoints.ts'; 5 | import { components } from './components.ts'; 6 | 7 | interface SetComponentsInput { 8 | componentGroups?: ComponentGroup[]; 9 | } 10 | 11 | export const setComponents = async ({ componentGroups }: SetComponentsInput) => { 12 | console.log(color('info', '▶️ Starting workshop on components setup...')); 13 | 14 | const requests = []; 15 | 16 | for (const component of components) { 17 | const componentGroup = componentGroups?.find(group => group.name === component.componentGroup); 18 | 19 | requests.push( 20 | storyblok.post(COMPONENTS_ENDPOINT, { 21 | // @ts-ignore 22 | component: { 23 | ...component.data, 24 | component_group_uuid: componentGroup?.uuid, 25 | }, 26 | }), 27 | ); 28 | } 29 | 30 | try { 31 | await Promise.all(requests); 32 | } catch (err) { 33 | console.error(color('danger', `🚨 Component -> ContentType - ${JSON.stringify(err)}`)); 34 | } 35 | 36 | console.log(color('success', '✅ Components setup complete.')); 37 | }; 38 | -------------------------------------------------------------------------------- /packages/ui/src/Tooltip/Tooltip.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import * as TooltipPrimitive from '@radix-ui/react-tooltip'; 4 | import { ComponentPropsWithoutRef, ElementRef, forwardRef } from 'react'; 5 | 6 | import { cn } from '@natu/utils'; 7 | 8 | const TooltipProvider = TooltipPrimitive.Provider; 9 | 10 | const Tooltip = TooltipPrimitive.Root; 11 | 12 | const TooltipTrigger = TooltipPrimitive.Trigger; 13 | 14 | const TooltipContent = forwardRef< 15 | ElementRef, 16 | ComponentPropsWithoutRef 17 | >(({ className, sideOffset = 4, ...props }, ref) => ( 18 | 27 | )); 28 | TooltipContent.displayName = TooltipPrimitive.Content.displayName; 29 | 30 | export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider }; 31 | -------------------------------------------------------------------------------- /apps/docs/pages/apps/storybook-ui.mdx: -------------------------------------------------------------------------------- 1 | import { Callout } from 'nextra/components'; 2 | 3 | # Storybook UI 4 | 5 | This application is a `Storybook`, a tool primarily used for developing UI components in isolation. It facilitates the creation and showcasing of various UI components by generating `stories` based on the [@natu/ui](/packages/ui) library. These stories serve as interactive documentation, allowing developers to visualize and interact with individual components independently of the larger application context. By leveraging Storybook along with the @natu/ui library, developers can efficiently iterate on and test UI components, ensuring consistency and quality across their projects. Storybook's ability to automatically generate stories from the `@natu/ui` library streamlines the development process, enabling developers to focus on building robust and visually appealing user interfaces. 6 | 7 | ## Available Scripts 8 | 9 | Run storybook locally: 10 | 11 | ```bash copy 12 | yarn storybook 13 | ``` 14 | 15 | Build storybook: 16 | 17 | ```bash copy 18 | yarn storybook:build 19 | ``` 20 | 21 | ## How to deploy on vercel 22 | 23 | 24 | TBD 25 | 26 | 27 | --- 28 | 29 | 30 | See `/packages/storybook` app as a reference [here](/apps/storybook-ui) 31 | 32 | -------------------------------------------------------------------------------- /packages/storyblok-setup/src/utils/checkEnv.ts: -------------------------------------------------------------------------------- 1 | import { color } from './color.ts'; 2 | 3 | /** 4 | * The function checks if the required environment variables STORYBLOK_SPACE_ID and 5 | * STORYBLOK_PERSONAL_ACCESS_TOKEN are set and returns true if they are, otherwise it logs an error 6 | * message and returns false. 7 | * @returns The function `checkEnv` returns a boolean value. It returns `true` if both 8 | * `process.env.STORYBLOK_SPACE_ID` and `process.env.STORYBLOK_PERSONAL_ACCESS_TOKEN` are truthy 9 | * values. Otherwise, it returns `false`. 10 | */ 11 | export const checkEnv = () => { 12 | console.log(color('info', '▶️ Starting to check the correctness of environmental variables')); 13 | 14 | if ( 15 | !process.env.STORYBLOK_SPACE_ID || 16 | !process.env.STORYBLOK_PERSONAL_ACCESS_TOKEN || 17 | !process.env.STORYBLOK_REGION 18 | ) { 19 | console.error(color('danger', '🚨 The environmental variables are incorrect.')); 20 | console.table({ 21 | STORYBLOK_SPACE_ID: process.env.STORYBLOK_SPACE_ID, 22 | STORYBLOK_PERSONAL_ACCESS_TOKEN: process.env.STORYBLOK_PERSONAL_ACCESS_TOKEN, 23 | STORYBLOK_REGION: process.env.STORYBLOK_REGION, 24 | }); 25 | 26 | return false; 27 | } 28 | 29 | console.log(color('success', '✅ The environmental variables are correct.')); 30 | 31 | return true; 32 | }; 33 | -------------------------------------------------------------------------------- /packages/env/src/env/env.mjs: -------------------------------------------------------------------------------- 1 | import { createEnv } from '@t3-oss/env-nextjs'; 2 | import { z } from 'zod'; 3 | 4 | const skipValidation = 5 | !!process.env.SKIP_ENV_VALIDATION && 6 | process.env.SKIP_ENV_VALIDATION !== 'false' && 7 | process.env.SKIP_ENV_VALIDATION !== '0'; 8 | 9 | export const env = createEnv({ 10 | skipValidation, 11 | server: {}, 12 | client: { 13 | NEXT_PUBLIC_APP_URL: z.string(), 14 | NEXT_PUBLIC_STORYBLOK_API_URL: z.string(), 15 | NEXT_PUBLIC_STORYBLOK_PREVIEW_TOKEN: z.string(), 16 | NEXT_PUBLIC_STORYBLOK_EXCLUDED_FOLDERS_FROM_ROUTING: z.string(), 17 | NEXT_PUBLIC_STORYBLOK_MAIN_APP_FOLDER: z.string(), 18 | NEXT_PUBLIC_STORYBLOK_TOKEN_VERSION: z 19 | .union([z.literal('draft'), z.literal('published')]) 20 | .nullish(), 21 | }, 22 | runtimeEnv: { 23 | NEXT_PUBLIC_APP_URL: process.env.NEXT_PUBLIC_APP_URL, 24 | NEXT_PUBLIC_STORYBLOK_API_URL: process.env.NEXT_PUBLIC_STORYBLOK_API_URL, 25 | NEXT_PUBLIC_STORYBLOK_TOKEN_VERSION: process.env.NEXT_PUBLIC_STORYBLOK_TOKEN_VERSION, 26 | NEXT_PUBLIC_STORYBLOK_PREVIEW_TOKEN: process.env.NEXT_PUBLIC_STORYBLOK_PREVIEW_TOKEN, 27 | NEXT_PUBLIC_STORYBLOK_MAIN_APP_FOLDER: process.env.NEXT_PUBLIC_STORYBLOK_MAIN_APP_FOLDER, 28 | NEXT_PUBLIC_STORYBLOK_EXCLUDED_FOLDERS_FROM_ROUTING: 29 | process.env.NEXT_PUBLIC_STORYBLOK_EXCLUDED_FOLDERS_FROM_ROUTING, 30 | }, 31 | }); 32 | -------------------------------------------------------------------------------- /packages/ui/src/Popover/Popover.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import * as PopoverPrimitive from '@radix-ui/react-popover'; 4 | import { ComponentPropsWithoutRef, ElementRef, forwardRef } from 'react'; 5 | 6 | import { cn } from '@natu/utils'; 7 | 8 | const Popover = PopoverPrimitive.Root; 9 | 10 | const PopoverTrigger = PopoverPrimitive.Trigger; 11 | 12 | const PopoverContent = forwardRef< 13 | ElementRef, 14 | ComponentPropsWithoutRef 15 | >(({ className, align = 'center', sideOffset = 4, ...props }, ref) => ( 16 | 17 | 27 | 28 | )); 29 | PopoverContent.displayName = PopoverPrimitive.Content.displayName; 30 | 31 | export { Popover, PopoverTrigger, PopoverContent }; 32 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "naturaily-storyblok", 3 | "version": "0.0.0", 4 | "private": true, 5 | "workspaces": [ 6 | "apps/*", 7 | "packages/*" 8 | ], 9 | "engines": { 10 | "node": ">=18.18.2" 11 | }, 12 | "packageManager": "yarn@1.22.19", 13 | "scripts": { 14 | "build": "turbo run build", 15 | "dev": "turbo run dev --concurrency 24", 16 | "dev:docs": "yarn workspace docs dev:docs", 17 | "prettier:format": "prettier --write \"**/*.{ts,tsx,md,mdx}\"", 18 | "eslint:format": "turbo run eslint:format --concurrency 24", 19 | "prepare": "husky install", 20 | "codegen": "turbo run codegen", 21 | "storyblok:setup": "yarn workspace @natu/storyblok-setup setup", 22 | "storybook": "yarn workspace @natu/storybook-ui storybook", 23 | "storybook:build": "yarn workspace @natu/storybook-ui storybook:build" 24 | }, 25 | "devDependencies": { 26 | "eslint-config-custom": "*", 27 | "husky": "9.0.11", 28 | "lint-staged": "15.2.2", 29 | "prettier": "^3.0.3", 30 | "tsconfig": "*", 31 | "turbo": "1.12.5" 32 | }, 33 | "lint-staged": { 34 | "apps/**/*": [ 35 | "yarn prettier:format", 36 | "eslint --fix --ext js,ts,jsx,tsx,md,mdx" 37 | ], 38 | "packages/**/*": [ 39 | "yarn prettier:format", 40 | "eslint --fix --ext js,ts,jsx,tsx,md,mdx" 41 | ], 42 | "*.json": [ 43 | "yarn prettier:format" 44 | ] 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /packages/storyblok-setup/src/components/data/redirect.ts: -------------------------------------------------------------------------------- 1 | import { ComponentSchema } from '../components.types.ts'; 2 | import { CONTENT_TYPE_COLOR } from '../consts.ts'; 3 | 4 | export const redirect: ComponentSchema = { 5 | componentGroup: 'content type', 6 | data: { 7 | name: 'redirect', 8 | display_name: 'Redirect', 9 | is_root: true, 10 | is_nestable: false, 11 | color: CONTENT_TYPE_COLOR, 12 | icon: 'block-wallet', 13 | schema: { 14 | oldPath: { 15 | type: 'text', 16 | pos: 0, 17 | required: true, 18 | description: `Valid value: 19 | / 20 | /new-path 21 | /new/new-path 22 | /blog/:slug 23 | .... 24 | 25 | The link should start with "/"`, 26 | }, 27 | newPath: { 28 | type: 'text', 29 | pos: 1, 30 | required: true, 31 | description: `Valid value: 32 | / 33 | /new-path 34 | /new/new-path 35 | /blog/:slug 36 | .... 37 | 38 | The link should start with "/"`, 39 | }, 40 | status: { 41 | type: 'option', 42 | pos: 2, 43 | required: true, 44 | default_value: '308', 45 | options: [ 46 | { 47 | name: 'Permanent - 308', 48 | value: '308', 49 | }, 50 | { 51 | name: 'Temporary - 307', 52 | value: '307', 53 | }, 54 | ], 55 | }, 56 | }, 57 | }, 58 | }; 59 | -------------------------------------------------------------------------------- /packages/storyblok-ui/src/components/elements/SBRichtext/SBRichtext.tsx: -------------------------------------------------------------------------------- 1 | import { StoryblokRichTextData, StoryblokRichtext } from '@natu/storyblok-richtext'; 2 | import { 3 | FontFamily, 4 | SBProps, 5 | Spacing, 6 | TextAlign, 7 | resolveStoryblokStyles, 8 | sbEditable, 9 | } from '@natu/storyblok-utils'; 10 | 11 | interface SBRichtextProps { 12 | content?: StoryblokRichTextData; 13 | textAlignMobile?: TextAlign; 14 | textAlignTablet?: TextAlign; 15 | textAlignDesktop?: TextAlign; 16 | fontFamily?: FontFamily; 17 | mtMobile?: Spacing; 18 | mtTablet?: Spacing; 19 | mtDesktop?: Spacing; 20 | mbMobile?: Spacing; 21 | mbTablet?: Spacing; 22 | mbDesktop?: Spacing; 23 | } 24 | 25 | export const SBRichtext = ({ blok }: SBProps) => { 26 | const { 27 | content, 28 | textAlignMobile, 29 | textAlignTablet, 30 | textAlignDesktop, 31 | fontFamily, 32 | mtMobile, 33 | mtTablet, 34 | mtDesktop, 35 | mbMobile, 36 | mbTablet, 37 | mbDesktop, 38 | } = blok; 39 | 40 | const className = resolveStoryblokStyles({ 41 | textAlign: textAlignMobile, 42 | textAlignTablet, 43 | textAlignDesktop, 44 | fontFamily, 45 | mt: mtMobile, 46 | mtTablet, 47 | mtDesktop, 48 | mb: mbMobile, 49 | mbTablet, 50 | mbDesktop, 51 | }); 52 | 53 | return ; 54 | }; 55 | -------------------------------------------------------------------------------- /apps/web/src/app/page.tsx: -------------------------------------------------------------------------------- 1 | import { Metadata, ResolvingMetadata } from 'next'; 2 | import { draftMode } from 'next/headers'; 3 | import { notFound } from 'next/navigation'; 4 | 5 | import { env } from '@natu/env'; 6 | import { relations, getStoryblokApi } from '@natu/storyblok-api'; 7 | import { getStoryblokSeoData } from '@natu/storyblok-seo'; 8 | import { DynamicRender } from '@natu/storyblok-utils'; 9 | 10 | export const generateMetadata = async ( 11 | _: unknown, 12 | parent: ResolvingMetadata, 13 | ): Promise => { 14 | const { isEnabled } = draftMode(); 15 | const { getContentNode } = getStoryblokApi({ draftMode: isEnabled }); 16 | 17 | const prevData = await parent; 18 | const configData = await getContentNode({ 19 | slug: env.NEXT_PUBLIC_STORYBLOK_MAIN_APP_FOLDER, 20 | relations, 21 | }); 22 | 23 | return getStoryblokSeoData(configData.ContentNode?.content.seo, { 24 | slug: '/', 25 | prevData, 26 | }); 27 | }; 28 | 29 | const Page = async () => { 30 | const { isEnabled } = draftMode(); 31 | const { getContentNode } = getStoryblokApi({ draftMode: isEnabled }); 32 | 33 | const story = await getContentNode({ 34 | slug: env.NEXT_PUBLIC_STORYBLOK_MAIN_APP_FOLDER, 35 | relations, 36 | }); 37 | 38 | if (!story || !story?.ContentNode) { 39 | return notFound(); 40 | } 41 | 42 | return ; 43 | }; 44 | 45 | export default Page; 46 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: Test application generation 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | - develop 8 | 9 | pull_request: 10 | types: [opened, synchronize, reopened] 11 | 12 | jobs: 13 | prettier-format: 14 | name: Prettier format 15 | runs-on: ubuntu-latest 16 | env: 17 | TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }} 18 | TURBO_TEAM: ${{ secrets.TURBO_TEAM }} 19 | SKIP_ENV_VALIDATION: ${{ secrets.SKIP_ENV_VALIDATION }} 20 | steps: 21 | - name: Checkout repository 22 | uses: actions/checkout@v3 23 | - name: Setup node 24 | uses: actions/setup-node@v3 25 | with: 26 | node-version: 18 27 | - name: Install dependencies 28 | run: yarn install 29 | - name: Run prettier format 30 | run: yarn prettier:format 31 | 32 | eslint-format: 33 | name: Eslint format 34 | runs-on: ubuntu-latest 35 | env: 36 | TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }} 37 | TURBO_TEAM: ${{ secrets.TURBO_TEAM }} 38 | SKIP_ENV_VALIDATION: ${{ secrets.SKIP_ENV_VALIDATION }} 39 | steps: 40 | - name: Checkout repository 41 | uses: actions/checkout@v3 42 | - name: Setup node 43 | uses: actions/setup-node@v3 44 | with: 45 | node-version: 18 46 | - name: Install dependencies 47 | run: yarn install 48 | - name: Run eslint format 49 | run: yarn eslint:format 50 | -------------------------------------------------------------------------------- /packages/storyblok-setup/src/componentGroup/setComponentGroups.ts: -------------------------------------------------------------------------------- 1 | import { storyblok } from '../utils/client.ts'; 2 | import { color } from '../utils/color.ts'; 3 | import { COMPONENTS_GROUP_ENDPOINT } from '../utils/endpoints.ts'; 4 | import { componentGroups } from './componentsGroup.ts'; 5 | 6 | export interface ComponentGroup { 7 | name: string; 8 | id: number; 9 | uuid: string; 10 | } 11 | 12 | export const setComponentGroups = async (): Promise => { 13 | console.log(color('info', '▶️ Starting workshop on component group setup...')); 14 | 15 | const requests = []; 16 | 17 | for (const group of componentGroups) { 18 | requests.push( 19 | storyblok.post(COMPONENTS_GROUP_ENDPOINT, { 20 | // @ts-ignore 21 | component_group: { 22 | name: group.name, 23 | }, 24 | }), 25 | ); 26 | } 27 | let componentGroupRes; 28 | 29 | try { 30 | componentGroupRes = await Promise.all(requests); 31 | } catch (err) { 32 | console.error(color('danger', `🚨 Component group error - ${JSON.stringify(err)}`)); 33 | } 34 | 35 | if (!componentGroupRes) { 36 | console.error(color('danger', '🚨 Component group error - Empty componentGroupRes array')); 37 | 38 | return undefined; 39 | } 40 | 41 | console.log(color('success', '✅ Component group setup complete.')); 42 | 43 | // @ts-ignore 44 | return componentGroupRes.map(group => group.data.component_group); 45 | }; 46 | -------------------------------------------------------------------------------- /packages/storyblok-utils/src/types/SBProps/SBProps.ts: -------------------------------------------------------------------------------- 1 | /* The `SbComponentType` interface is defining a generic type that takes one type parameter 2 | `TComponent` which defaults to `string`. It defines an object with three properties: `_uid` of type 3 | `string`, `component` of type `TComponent`, and an optional `_editable` property of type `string`. 4 | This interface is used as a base for other interfaces that define specific components in a page 5 | builder or content management system. */ 6 | export interface SbComponentType { 7 | _uid: string; 8 | component: TComponent; 9 | _editable?: string; 10 | } 11 | 12 | type SbBlokKeyDataTypes = string | number | object | boolean | undefined; 13 | 14 | export interface BlokItem extends SbComponentType { 15 | [index: string]: SbBlokKeyDataTypes; 16 | } 17 | 18 | /* The `SBProps` interface is defining a generic type that takes two type parameters: `TBlokProps` and 19 | `UComponentName`. `TBlokProps` is set to default to `SbComponentType`, which means it is an 20 | object with `_uid` and `component` properties of type `string`, and an optional `_editable` property 21 | of type `string`. `UComponentName` is set to default to `any`, which means it can be any string. */ 22 | export interface SBProps< 23 | TBlokProps = SbComponentType, 24 | UComponentName extends string = string, 25 | > { 26 | blok: TBlokProps & SbComponentType; 27 | } 28 | -------------------------------------------------------------------------------- /packages/ui/src/Typography/Typography.stories.tsx: -------------------------------------------------------------------------------- 1 | import type { Meta, StoryObj } from '@storybook/react'; 2 | 3 | import { Typography } from './Typography'; 4 | import { TypographyVariant } from './Typography.type'; 5 | import { typographyVariantsMobile } from './utils/getTypographyVariantStyles'; 6 | 7 | const meta: Meta = { 8 | title: 'Components/Typography', 9 | component: Typography, 10 | args: { 11 | children: 'Lorem ipsum dolor sit ament', 12 | variant: 'text-base', 13 | component: 'p', 14 | }, 15 | argTypes: { 16 | variant: { 17 | control: { 18 | type: 'select', 19 | }, 20 | options: Object.keys(typographyVariantsMobile), 21 | }, 22 | ref: { 23 | table: { disable: true }, 24 | }, 25 | }, 26 | }; 27 | 28 | export default meta; 29 | 30 | type Story = StoryObj; 31 | 32 | const allItems = Object.keys(typographyVariantsMobile) as TypographyVariant[]; 33 | 34 | export const All: Story = { 35 | argTypes: { 36 | variant: { 37 | table: { disable: true }, 38 | }, 39 | component: { 40 | table: { disable: true }, 41 | }, 42 | }, 43 | render: ({ children }) => ( 44 |
    45 | {allItems.map(variant => ( 46 | 47 | {variant} > {children} 48 | 49 | ))} 50 |
    51 | ), 52 | }; 53 | 54 | export const Base: Story = {}; 55 | -------------------------------------------------------------------------------- /packages/storyblok-revalidate/src/revalidateHandler/revalidateHandler.ts: -------------------------------------------------------------------------------- 1 | import { revalidatePath, revalidateTag } from 'next/cache'; 2 | import { NextRequest } from 'next/server'; 3 | 4 | import { env } from '@natu/env'; 5 | import { TAGS } from '@natu/storyblok-api'; 6 | import { getSlugWithoutAppName } from '@natu/storyblok-utils'; 7 | 8 | export const revalidateHandler = async (request: NextRequest): Promise => { 9 | const { searchParams } = new URL(request.url); 10 | 11 | const token = searchParams.get('token'); 12 | 13 | // Check if token is valid 14 | if (token !== env.NEXT_PUBLIC_STORYBLOK_PREVIEW_TOKEN) { 15 | return new Response(JSON.stringify({ message: 'Invalid token' }), { status: 401 }); 16 | } 17 | 18 | const { full_slug: storyblokSlug } = await request.json(); 19 | 20 | if (!storyblokSlug) { 21 | return Response.json({ 22 | revalidated: false, 23 | now: Date.now(), 24 | message: 'Missing path to revalidate', 25 | }); 26 | } 27 | 28 | const slug = getSlugWithoutAppName(storyblokSlug) || '/'; 29 | 30 | if (slug.includes(env.NEXT_PUBLIC_STORYBLOK_EXCLUDED_FOLDERS_FROM_ROUTING)) { 31 | revalidatePath('/', 'layout'); 32 | revalidatePath('/[slug]', 'layout'); 33 | revalidateTag(TAGS.SB_CONFIG); 34 | 35 | return Response.json({ revalidated: true, now: Date.now(), slug, tags: [TAGS.SB_CONFIG] }); 36 | } 37 | 38 | revalidatePath(slug); 39 | 40 | return Response.json({ revalidated: true, now: Date.now(), slug }); 41 | }; 42 | -------------------------------------------------------------------------------- /packages/storyblok-setup/src/stories/utils/createConfigFolder.ts: -------------------------------------------------------------------------------- 1 | import { storyblok } from '../../utils/client.ts'; 2 | import { color } from '../../utils/color.ts'; 3 | import { STORIES_ENDPOINT } from '../../utils/endpoints.ts'; 4 | 5 | const rootSlug = 'configuration-a93cfcb3'; 6 | 7 | interface CreateConfigFolderInput { 8 | parentID?: number | string; 9 | } 10 | 11 | interface CreateConfigFolderOutput { 12 | rootConfigFolderID?: number; 13 | NEXT_PUBLIC_STORYBLOK_EXCLUDED_FOLDERS_FROM_ROUTING: string; 14 | } 15 | 16 | export const createConfigFolder = async ({ 17 | parentID, 18 | }: CreateConfigFolderInput): Promise => { 19 | let rootConfigFolderID: number | undefined; 20 | 21 | if (!parentID) { 22 | console.log(color('subtitle', 'ℹ️ Missing "rootAppFolderID".')); 23 | 24 | return; 25 | } 26 | 27 | try { 28 | const res = await storyblok.post(STORIES_ENDPOINT, { 29 | story: { 30 | name: 'Config', 31 | slug: rootSlug, 32 | is_folder: true, 33 | // @ts-ignore 34 | default_root: 'page', 35 | disble_fe_editor: true, 36 | parent_id: `${parentID}`, 37 | }, 38 | }); 39 | 40 | // @ts-ignore 41 | rootConfigFolderID = res.data.story.id; 42 | } catch (err) { 43 | console.log(err); 44 | } 45 | 46 | // eslint-disable-next-line consistent-return 47 | return { 48 | rootConfigFolderID, 49 | NEXT_PUBLIC_STORYBLOK_EXCLUDED_FOLDERS_FROM_ROUTING: rootSlug, 50 | }; 51 | }; 52 | -------------------------------------------------------------------------------- /apps/web/getStoryblokRedirects.mjs: -------------------------------------------------------------------------------- 1 | import StoryblokClient from 'storyblok-js-client'; 2 | 3 | const getStoryblokRedirects = async () => { 4 | const Storyblok = new StoryblokClient({ 5 | accessToken: process.env.NEXT_PUBLIC_STORYBLOK_PREVIEW_TOKEN, 6 | cache: { 7 | clear: 'auto', 8 | type: 'memory', 9 | }, 10 | }); 11 | 12 | let redirects = null; 13 | 14 | try { 15 | const { data } = await Storyblok.get('cdn/stories', { 16 | filter_query: { 17 | component: { 18 | in: 'redirect', 19 | }, 20 | oldPath: { 21 | is: 'not_empty', 22 | }, 23 | newPath: { 24 | is: 'not_empty', 25 | }, 26 | status: { 27 | is: 'not_empty', 28 | }, 29 | }, 30 | starts_with: process.env.NEXT_PUBLIC_STORYBLOK_MAIN_APP_FOLDER, 31 | }); 32 | const items = data?.stories || []; 33 | 34 | redirects = items 35 | .map(item => ({ 36 | source: item?.content?.oldPath || '', 37 | destination: item?.content?.newPath || '', 38 | permanent: !(item?.content?.status === '307'), 39 | })) 40 | .filter( 41 | item => 42 | item.source && 43 | item.source.startsWith('/') && 44 | item.destination && 45 | item.destination.startsWith('/'), 46 | ); 47 | } catch (err) { 48 | console.log('getStoryblokRedirects Error -> ', err); 49 | } 50 | 51 | return redirects ?? []; 52 | }; 53 | 54 | export { getStoryblokRedirects }; 55 | -------------------------------------------------------------------------------- /packages/storyblok-ui/src/components/elements/SBCta/SBCta.tsx: -------------------------------------------------------------------------------- 1 | import { Link } from '@natu/next-link'; 2 | import { 3 | SBProps, 4 | Spacing, 5 | StoryblokLink, 6 | getLinkPropsFromStoryblok, 7 | resolveStoryblokStyles, 8 | sbEditable, 9 | } from '@natu/storyblok-utils'; 10 | import { Button, ButtonProps } from '@natu/ui'; 11 | 12 | interface SBButtonProps { 13 | content?: string; 14 | link?: StoryblokLink; 15 | variant?: ButtonProps['variant']; 16 | size?: ButtonProps['size']; 17 | mtMobile?: Spacing; 18 | mtTablet?: Spacing; 19 | mtDesktop?: Spacing; 20 | mbMobile?: Spacing; 21 | mbTablet?: Spacing; 22 | mbDesktop?: Spacing; 23 | } 24 | 25 | export const SBCta = ({ blok }: SBProps) => { 26 | const { 27 | content, 28 | size, 29 | link, 30 | variant, 31 | mbMobile, 32 | mbTablet, 33 | mbDesktop, 34 | mtMobile, 35 | mtTablet, 36 | mtDesktop, 37 | } = blok; 38 | 39 | const linkProps = getLinkPropsFromStoryblok(link); 40 | 41 | if (!linkProps.href) { 42 | return null; 43 | } 44 | 45 | const className = resolveStoryblokStyles({ 46 | mt: mtMobile, 47 | mtTablet, 48 | mtDesktop, 49 | mb: mbMobile, 50 | mbTablet, 51 | mbDesktop, 52 | }); 53 | 54 | return ( 55 | 65 | ); 66 | }; 67 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Naturaily's Jamstack starter 2 | 3 | Welcome to the Naturaily Storyblok starter! 4 | 5 | ## What is our project? 6 | 7 | It's a **ready-made set of tools, templates, and examples** that facilitate the initiation of a project or task. 8 | With it, we can avoid the need for continuously recreating the same basic elements from scratch, saving time and effort. 9 | 10 | Tech stack is based on: 11 | 12 | - 🏎️ **Next.js** - fast and optimized React's framework 13 | - 🌈 **Turborepo** - high-performance build system for JavaScript and TypeScript codebases 14 | - 💅 **Tailwind CSS** - utility-first CSS framework for rapid UI development 15 | 16 | ...including ready-to-use configurations: 17 | 18 | - 📚 **Storyblok** - headless CMS to manage your content 19 | - 📕 **Storybook** - for testing and showcasing components 20 | - ✨ **ESlint** & **Prettier** - for clean, consistent, and error-free code 21 | - 🚀 **GitHub Actions** for smooth workflows 22 | - 💻 **T3 Env** - validation for type-safe environment variables 23 | - 🧬 **Codegen** - for auto-generating code from your GraphQL schema 24 | - 🔥 **Framer motion** - for powerful animations 25 | 26 | ...and much, much more. 27 | 28 | - [Discover its full potential in the documentation](https://naturaily-starter-docs.vercel.app/) 29 | - [App demo](https://naturaily-starter.vercel.app/) 30 | 31 | ## License 32 | 33 | Licensed under the MIT License, Copyright © 2024 34 | 35 | See [LICENSE](LICENSE) for more information. 36 | 37 | --- 38 | 39 | Made with much 🧡 and 💪 by Naturaily 40 | -------------------------------------------------------------------------------- /apps/docs/pages/project-structure.mdx: -------------------------------------------------------------------------------- 1 | import { FileTree } from 'nextra/components'; 2 | 3 | # Project structure 4 | 5 | ### 🔥 Difference between `apps` and `packages` 6 | 7 | - `apps` are reusable applications. Each of them is deployable. 8 | - `packages` in turn are shared between apps - this is the place where reusable logic lays. 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | -------------------------------------------------------------------------------- /packages/env/README.md: -------------------------------------------------------------------------------- 1 | # @natu/env 2 | 3 | ## 💻 Environment Variables handling 4 | 5 | [T3 Env](https://env.t3.gg/) is a library that provides environmental variables checking at build time, type validation and transforming. It ensures that your application is using the correct environment variables and their values are of the expected type. You’ll never again struggle with runtime errors caused by incorrect environment variable usage. 6 | 7 | Config file is located at `env.mjs`. Simply set your client and server variables and import `env` from any file in your project. 8 | 9 | ```ts 10 | export const env = createEnv({ 11 | server: { 12 | // Server variables 13 | SECRET_KEY: z.string(), 14 | }, 15 | client: { 16 | // Client variables 17 | API_URL: z.string().url(), 18 | }, 19 | runtimeEnv: { 20 | // Assign runtime variables 21 | SECRET_KEY: process.env.SECRET_KEY, 22 | API_URL: process.env.NEXT_PUBLIC_API_URL, 23 | }, 24 | }); 25 | ``` 26 | 27 | If the required environment variables are not set, you'll get an error message: 28 | 29 | ```sh 30 | ❌ Invalid environment variables: { SECRET_KEY: [ 'Required' ] } 31 | ``` 32 | 33 | ## 🏗️ Validate schema on build 34 | 35 | Import your newly created file in your `next.config.mjs`. This will make sure your environment variables are validated at build time which will save you a lot of time and headaches down the road. 36 | 37 | ```mjs 38 | import '@natu/env/src/env/env.mjs'; 39 | 40 | /** @type {import("next").NextConfig} */ 41 | const config = { 42 | /** ... */ 43 | }; 44 | 45 | export default config; 46 | ``` 47 | -------------------------------------------------------------------------------- /apps/docs/pages/packages/env.mdx: -------------------------------------------------------------------------------- 1 | # @natu/env 2 | 3 | ## 💻 Environment Variables handling 4 | 5 | [T3 Env ↗](https://env.t3.gg/) is a library that provides environmental variables checking at build time, type validation and transforming. It ensures that your application is using the correct environment variables and their values are of the expected type. You’ll never again struggle with runtime errors caused by incorrect environment variable usage. 6 | 7 | Config file is located at `env.mjs`. Simply set your client and server variables and import `env` from any file in your project. 8 | 9 | ```ts 10 | export const env = createEnv({ 11 | server: { 12 | // Server variables 13 | SECRET_KEY: z.string(), 14 | }, 15 | client: { 16 | // Client variables 17 | API_URL: z.string().url(), 18 | }, 19 | runtimeEnv: { 20 | // Assign runtime variables 21 | SECRET_KEY: process.env.SECRET_KEY, 22 | API_URL: process.env.NEXT_PUBLIC_API_URL, 23 | }, 24 | }); 25 | ``` 26 | 27 | If the required environment variables are not set, you'll get an error message: 28 | 29 | ```sh 30 | ❌ Invalid environment variables: { SECRET_KEY: [ 'Required' ] } 31 | ``` 32 | 33 | ## 🏗️ Validate schema on build 34 | 35 | Import your newly created file in your `next.config.mjs`. This will make sure your environment variables are validated at build time which will save you a lot of time and headaches down the road. 36 | 37 | ```ts 38 | import '@natu/env/src/env/env.mjs'; 39 | 40 | /** @type {import("next").NextConfig} */ 41 | const config = { 42 | /** ... */ 43 | }; 44 | 45 | export default config; 46 | ``` 47 | -------------------------------------------------------------------------------- /packages/next-api-fetcher/src/apiFetcher/apiFetcher.ts: -------------------------------------------------------------------------------- 1 | interface NextFetchRequestConfig { 2 | revalidate?: number | false; 3 | tags?: string[]; 4 | } 5 | 6 | type Exact = { [K in keyof T]: T[K] }; 7 | 8 | interface FetcherInput { 9 | query: string; 10 | cache?: RequestCache; 11 | variables?: Exact; 12 | headers?: HeadersInit; 13 | next?: NextFetchRequestConfig; 14 | } 15 | 16 | export class ApiFetcher { 17 | constructor( 18 | private endpoint: string, 19 | public readonly requestParams: Pick = {}, 20 | ) { 21 | this.fetcher = this.fetcher.bind(this); 22 | } 23 | 24 | async fetcher({ 25 | cache, 26 | headers, 27 | query, 28 | next, 29 | variables, 30 | ...rest 31 | }: FetcherInput): Promise<{ status: number; data: T } | never> { 32 | try { 33 | const result = await fetch(this.endpoint, { 34 | method: 'POST', 35 | headers: { 36 | 'Content-Type': 'application/json', 37 | ...this.requestParams.headers, 38 | ...headers, 39 | }, 40 | body: JSON.stringify({ 41 | query, 42 | ...(variables && { variables }), 43 | }), 44 | cache: cache || this.requestParams.cache, 45 | next: next || this.requestParams.next, 46 | ...rest, 47 | }); 48 | 49 | const data = await result.json(); 50 | 51 | return { 52 | data: data.data, 53 | status: data.status, 54 | }; 55 | } catch (e) { 56 | // eslint-disable-next-line @typescript-eslint/no-throw-literal 57 | throw { 58 | error: e, 59 | query, 60 | }; 61 | } 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /packages/storyblok-ui/src/components/elements/SBGrid/SBGrid.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | BlokItem, 3 | DynamicRender, 4 | Grid, 5 | SBProps, 6 | Size, 7 | Spacing, 8 | resolveStoryblokStyles, 9 | sbEditable, 10 | } from '@natu/storyblok-utils'; 11 | 12 | interface SBGridProps { 13 | body?: BlokItem[]; 14 | gridMobile?: Grid; 15 | gridTablet?: Grid; 16 | gridDesktop?: Grid; 17 | gapMobile?: Spacing; 18 | gapTablet?: Spacing; 19 | gapDesktop?: Spacing; 20 | containerSize?: Size; 21 | mtMobile?: Spacing; 22 | mtTablet?: Spacing; 23 | mtDesktop?: Spacing; 24 | mbMobile?: Spacing; 25 | mbTablet?: Spacing; 26 | mbDesktop?: Spacing; 27 | tag?: string; 28 | } 29 | 30 | export const SBGrid = ({ blok }: SBProps) => { 31 | const { 32 | body, 33 | gridMobile, 34 | gridTablet, 35 | gridDesktop, 36 | gapMobile, 37 | gapTablet, 38 | gapDesktop, 39 | containerSize, 40 | mbMobile, 41 | mbTablet, 42 | mbDesktop, 43 | mtMobile, 44 | mtTablet, 45 | mtDesktop, 46 | tag, 47 | } = blok; 48 | 49 | const className = resolveStoryblokStyles({ 50 | gridMobile, 51 | gridTablet, 52 | gridDesktop, 53 | gap: gapMobile, 54 | gapTablet, 55 | gapDesktop, 56 | size: containerSize, 57 | mt: mtMobile, 58 | mtTablet, 59 | mtDesktop, 60 | mb: mbMobile, 61 | mbTablet, 62 | mbDesktop, 63 | className: 'grid', 64 | }); 65 | 66 | const Comp = tag || 'div'; 67 | const asListItem = tag === 'ul' || tag === 'ol'; 68 | 69 | return ( 70 | 71 | 72 | 73 | ); 74 | }; 75 | -------------------------------------------------------------------------------- /packages/storyblok-api/src/api.ts: -------------------------------------------------------------------------------- 1 | import { env } from '@natu/env'; 2 | import { ApiFetcher } from '@natu/next-api-fetcher'; 3 | import { isDraftMode } from '@natu/storyblok-preview'; 4 | 5 | import { Sdk, SdkFunctionWrapper, getSdk } from './sdk'; 6 | 7 | const defaultRequestHeaders = { 8 | token: env.NEXT_PUBLIC_STORYBLOK_PREVIEW_TOKEN, 9 | version: 'published', 10 | }; 11 | 12 | const { fetcher } = new ApiFetcher(env.NEXT_PUBLIC_STORYBLOK_API_URL, { 13 | headers: defaultRequestHeaders, 14 | cache: 'force-cache', 15 | }); 16 | 17 | export interface GetStoryblokApiInput { 18 | draftMode: boolean; 19 | } 20 | 21 | /** 22 | * This TypeScript function returns an SDK with methods that make GraphQL requests, and sets headers 23 | * based on whether draft mode is enabled. 24 | * @param {GetStoryblokApiInput} - - `GetApiInput`: an object containing the `draftMode` boolean parameter, 25 | * which defaults to `false`. 26 | * @returns A function that returns an SDK object with methods for making GraphQL requests, wrapped in 27 | * a function that sets headers based on the `draftMode` parameter. 28 | */ 29 | export const getStoryblokApi = ({ draftMode = false }: GetStoryblokApiInput): Sdk => { 30 | const isDraftModeEnabled = isDraftMode(draftMode); 31 | 32 | const wrapper: SdkFunctionWrapper = action => { 33 | // set headers based on draftMode (previewMode) 34 | const headers = { 35 | version: isDraftModeEnabled ? 'draft' : 'published', 36 | }; 37 | 38 | return action({ 39 | headers, 40 | cache: isDraftModeEnabled ? 'no-store' : 'force-cache', 41 | }); // pass other global fetch options 42 | }; 43 | 44 | // Return all methods from the SDK 45 | return getSdk(fetcher, wrapper); 46 | }; 47 | -------------------------------------------------------------------------------- /packages/storyblok-utils/src/utils/resolveStoryblokStyles/styles/flex/flex.ts: -------------------------------------------------------------------------------- 1 | export type JustifyItems = 'center' | 'end' | 'spaceBetween' | 'start' | 'around' | 'evenly'; 2 | 3 | export const justifyItemsVariants: Record = { 4 | center: 'justify-center', 5 | end: 'justify-end', 6 | spaceBetween: 'justify-between', 7 | start: 'justify-start', 8 | around: 'justify-around', 9 | evenly: 'justify-evenly', 10 | }; 11 | 12 | export const justifyItemsTabletVariants: Record = { 13 | center: 'md:justify-center', 14 | end: 'md:justify-end', 15 | spaceBetween: 'md:justify-between', 16 | start: 'md:justify-start', 17 | around: 'md:justify-around', 18 | evenly: 'md:justify-evenly', 19 | }; 20 | 21 | export const justifyItemsDesktopVariants: Record = { 22 | center: 'lg:justify-center', 23 | end: 'lg:justify-end', 24 | spaceBetween: 'lg:justify-between', 25 | start: 'lg:justify-start', 26 | around: 'lg:justify-around', 27 | evenly: 'lg:justify-evenly', 28 | }; 29 | 30 | export type AlignItems = 'start' | 'center' | 'end' | 'baseline'; 31 | 32 | export const alignItemsVariants: Record = { 33 | start: 'items-start', 34 | center: 'items-center', 35 | end: 'items-end', 36 | baseline: 'items-baseline', 37 | }; 38 | 39 | export const alignItemsTabletVariants: Record = { 40 | start: 'md:items-start', 41 | center: 'md:items-center', 42 | end: 'md:items-end', 43 | baseline: 'md:items-baseline', 44 | }; 45 | 46 | export const alignItemsDesktopVariants: Record = { 47 | start: 'lg:items-start', 48 | center: 'lg:items-center', 49 | end: 'lg:items-end', 50 | baseline: 'lg:items-baseline', 51 | }; 52 | -------------------------------------------------------------------------------- /packages/storyblok-setup/src/stories/utils/createLayoutsStories.ts: -------------------------------------------------------------------------------- 1 | import { storyblok } from '../../utils/client.ts'; 2 | import { color } from '../../utils/color.ts'; 3 | import { STORIES_ENDPOINT } from '../../utils/endpoints.ts'; 4 | import { layoutContent } from '../data/layoutContent.ts'; 5 | 6 | export interface LayoutStoryData { 7 | uuid: string; 8 | slug: string; 9 | } 10 | interface CreateLayoutsStoriesInput { 11 | parentFolderID?: number | null; 12 | homepageUUID?: string; 13 | NEXT_PUBLIC_STORYBLOK_MAIN_APP_FOLDER?: string; 14 | } 15 | interface CreateLayoutsStoriesOutput { 16 | layoutsUUID?: LayoutStoryData[]; 17 | } 18 | 19 | export const createLayoutsStories = async ({ 20 | parentFolderID, 21 | homepageUUID, 22 | NEXT_PUBLIC_STORYBLOK_MAIN_APP_FOLDER, 23 | }: CreateLayoutsStoriesInput): Promise => { 24 | const requests = []; 25 | 26 | for (const layout of layoutContent({ 27 | homepageUUID, 28 | NEXT_PUBLIC_STORYBLOK_MAIN_APP_FOLDER: `${NEXT_PUBLIC_STORYBLOK_MAIN_APP_FOLDER}/`, 29 | })) { 30 | requests.push( 31 | storyblok.post(STORIES_ENDPOINT, { 32 | story: { 33 | ...layout, 34 | parent_id: `${parentFolderID}`, 35 | }, 36 | publish: 1, 37 | }), 38 | ); 39 | } 40 | 41 | let layoutsUUID; 42 | 43 | try { 44 | const res = await Promise.all(requests); 45 | 46 | layoutsUUID = res.map(item => ({ 47 | // @ts-ignore 48 | uuid: item.data.story.uuid, 49 | // @ts-ignore 50 | slug: item.data.story.slug, 51 | })); 52 | } catch (err) { 53 | console.error(color('danger', `🚨 Layout stories - ${JSON.stringify(err)}`)); 54 | } 55 | 56 | return { 57 | layoutsUUID, 58 | }; 59 | }; 60 | -------------------------------------------------------------------------------- /packages/storyblok-ui/src/components/elements/SBTable/SBTable.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | SBProps, 3 | Spacing, 4 | StoryblokTable, 5 | resolveStoryblokStyles, 6 | sbEditable, 7 | } from '@natu/storyblok-utils'; 8 | import { 9 | Table, 10 | TableBody, 11 | TableCaption, 12 | TableCell, 13 | TableHead, 14 | TableHeader, 15 | TableRow, 16 | } from '@natu/ui'; 17 | 18 | interface SBTableProps { 19 | table?: StoryblokTable; 20 | caption?: string; 21 | mtMobile?: Spacing; 22 | mtTablet?: Spacing; 23 | mtDesktop?: Spacing; 24 | mbMobile?: Spacing; 25 | mbTablet?: Spacing; 26 | mbDesktop?: Spacing; 27 | } 28 | 29 | export const SBTable = ({ blok }: SBProps) => { 30 | const { caption, table, mbMobile, mbTablet, mbDesktop, mtMobile, mtTablet, mtDesktop } = blok; 31 | 32 | if (!table) { 33 | return null; 34 | } 35 | 36 | const { tbody, thead } = table; 37 | 38 | const className = resolveStoryblokStyles({ 39 | mt: mtMobile, 40 | mtTablet, 41 | mtDesktop, 42 | mb: mbMobile, 43 | mbTablet, 44 | mbDesktop, 45 | }); 46 | 47 | return ( 48 | 49 | {caption && {caption}} 50 | 51 | 52 | {thead?.map(item => {item.value})} 53 | 54 | 55 | 56 | {tbody?.map(rowItem => ( 57 | 58 | {rowItem.body?.map(cellItem => ( 59 | {cellItem.value} 60 | ))} 61 | 62 | ))} 63 | 64 |
    65 | ); 66 | }; 67 | -------------------------------------------------------------------------------- /packages/storyblok-setup/src/stories/utils/createRootFolder.ts: -------------------------------------------------------------------------------- 1 | import prompts from 'prompts'; 2 | import slugify from 'slugify'; 3 | 4 | import { capitalizeFirstLetter } from '../../utils/capitalizeFirstLetter.ts'; 5 | import { storyblok } from '../../utils/client.ts'; 6 | import { color } from '../../utils/color.ts'; 7 | import { STORIES_ENDPOINT } from '../../utils/endpoints.ts'; 8 | 9 | interface CreateRootFolderOutput { 10 | rootAppFolderID?: number; 11 | NEXT_PUBLIC_STORYBLOK_MAIN_APP_FOLDER: `${string}-root`; 12 | } 13 | 14 | export const createRootFolder = async (): Promise => { 15 | const rootFolderName = await prompts({ 16 | type: 'text', 17 | name: 'value', 18 | message: 'What is the name of your application / website?', 19 | initial: 'my-app', 20 | validate: (value?: string) => !!value, 21 | }); 22 | 23 | if (!rootFolderName.value) { 24 | console.log(color('subtitle', 'ℹ️ No root folder name provided')); 25 | 26 | return; 27 | } 28 | 29 | const rootFolder = rootFolderName.value.toLowerCase(); 30 | 31 | let rootAppFolderID: number | undefined; 32 | 33 | try { 34 | const res = await storyblok.post(STORIES_ENDPOINT, { 35 | story: { 36 | name: capitalizeFirstLetter(rootFolder), 37 | slug: slugify(`${rootFolder}-root`), 38 | is_folder: true, 39 | // @ts-ignore 40 | default_root: 'page', 41 | parent_id: undefined, 42 | }, 43 | }); 44 | 45 | // @ts-ignore 46 | rootAppFolderID = res.data.story.id; 47 | } catch (err) { 48 | console.log(err); 49 | } 50 | 51 | // eslint-disable-next-line consistent-return 52 | return { 53 | rootAppFolderID, 54 | NEXT_PUBLIC_STORYBLOK_MAIN_APP_FOLDER: `${rootFolder}-root`, 55 | }; 56 | }; 57 | -------------------------------------------------------------------------------- /packages/storyblok-setup/src/stories/utils/createSpecialPagesStories.ts: -------------------------------------------------------------------------------- 1 | import { storyblok } from '../../utils/client.ts'; 2 | import { color } from '../../utils/color.ts'; 3 | import { STORIES_ENDPOINT } from '../../utils/endpoints.ts'; 4 | import { specialPagesContent } from '../data/specialPagesContent.ts'; 5 | 6 | interface SpecialPagesUUIDData { 7 | uuid: string; 8 | slug: string; 9 | } 10 | 11 | interface CreateSpecialPagesStoriesInput { 12 | parentFolderID?: number | null; 13 | homepageUUID?: string; 14 | NEXT_PUBLIC_STORYBLOK_MAIN_APP_FOLDER?: string; 15 | } 16 | 17 | interface CreateSpecialPagesStoriesOutput { 18 | specialPagesUUID?: SpecialPagesUUIDData[]; 19 | } 20 | 21 | export const createSpecialPagesStories = async ({ 22 | parentFolderID, 23 | NEXT_PUBLIC_STORYBLOK_MAIN_APP_FOLDER, 24 | homepageUUID, 25 | }: CreateSpecialPagesStoriesInput): Promise => { 26 | const requests = []; 27 | 28 | for (const story of specialPagesContent({ 29 | homepageUUID, 30 | NEXT_PUBLIC_STORYBLOK_MAIN_APP_FOLDER: `${NEXT_PUBLIC_STORYBLOK_MAIN_APP_FOLDER}/`, 31 | })) { 32 | requests.push( 33 | storyblok.post(STORIES_ENDPOINT, { 34 | story: { 35 | ...story, 36 | parent_id: `${parentFolderID}`, 37 | }, 38 | publish: 1, 39 | }), 40 | ); 41 | } 42 | let specialPagesUUID; 43 | 44 | try { 45 | const res = await Promise.all(requests); 46 | specialPagesUUID = res.map(item => ({ 47 | // @ts-ignore 48 | uuid: item.data.story.uuid, 49 | // @ts-ignore 50 | slug: item.data.story.slug, 51 | })); 52 | } catch (err) { 53 | console.error(color('danger', `🚨 Layout stories - ${JSON.stringify(err)}`)); 54 | } 55 | 56 | return { specialPagesUUID }; 57 | }; 58 | -------------------------------------------------------------------------------- /packages/storyblok-ui/src/components/elements/SBContainer/SBContainer.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | BlokItem, 3 | DynamicRender, 4 | SBProps, 5 | Size, 6 | Spacing, 7 | resolveStoryblokStyles, 8 | sbEditable, 9 | } from '@natu/storyblok-utils'; 10 | 11 | interface SBContainerProps { 12 | body?: BlokItem[]; 13 | containerSize?: Size; 14 | mtMobile?: Spacing; 15 | mtTablet?: Spacing; 16 | mtDesktop?: Spacing; 17 | mbMobile?: Spacing; 18 | mbTablet?: Spacing; 19 | mbDesktop?: Spacing; 20 | pxMobile?: Spacing; 21 | pxTablet?: Spacing; 22 | pxDesktop?: Spacing; 23 | pyMobile?: Spacing; 24 | pyTablet?: Spacing; 25 | pyDesktop?: Spacing; 26 | tag?: string; 27 | } 28 | 29 | export const SBContainer = ({ blok }: SBProps) => { 30 | const { 31 | body, 32 | containerSize, 33 | mbMobile, 34 | mbTablet, 35 | mbDesktop, 36 | mtMobile, 37 | mtTablet, 38 | mtDesktop, 39 | pxMobile, 40 | pxTablet, 41 | pxDesktop, 42 | pyMobile, 43 | pyTablet, 44 | pyDesktop, 45 | tag, 46 | } = blok; 47 | 48 | const className = resolveStoryblokStyles({ 49 | size: containerSize, 50 | mt: mtMobile, 51 | mtTablet, 52 | mtDesktop, 53 | mb: mbMobile, 54 | mbTablet, 55 | mbDesktop, 56 | pr: pxMobile, 57 | prTablet: pxTablet, 58 | prDesktop: pxDesktop, 59 | pl: pxMobile, 60 | plTablet: pxTablet, 61 | plDesktop: pxDesktop, 62 | pt: pyMobile, 63 | ptTablet: pyTablet, 64 | ptDesktop: pyDesktop, 65 | pb: pyMobile, 66 | pbTablet: pyTablet, 67 | pbDesktop: pyDesktop, 68 | }); 69 | 70 | const Comp = tag || 'div'; 71 | 72 | return ( 73 | 74 | 75 | 76 | ); 77 | }; 78 | -------------------------------------------------------------------------------- /packages/storyblok-richtext/src/components/MarkLink/MarkLink.tsx: -------------------------------------------------------------------------------- 1 | import { ReactNode } from 'react'; 2 | 3 | import { env } from '@natu/env'; 4 | import { Link } from '@natu/next-link'; 5 | import { StoryblokLink, getLinkPropsFromStoryblok } from '@natu/storyblok-utils'; 6 | import { Button } from '@natu/ui'; 7 | 8 | interface MarkLinkProps { 9 | linktype?: string; 10 | href?: string; 11 | target?: string; 12 | anchor?: string; 13 | uuid?: string; 14 | } 15 | 16 | /** 17 | * This function creates a StoryblokLink object from provided props and returns an Anchor component 18 | * with link properties obtained from the StoryblokLink object. 19 | * @param {ReactNode} children - ReactNode, which is a type for any valid React child element, such as 20 | * a string, number, or JSX element. 21 | * @param {MarkLinkProps} props - The `props` parameter is an object that contains the following 22 | * properties: 23 | * @returns The `markLink` function returns a JSX element that renders an `Anchor` component with the 24 | * `linkProps` obtained from the `getLinkPropsFromStoryblok` function, and the `children` passed as a 25 | * parameter. 26 | */ 27 | export const MarkLink = ( 28 | children: ReactNode, 29 | { linktype, href, target, anchor, uuid }: MarkLinkProps, 30 | ) => { 31 | const hrefWithoutAppFolder = href?.replace(`${env.NEXT_PUBLIC_STORYBLOK_MAIN_APP_FOLDER}/`, ''); 32 | 33 | const storyblokLink = { 34 | id: uuid, 35 | linktype, 36 | cached_url: hrefWithoutAppFolder, 37 | email: hrefWithoutAppFolder, 38 | url: hrefWithoutAppFolder, 39 | target, 40 | anchor, 41 | } as StoryblokLink; 42 | 43 | const linkProps = getLinkPropsFromStoryblok(storyblokLink); 44 | 45 | return ( 46 | 49 | ); 50 | }; 51 | -------------------------------------------------------------------------------- /packages/ui/src/ScrollArea/ScrollArea.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import * as ScrollAreaPrimitive from '@radix-ui/react-scroll-area'; 4 | import { ComponentPropsWithoutRef, ElementRef, forwardRef } from 'react'; 5 | 6 | import { cn } from '@natu/utils'; 7 | 8 | const ScrollBar = forwardRef< 9 | ElementRef, 10 | ComponentPropsWithoutRef 11 | >(({ className, orientation = 'vertical', ...props }, ref) => ( 12 | 23 | 26 | 27 | )); 28 | ScrollBar.displayName = ScrollAreaPrimitive.ScrollAreaScrollbar.displayName; 29 | 30 | const ScrollArea = forwardRef< 31 | ElementRef, 32 | ComponentPropsWithoutRef 33 | >(({ className, children, ...props }, ref) => ( 34 | 39 | 40 | {children} 41 | 42 | 43 | 44 | 45 | )); 46 | ScrollArea.displayName = ScrollAreaPrimitive.Root.displayName; 47 | 48 | export { ScrollArea, ScrollBar }; 49 | -------------------------------------------------------------------------------- /apps/docs/pages/getting-started/deploy-on-vercel.mdx: -------------------------------------------------------------------------------- 1 | import { Steps, Callout } from 'nextra/components'; 2 | 3 | 4 | ### Import repository to Vercel 5 | 6 | ![Vercel](https://a.storyblok.com/f/218794/2452x2052/aa7c8a613b/screenshot-2024-02-07-at-1-02-08-pm.png) 7 | 8 | - Ensure that the `root directory` is set to `apps/web` (it should be the default). 9 | 10 | - Add environment variables. You will find all the variables after successfully completing the setup process at this path: `apps/web/.env` 11 | 12 | ### Set the preview domain in Storyblok CMS 13 | 14 | If the deployment process was successful, copy the page address and add it to Storyblok. 15 | 16 | Copy `URL` from Vercel: 17 | ![Vercel success](https://a.storyblok.com/f/218794/1253x620/479645ff1c/screenshot-2024-02-07-at-1-13-31-pm.png) 18 | 19 | Paste `URL` in Storyblok: 20 | ![Vercel success](https://a.storyblok.com/f/218794/3836x1382/5b93525f3d/screenshot-2024-02-07-at-1-15-37-pm.png) 21 | 22 | 23 | `URL` should end with `/` **slash**. 24 | 25 | _Example:_ 26 | `https://.vercel.app/` 27 | 28 | 29 | 30 | ### Setup revalidate webhook 31 | 32 | A webhook allows for updating pages without the need to rebuild the entire project. 33 | 34 | As the endpoint URL, pass: 35 | 36 | `https:///api/revalidate?token=NEXT_PUBLIC_STORYBLOK_PREVIEW_TOKEN` 37 | 38 | You will find the token under the variable `NEXT_PUBLIC_STORYBLOK_PREVIEW_TOKEN` in the file `apps/web/.env` 39 | 40 | ![Vercel success](https://a.storyblok.com/f/218794/4074x2210/0826083bdc/screenshot-2024-02-07-at-1-21-22-pm.png) 41 | 42 | 43 | Remember to check all the checkboxes in the `Story` accordion 44 | 45 | 46 | 47 | More information about revalidate webhook [here](/packages/storyblok/storyblok-revalidate) 48 | 49 | 50 | 51 | -------------------------------------------------------------------------------- /packages/storyblok-setup/src/stories/setStories.ts: -------------------------------------------------------------------------------- 1 | import { color } from '../utils/color.ts'; 2 | import { createConfig } from './utils/createConfig.ts'; 3 | import { createConfigElements } from './utils/createConfigElements.ts'; 4 | import { createConfigFolder } from './utils/createConfigFolder.ts'; 5 | import { createHomepage } from './utils/createHomepage.ts'; 6 | import { createLayoutsStories } from './utils/createLayoutsStories.ts'; 7 | import { createRootFolder } from './utils/createRootFolder.ts'; 8 | import { createSpecialPagesStories } from './utils/createSpecialPagesStories.ts'; 9 | 10 | export const setStories = async () => { 11 | console.log(color('info', '▶️ Starting workshop on stories setup...')); 12 | 13 | const { rootAppFolderID, NEXT_PUBLIC_STORYBLOK_MAIN_APP_FOLDER } = 14 | (await createRootFolder()) || {}; 15 | 16 | const { rootConfigFolderID, NEXT_PUBLIC_STORYBLOK_EXCLUDED_FOLDERS_FROM_ROUTING } = 17 | (await createConfigFolder({ parentID: rootAppFolderID })) || {}; 18 | 19 | const { layoutFolderID, specialPagesFolderID } = 20 | (await createConfigElements({ rootConfigFolderID })) || {}; 21 | 22 | const { homepageUUID } = await createHomepage({ 23 | parentFolderID: rootAppFolderID, 24 | }); 25 | 26 | const { layoutsUUID } = await createLayoutsStories({ 27 | parentFolderID: layoutFolderID, 28 | homepageUUID, 29 | NEXT_PUBLIC_STORYBLOK_MAIN_APP_FOLDER, 30 | }); 31 | 32 | const { specialPagesUUID } = await createSpecialPagesStories({ 33 | parentFolderID: specialPagesFolderID, 34 | homepageUUID, 35 | NEXT_PUBLIC_STORYBLOK_MAIN_APP_FOLDER, 36 | }); 37 | 38 | await createConfig({ parentFolderID: rootConfigFolderID, layoutsUUID, specialPagesUUID }); 39 | 40 | console.log(color('success', '✅ Stories setup complete.')); 41 | 42 | return { 43 | NEXT_PUBLIC_STORYBLOK_MAIN_APP_FOLDER, 44 | NEXT_PUBLIC_STORYBLOK_EXCLUDED_FOLDERS_FROM_ROUTING, 45 | }; 46 | }; 47 | -------------------------------------------------------------------------------- /apps/web/src/app/[...slug]/page.tsx: -------------------------------------------------------------------------------- 1 | import { Metadata, ResolvingMetadata } from 'next'; 2 | import { draftMode } from 'next/headers'; 3 | import { notFound } from 'next/navigation'; 4 | 5 | import { getStoryblokApi, relations } from '@natu/storyblok-api'; 6 | import { getStoryblokSeoData } from '@natu/storyblok-seo'; 7 | import { 8 | DynamicRender, 9 | getSlugWithAppName, 10 | isSlugExcludedFromRouting, 11 | } from '@natu/storyblok-utils'; 12 | 13 | const getSlugFromParams = (slug?: T) => { 14 | const path = (slug && Array.isArray(slug) && slug.join('/')) || ''; 15 | 16 | return path; 17 | }; 18 | 19 | interface PageProps { 20 | params: { 21 | slug: string[]; 22 | }; 23 | } 24 | 25 | export const generateMetadata = async ( 26 | { params }: PageProps, 27 | parent: ResolvingMetadata, 28 | ): Promise => { 29 | const { isEnabled } = draftMode(); 30 | const { getContentNode } = getStoryblokApi({ draftMode: isEnabled }); 31 | 32 | const slug = getSlugWithAppName({ slug: getSlugFromParams(params.slug) }); 33 | 34 | const prevData = await parent; 35 | const configData = await getContentNode({ 36 | slug, 37 | relations, 38 | }); 39 | 40 | return getStoryblokSeoData(configData.ContentNode?.content.seo, { 41 | slug: `/${getSlugFromParams(params.slug)}`, 42 | prevData, 43 | }); 44 | }; 45 | 46 | const Page = async ({ params }: PageProps) => { 47 | const { isEnabled } = draftMode(); 48 | const { getContentNode } = getStoryblokApi({ draftMode: isEnabled }); 49 | 50 | const slug = getSlugWithAppName({ slug: getSlugFromParams(params.slug) }); 51 | 52 | if (isSlugExcludedFromRouting(slug)) { 53 | return notFound(); 54 | } 55 | 56 | const story = await getContentNode({ slug, relations }); 57 | 58 | if (!story || !story?.ContentNode) { 59 | return notFound(); 60 | } 61 | 62 | return ; 63 | }; 64 | 65 | export default Page; 66 | -------------------------------------------------------------------------------- /packages/tailwind-config/global.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | @layer base { 6 | :root { 7 | --background: 0 0% 100%; 8 | --foreground: 224 71.4% 4.1%; 9 | --card: 0 0% 100%; 10 | --card-foreground: 224 71.4% 4.1%; 11 | --popover: 0 0% 100%; 12 | --popover-foreground: 224 71.4% 4.1%; 13 | --primary: 220.9 39.3% 11%; 14 | --primary-foreground: 210 20% 98%; 15 | --secondary: 220 14.3% 95.9%; 16 | --secondary-foreground: 220.9 39.3% 11%; 17 | --muted: 220 14.3% 95.9%; 18 | --muted-foreground: 220 8.9% 46.1%; 19 | --accent: 220 14.3% 95.9%; 20 | --accent-foreground: 220.9 39.3% 11%; 21 | --destructive: 0 84.2% 60.2%; 22 | --destructive-foreground: 210 20% 98%; 23 | --border: 220 13% 91%; 24 | --input: 220 13% 91%; 25 | --ring: 224 71.4% 4.1%; 26 | --radius: 0.5rem; 27 | } 28 | 29 | .dark { 30 | --background: 224 71.4% 4.1%; 31 | --foreground: 210 20% 98%; 32 | --card: 224 71.4% 4.1%; 33 | --card-foreground: 210 20% 98%; 34 | --popover: 224 71.4% 4.1%; 35 | --popover-foreground: 210 20% 98%; 36 | --primary: 210 20% 98%; 37 | --primary-foreground: 220.9 39.3% 11%; 38 | --secondary: 215 27.9% 16.9%; 39 | --secondary-foreground: 210 20% 98%; 40 | --muted: 215 27.9% 16.9%; 41 | --muted-foreground: 217.9 10.6% 64.9%; 42 | --accent: 215 27.9% 16.9%; 43 | --accent-foreground: 210 20% 98%; 44 | --destructive: 0 62.8% 30.6%; 45 | --destructive-foreground: 210 20% 98%; 46 | --border: 215 27.9% 16.9%; 47 | --input: 215 27.9% 16.9%; 48 | --ring: 216 12.2% 83.9%; 49 | } 50 | 51 | * { 52 | @apply border-border; 53 | } 54 | html, 55 | a, 56 | p, 57 | li, 58 | code { 59 | -webkit-font-smoothing: antialiased; 60 | -moz-osx-font-smoothing: grayscale; 61 | } 62 | body { 63 | @apply bg-background text-foreground font-primary; 64 | font-feature-settings: 65 | 'rlig' 1, 66 | 'calt' 1; 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /packages/storyblok-setup/src/stories/utils/createConfigElements.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable consistent-return */ 2 | import { storyblok } from '../../utils/client.ts'; 3 | import { color } from '../../utils/color.ts'; 4 | import { STORIES_ENDPOINT } from '../../utils/endpoints.ts'; 5 | import { configRootFolders } from '../data/configRootFolders.ts'; 6 | 7 | interface CreateConfigElementsOutput { 8 | layoutFolderID?: number | null; 9 | specialPagesFolderID?: number | null; 10 | } 11 | interface CreateConfigElementsInput { 12 | rootConfigFolderID?: number; 13 | } 14 | 15 | export const createConfigElements = async ({ 16 | rootConfigFolderID, 17 | }: CreateConfigElementsInput): Promise => { 18 | const configColderRequests = []; 19 | 20 | if (!rootConfigFolderID) { 21 | console.log(color('subtitle', 'ℹ️ Missing rootConfigFolderID.')); 22 | 23 | return { 24 | layoutFolderID: null, 25 | specialPagesFolderID: null, 26 | }; 27 | } 28 | 29 | for (const configColder of configRootFolders) { 30 | configColderRequests.push( 31 | storyblok.post(STORIES_ENDPOINT, { 32 | // @ts-ignore 33 | story: { 34 | ...configColder, 35 | parent_id: `${rootConfigFolderID}`, 36 | }, 37 | }), 38 | ); 39 | } 40 | 41 | let layoutFolderID; 42 | let specialPagesFolderID; 43 | 44 | try { 45 | const res = await Promise.all(configColderRequests); 46 | 47 | const folders = res.map(item => ({ 48 | // @ts-ignore 49 | id: item.data.story.id, 50 | // @ts-ignore 51 | layout: item.data.story.slug, 52 | })); 53 | 54 | layoutFolderID = folders.find(folder => folder.layout === 'layout')?.id; 55 | specialPagesFolderID = folders.find(folder => folder.layout === 'special-pages')?.id; 56 | } catch (err) { 57 | console.error(color('danger', `🚨 CreateConfigElements - ${JSON.stringify(err)}`)); 58 | } 59 | 60 | return { 61 | layoutFolderID, 62 | specialPagesFolderID, 63 | }; 64 | }; 65 | --------------------------------------------------------------------------------