@majordigital/create-acorn 1.0.4 → 1.0.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +35 -70
- package/bin/create-acorn.mjs +25 -2
- package/package.json +2 -1
- package/template/next.config.js +48 -0
- package/template/postcss.config.js +6 -0
- package/template/public/favicon/favicon.ico +0 -0
- package/template/src/app/layout.tsx +60 -0
- package/template/src/app/not-found.tsx +7 -0
- package/template/src/app/page.tsx +7 -0
- package/template/src/app/robots.ts +32 -0
- package/template/src/app/sitemap.ts +22 -0
- package/template/src/icons/logo.svg +3 -0
- package/template/src/lib/buildCache.ts +29 -0
- package/template/src/lib/config.ts +7 -0
- package/template/src/lib/constants.ts +0 -0
- package/template/src/lib/fonts.ts +15 -0
- package/template/src/lib/getMetadata.ts +124 -0
- package/template/src/lib/utils.ts +12 -0
- package/template/src/styles/globals.css +23 -0
- package/template/src/types/components.ts +25 -0
- package/template/src/types/custom.d.ts +3 -0
- package/template/src/ui/ConditionalWrapper.tsx +13 -0
- package/template/src/ui/components/Accordion.tsx +73 -0
- package/template/src/ui/components/AnnouncementBar.tsx +36 -0
- package/template/src/ui/components/Breadcrumbs.tsx +60 -0
- package/template/src/ui/components/ButtonGroup.tsx +42 -0
- package/template/src/ui/components/CallToAction.tsx +21 -0
- package/template/src/ui/components/Card.tsx +21 -0
- package/template/src/ui/components/FeaturedContent.tsx +21 -0
- package/template/src/ui/components/FormContact.tsx +190 -0
- package/template/src/ui/components/Nav.tsx +39 -0
- package/template/src/ui/components/NavCollapsed.tsx +91 -0
- package/template/src/ui/components/Pagination.tsx +96 -0
- package/template/src/ui/components/Quote.tsx +21 -0
- package/template/src/ui/elements/Button.tsx +97 -0
- package/template/src/ui/elements/ButtonWrapper.tsx +42 -0
- package/template/src/ui/elements/Chip.tsx +27 -0
- package/template/src/ui/elements/Tooltip.tsx +71 -0
- package/template/src/ui/elements/form/Checkbox.tsx +24 -0
- package/template/src/ui/elements/form/Form.tsx +134 -0
- package/template/src/ui/elements/form/FormLabel.tsx +15 -0
- package/template/src/ui/elements/form/FormMessage.tsx +34 -0
- package/template/src/ui/elements/form/Input.tsx +24 -0
- package/template/src/ui/elements/form/Textarea.tsx +24 -0
- package/template/src/ui/elements/navigation/NavPopover.tsx +84 -0
- package/template/src/ui/elements/navigation/NavPrimaryLink.tsx +27 -0
- package/template/src/ui/elements/navigation/NavSecondaryLink.tsx +28 -0
- package/template/src/ui/elements/typography/Blockquote.tsx +30 -0
- package/template/src/ui/elements/typography/H.tsx +92 -0
- package/template/src/ui/elements/typography/List.tsx +64 -0
- package/template/src/ui/elements/typography/P.tsx +88 -0
- package/template/src/ui/elements/typography/TypoWrapper.tsx +15 -0
- package/template/src/ui/layout/Container.tsx +27 -0
- package/template/src/ui/layout/PageSection.tsx +39 -0
- package/template/src/ui/sections/Footer.tsx +11 -0
- package/template/src/ui/sections/Header.tsx +21 -0
- package/template/tailwind.config.js +33 -0
- package/template/tsconfig.json +69 -0
package/README.md
CHANGED
|
@@ -1,92 +1,57 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @majordigital/create-acorn
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Interactive CLI to scaffold a Next.js 15 project with a headless CMS and Acorn components.
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
- Primsic
|
|
8
|
-
- Storyblok
|
|
9
|
-
|
|
10
|
-
## Running This Project 🚀
|
|
11
|
-
|
|
12
|
-
This website is built using the [NextJS](https://nextjs.org/) framework, utilising [TypeScript](https://www.typescriptlang.org/) to strongly check types/props and provide helpful intelisense, and the utility-first CSS framework [Tailwind](https://tailwindcss.com/) as the styling library.
|
|
13
|
-
|
|
14
|
-
To run this project, following the following instructions:
|
|
15
|
-
|
|
16
|
-
1. **Install dependencies**
|
|
17
|
-
Navigate to the root directory.
|
|
18
|
-
|
|
19
|
-
```bash
|
|
20
|
-
npm i
|
|
21
|
-
```
|
|
22
|
-
|
|
23
|
-
2. **Run the website**
|
|
24
|
-
In the root directory.
|
|
25
|
-
|
|
26
|
-
```bash
|
|
27
|
-
npm run dev
|
|
28
|
-
```
|
|
29
|
-
|
|
30
|
-
## Create Acorn CLI 🧰
|
|
31
|
-
|
|
32
|
-
First iteration of an interactive CLI to scaffold a Headless CMS skeleton with Next.js 15 and Acorn components.
|
|
33
|
-
|
|
34
|
-
- Default framework: Next.js 15
|
|
35
|
-
- UI base: Acorn components (this repo)
|
|
36
|
-
- CMS options: Prismic, Storyblok, Dato
|
|
37
|
-
|
|
38
|
-
Recommended usage (once published to npm):
|
|
5
|
+
## Quick Start
|
|
39
6
|
|
|
40
7
|
```bash
|
|
41
8
|
npx @majordigital/create-acorn@latest
|
|
42
9
|
```
|
|
43
10
|
|
|
44
|
-
|
|
11
|
+
The CLI will walk you through selecting a CMS (Prismic, Storyblok, or Dato) and scaffold a Next.js 15 project with TypeScript, Tailwind, and Biome.
|
|
45
12
|
|
|
46
|
-
|
|
47
|
-
npm run create
|
|
48
|
-
# or
|
|
49
|
-
npm run acorn
|
|
50
|
-
```
|
|
51
|
-
|
|
52
|
-
### Prismic setup
|
|
13
|
+
## What You Get
|
|
53
14
|
|
|
54
|
-
|
|
15
|
+
The CLI scaffolds a complete project with:
|
|
55
16
|
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
17
|
+
- **Next.js 15** with App Router, TypeScript, and Tailwind
|
|
18
|
+
- **Acorn component library** — UI elements, layout components, sections, forms, typography, and navigation
|
|
19
|
+
- **Biome** for linting and formatting
|
|
20
|
+
- **Your chosen CMS** pre-configured
|
|
59
21
|
|
|
60
|
-
|
|
22
|
+
### Included Acorn Boilerplate
|
|
61
23
|
|
|
62
|
-
```
|
|
63
|
-
|
|
64
|
-
#
|
|
65
|
-
|
|
24
|
+
```
|
|
25
|
+
src/
|
|
26
|
+
├── app/ # Next.js app routes (layout, page, not-found, robots, sitemap)
|
|
27
|
+
├── icons/ # SVG icons
|
|
28
|
+
├── lib/ # Utilities, fonts, config, metadata helpers
|
|
29
|
+
├── styles/ # Global CSS
|
|
30
|
+
├── types/ # TypeScript type definitions
|
|
31
|
+
└── ui/
|
|
32
|
+
├── components/ # Nav, Card, Accordion, CTA, Quote, Form, etc.
|
|
33
|
+
├── elements/ # Button, Chip, Tooltip, typography, form inputs
|
|
34
|
+
├── layout/ # Container, PageSection
|
|
35
|
+
└── sections/ # Header, Footer
|
|
66
36
|
```
|
|
67
37
|
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
Or pass a non-interactive flag:
|
|
38
|
+
## Non-Interactive Usage
|
|
71
39
|
|
|
72
40
|
```bash
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
41
|
+
npx @majordigital/create-acorn@latest --cms prismic --repo my-repo
|
|
42
|
+
npx @majordigital/create-acorn@latest --cms storyblok
|
|
43
|
+
npx @majordigital/create-acorn@latest --cms dato
|
|
76
44
|
```
|
|
77
45
|
|
|
78
|
-
|
|
79
|
-
- This first iteration collects your CMS choice only and prints the selection. In the next iteration, the CLI will scaffold the appropriate folder structure and config for your chosen CMS.
|
|
80
|
-
- To expose this CLI as global commands (`create-acorn` and `major-acorn`) via `npm create`/`npx`, remove `"private": true` and publish to npm.
|
|
81
|
-
- While it’s technically possible to trigger prompts on `npm install major-acorn` via a `postinstall` script, it’s discouraged as it surprises installs and breaks CI. The recommended flow is `npm create acorn` or `npx major-acorn` to start a new project.
|
|
46
|
+
## Prismic Setup
|
|
82
47
|
|
|
83
|
-
|
|
48
|
+
If you select Prismic, the CLI runs Slice Machine init and connects to your new or existing repository.
|
|
84
49
|
|
|
85
|
-
|
|
50
|
+
Repository name rules: lowercase letters, numbers, and hyphens only.
|
|
86
51
|
|
|
87
|
-
|
|
52
|
+
After setup:
|
|
88
53
|
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
54
|
+
1. Start Slice Machine: `npm run slicemachine`
|
|
55
|
+
2. Create your custom types at http://localhost:9999
|
|
56
|
+
3. Push your custom types to Prismic
|
|
57
|
+
4. Start your dev server: `npm run dev`
|
package/bin/create-acorn.mjs
CHANGED
|
@@ -3,8 +3,9 @@
|
|
|
3
3
|
import readline from 'node:readline';
|
|
4
4
|
import { argv, exit } from 'node:process';
|
|
5
5
|
import { spawn } from 'node:child_process';
|
|
6
|
-
import { basename, join } from 'node:path';
|
|
7
|
-
import { readFileSync, writeFileSync } from 'node:fs';
|
|
6
|
+
import { basename, join, dirname } from 'node:path';
|
|
7
|
+
import { readFileSync, writeFileSync, cpSync } from 'node:fs';
|
|
8
|
+
import { fileURLToPath } from 'node:url';
|
|
8
9
|
|
|
9
10
|
const CMS_CHOICES = [
|
|
10
11
|
{ key: 'prismic', label: 'Prismic' },
|
|
@@ -96,6 +97,28 @@ async function scaffoldNextApp() {
|
|
|
96
97
|
console.log('Next.js project scaffolded successfully.');
|
|
97
98
|
console.log('');
|
|
98
99
|
|
|
100
|
+
// Copy Acorn template files (src/, public/, config files) over the Next.js scaffold
|
|
101
|
+
console.log('Copying Acorn boilerplate...');
|
|
102
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
103
|
+
const templateDir = join(__dirname, '..', 'template');
|
|
104
|
+
cpSync(templateDir, process.cwd(), { recursive: true, force: true });
|
|
105
|
+
console.log('Acorn components and config copied successfully.');
|
|
106
|
+
console.log('');
|
|
107
|
+
|
|
108
|
+
// Install Acorn dependencies
|
|
109
|
+
console.log('Installing Acorn dependencies...');
|
|
110
|
+
await runCommand('npm', ['install',
|
|
111
|
+
'@headlessui/react', '@headlessui/tailwindcss', '@heroicons/react',
|
|
112
|
+
'@hookform/error-message', '@hookform/resolvers',
|
|
113
|
+
'@next/bundle-analyzer', 'clsx', 'next-seo',
|
|
114
|
+
'react-accessible-accordion', 'react-hook-form', 'zod'
|
|
115
|
+
]);
|
|
116
|
+
await runCommand('npm', ['install', '--save-dev',
|
|
117
|
+
'@svgr/webpack', '@types/lodash.get'
|
|
118
|
+
]);
|
|
119
|
+
console.log('Acorn dependencies installed.');
|
|
120
|
+
console.log('');
|
|
121
|
+
|
|
99
122
|
console.log('Installing Biome for linting...');
|
|
100
123
|
await runCommand('npm', ['install', '--save-dev', '--save-exact', '@biomejs/biome']);
|
|
101
124
|
await runCommand('npx', ['@biomejs/biome', 'init']);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@majordigital/create-acorn",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.5",
|
|
4
4
|
"description": "Interactive scaffold for Acorn with Storyblok/Prismic/DatoCMS, TypeScript, and Tailwind.",
|
|
5
5
|
"bin": {
|
|
6
6
|
"create-acorn": "bin/create-acorn.mjs",
|
|
@@ -19,6 +19,7 @@
|
|
|
19
19
|
},
|
|
20
20
|
"files": [
|
|
21
21
|
"bin",
|
|
22
|
+
"template",
|
|
22
23
|
"README.md"
|
|
23
24
|
],
|
|
24
25
|
"engines": {
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/** @type {import('next').NextConfig} */
|
|
2
|
+
|
|
3
|
+
const baseConfig = {
|
|
4
|
+
poweredByHeader: false,
|
|
5
|
+
reactStrictMode: true,
|
|
6
|
+
trailingSlash: false,
|
|
7
|
+
|
|
8
|
+
turbopack: {
|
|
9
|
+
rules: {
|
|
10
|
+
'*.svg': {
|
|
11
|
+
loaders: ['@svgr/webpack'],
|
|
12
|
+
as: '*.js',
|
|
13
|
+
},
|
|
14
|
+
},
|
|
15
|
+
},
|
|
16
|
+
|
|
17
|
+
webpack(config) {
|
|
18
|
+
config.module.rules.push({
|
|
19
|
+
test: /\.svg$/,
|
|
20
|
+
use: ['@svgr/webpack'],
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
return config;
|
|
24
|
+
},
|
|
25
|
+
|
|
26
|
+
images: {
|
|
27
|
+
remotePatterns: [
|
|
28
|
+
{
|
|
29
|
+
protocol: 'https',
|
|
30
|
+
hostname: 'images.prismic.io',
|
|
31
|
+
port: '',
|
|
32
|
+
pathname: '/**',
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
protocol: 'https',
|
|
36
|
+
hostname: 'io.prismic.preview',
|
|
37
|
+
port: '',
|
|
38
|
+
pathname: '/**',
|
|
39
|
+
},
|
|
40
|
+
],
|
|
41
|
+
},
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
const withBundleAnalyzer = require('@next/bundle-analyzer')({
|
|
45
|
+
enabled: process.env.ANALYZE === 'true',
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
module.exports = withBundleAnalyzer(baseConfig);
|
|
Binary file
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import '@/styles/globals.css';
|
|
2
|
+
|
|
3
|
+
import clsx from 'clsx';
|
|
4
|
+
import type { Metadata } from 'next';
|
|
5
|
+
import type { PropsWithChildren } from 'react';
|
|
6
|
+
|
|
7
|
+
import { siteConfig } from '@/lib/config';
|
|
8
|
+
import { fonts } from '@/lib/fonts';
|
|
9
|
+
import Footer from '@/ui/sections/Footer';
|
|
10
|
+
import Header from '@/ui/sections/Header';
|
|
11
|
+
|
|
12
|
+
export const metadata: Metadata = {
|
|
13
|
+
metadataBase: new URL(siteConfig.url),
|
|
14
|
+
title: {
|
|
15
|
+
default: siteConfig.title,
|
|
16
|
+
template: `%s | ${siteConfig.title}`,
|
|
17
|
+
},
|
|
18
|
+
description: siteConfig.description,
|
|
19
|
+
robots: { index: true, follow: true },
|
|
20
|
+
icons: {
|
|
21
|
+
icon: '/favicon/favicon.ico',
|
|
22
|
+
shortcut: '/favicon/favicon-16x16.png',
|
|
23
|
+
apple: '/favicon/apple-touch-icon.png',
|
|
24
|
+
},
|
|
25
|
+
openGraph: {
|
|
26
|
+
url: siteConfig.url,
|
|
27
|
+
title: siteConfig.title,
|
|
28
|
+
description: siteConfig.description,
|
|
29
|
+
siteName: siteConfig.title,
|
|
30
|
+
images: '/opengraph-image.png',
|
|
31
|
+
type: 'website',
|
|
32
|
+
locale: 'en_GB',
|
|
33
|
+
},
|
|
34
|
+
twitter: {
|
|
35
|
+
card: 'summary_large_image',
|
|
36
|
+
title: siteConfig.title,
|
|
37
|
+
description: siteConfig.description,
|
|
38
|
+
images: '/opengraph-image.png',
|
|
39
|
+
},
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
export default function RootLayout({ children }: PropsWithChildren) {
|
|
43
|
+
return (
|
|
44
|
+
<html lang="en" className="scroll-smooth break-words">
|
|
45
|
+
<body className={clsx(`min-h-screen antialiased`, fonts)}>
|
|
46
|
+
<div id="skiptocontent">
|
|
47
|
+
<a href="#main" className="sr-only focus:not-sr-only">
|
|
48
|
+
skip to main content
|
|
49
|
+
</a>
|
|
50
|
+
</div>
|
|
51
|
+
|
|
52
|
+
<div id="site-container">
|
|
53
|
+
<Header />
|
|
54
|
+
<main id="main">{children}</main>
|
|
55
|
+
<Footer />
|
|
56
|
+
</div>
|
|
57
|
+
</body>
|
|
58
|
+
</html>
|
|
59
|
+
);
|
|
60
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import type { MetadataRoute } from 'next';
|
|
2
|
+
|
|
3
|
+
import { siteConfig } from '@/lib/config';
|
|
4
|
+
|
|
5
|
+
export default function robots(): MetadataRoute.Robots {
|
|
6
|
+
const isProduction = process.env.NODE_ENV === 'production';
|
|
7
|
+
|
|
8
|
+
if (!isProduction) {
|
|
9
|
+
// Block everything in development/staging
|
|
10
|
+
return {
|
|
11
|
+
rules: [
|
|
12
|
+
{
|
|
13
|
+
userAgent: '*',
|
|
14
|
+
disallow: '/',
|
|
15
|
+
},
|
|
16
|
+
],
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
// Production rules
|
|
21
|
+
return {
|
|
22
|
+
rules: [
|
|
23
|
+
{
|
|
24
|
+
userAgent: '*',
|
|
25
|
+
allow: '/',
|
|
26
|
+
disallow: ['/api/', '/search', '/*?*utm_*', '/*?*ref=*'],
|
|
27
|
+
},
|
|
28
|
+
],
|
|
29
|
+
sitemap: `${siteConfig.url}/sitemap.xml`,
|
|
30
|
+
host: siteConfig.url,
|
|
31
|
+
};
|
|
32
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { MetadataRoute } from 'next';
|
|
2
|
+
|
|
3
|
+
import { siteConfig } from '@/lib/config';
|
|
4
|
+
|
|
5
|
+
export default function sitemap(): MetadataRoute.Sitemap {
|
|
6
|
+
const isProduction = process.env.NODE_ENV === 'production';
|
|
7
|
+
|
|
8
|
+
if (!isProduction) {
|
|
9
|
+
// Prevent search engines from indexing anything
|
|
10
|
+
return [];
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
return [
|
|
14
|
+
{
|
|
15
|
+
url: `${siteConfig.url}`,
|
|
16
|
+
lastModified: new Date(),
|
|
17
|
+
changeFrequency: 'daily',
|
|
18
|
+
priority: 0.7,
|
|
19
|
+
},
|
|
20
|
+
// Add more URLs here
|
|
21
|
+
];
|
|
22
|
+
}
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 244 56.742">
|
|
2
|
+
<path d="M48.4.831h4.968v54.79H39.034V31.207L27.395 43.414h-1.561l-11.5-12.207v24.414H0V.831h5.11l21.433 23.7ZM94.393 48.663H72.817l-3.265 6.955H55.926v-1.984L80.482.263h6.1l24.7 53.371.994 1.987H97.515Zm-10.93-26.257-6.1 14.336H89.85ZM138.253 28.368c0-37.757 56.067-37.757 56.067 0 0 37.898-56.067 37.898-56.067 0Zm41.873 0c0-19.73-27.4-19.73-27.4 0-.137 19.732 27.4 19.732 27.4 0ZM244 53.492v1.987h-15.613l-9.368-16.04h-7.24v16.04h-13.91V.831h23.988c21.433.142 25.408 25.976 11.5 35.344Zm-22.285-40.028c-3.265-.142-6.671 0-9.936 0V27.09h9.936c7.665 0 7.954-13.626 0-13.626ZM120.651.973h14.194V33.62c0 8.517-2.555 14.762-7.523 18.595a22.3 22.3 0 0 1-10.5 3.974l-5.539-11.64a12.875 12.875 0 0 0 5.394-1.561c2.271-1.7 3.974-5.394 3.974-9.51Z"/>
|
|
3
|
+
</svg>
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
|
|
4
|
+
// File to store the build ID
|
|
5
|
+
const BUILD_ID_FILE = path.join(process.cwd(), '.build-id');
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Returns the build cache ID generated at the start of the build
|
|
9
|
+
* Falls back to current timestamp if no build ID file exists
|
|
10
|
+
*/
|
|
11
|
+
export function getBuildCacheId(): number {
|
|
12
|
+
try {
|
|
13
|
+
if (fs.existsSync(BUILD_ID_FILE)) {
|
|
14
|
+
const storedId = fs.readFileSync(BUILD_ID_FILE, 'utf8').trim();
|
|
15
|
+
const buildId = parseInt(storedId, 10);
|
|
16
|
+
|
|
17
|
+
if (Number.isNaN(buildId)) {
|
|
18
|
+
throw new Error('Stored build ID is not a valid number');
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
return buildId;
|
|
22
|
+
}
|
|
23
|
+
} catch (error) {
|
|
24
|
+
console.warn('Error reading build ID:', error);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// Fallback to current timestamp if file doesn't exist or has invalid content
|
|
28
|
+
return Date.now();
|
|
29
|
+
}
|
|
File without changes
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { Inter, JetBrains_Mono } from 'next/font/google';
|
|
2
|
+
|
|
3
|
+
const fontHeading = JetBrains_Mono({
|
|
4
|
+
subsets: ['latin'],
|
|
5
|
+
variable: '--font-heading',
|
|
6
|
+
fallback: ['system-ui', 'arial'],
|
|
7
|
+
});
|
|
8
|
+
|
|
9
|
+
const fontSans = Inter({
|
|
10
|
+
subsets: ['latin'],
|
|
11
|
+
variable: '--font-sans',
|
|
12
|
+
fallback: ['system-ui', 'arial'],
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
export const fonts = [fontHeading.variable, fontSans.variable];
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import type { Metadata } from 'next';
|
|
2
|
+
|
|
3
|
+
import { siteConfig } from '@/lib/config';
|
|
4
|
+
|
|
5
|
+
interface SeoComponent {
|
|
6
|
+
title: string;
|
|
7
|
+
description: string;
|
|
8
|
+
og_title: string;
|
|
9
|
+
og_description: string;
|
|
10
|
+
og_image: string;
|
|
11
|
+
twitter_title: string;
|
|
12
|
+
twitter_description: string;
|
|
13
|
+
twitter_image: string;
|
|
14
|
+
no_index?: boolean;
|
|
15
|
+
no_follow?: boolean;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
interface SeoFallback {
|
|
19
|
+
title?: string;
|
|
20
|
+
description?: string;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
interface Config {
|
|
24
|
+
slug?: string;
|
|
25
|
+
twitterCreator?: string;
|
|
26
|
+
googleVerificationId?: string;
|
|
27
|
+
siteName?: string;
|
|
28
|
+
fallback?: SeoFallback;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export const getMetaData = (
|
|
32
|
+
data?: SeoComponent | null,
|
|
33
|
+
config?: Config
|
|
34
|
+
): Metadata => {
|
|
35
|
+
if (!config && !data) {
|
|
36
|
+
return {};
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const { googleVerificationId, slug, twitterCreator, siteName, fallback } =
|
|
40
|
+
config || {};
|
|
41
|
+
|
|
42
|
+
const fallbackTitle = fallback?.title || '';
|
|
43
|
+
const fallbackDescription = fallback?.description || '';
|
|
44
|
+
|
|
45
|
+
const fallbackMetadata = {
|
|
46
|
+
metadataBase: new URL(
|
|
47
|
+
process.env.NEXT_PUBLIC_APP_URL || siteConfig.url
|
|
48
|
+
),
|
|
49
|
+
title: fallbackTitle,
|
|
50
|
+
description: fallbackDescription,
|
|
51
|
+
alternates: {
|
|
52
|
+
canonical: slug,
|
|
53
|
+
},
|
|
54
|
+
openGraph: {
|
|
55
|
+
title: fallbackTitle,
|
|
56
|
+
description: fallbackDescription,
|
|
57
|
+
url: slug,
|
|
58
|
+
siteName,
|
|
59
|
+
type: 'website',
|
|
60
|
+
},
|
|
61
|
+
twitter: {
|
|
62
|
+
card: 'summary_large_image',
|
|
63
|
+
title: fallbackTitle,
|
|
64
|
+
description: fallbackDescription,
|
|
65
|
+
site: twitterCreator,
|
|
66
|
+
creator: twitterCreator,
|
|
67
|
+
},
|
|
68
|
+
...(googleVerificationId && {
|
|
69
|
+
verification: {
|
|
70
|
+
google: `google-site-verification=${googleVerificationId}`,
|
|
71
|
+
},
|
|
72
|
+
}),
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
if (!data) {
|
|
76
|
+
return fallbackMetadata;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const {
|
|
80
|
+
title,
|
|
81
|
+
description,
|
|
82
|
+
og_title,
|
|
83
|
+
og_description,
|
|
84
|
+
og_image,
|
|
85
|
+
twitter_title,
|
|
86
|
+
twitter_description,
|
|
87
|
+
twitter_image,
|
|
88
|
+
} = data;
|
|
89
|
+
|
|
90
|
+
const customMetaData = {
|
|
91
|
+
title: title || fallbackTitle,
|
|
92
|
+
description: description || fallbackDescription,
|
|
93
|
+
openGraph: {
|
|
94
|
+
title: og_title || title || fallbackTitle,
|
|
95
|
+
description: og_description || description || fallbackDescription,
|
|
96
|
+
url: slug,
|
|
97
|
+
siteName,
|
|
98
|
+
...(og_image && {
|
|
99
|
+
images: {
|
|
100
|
+
url: og_image,
|
|
101
|
+
},
|
|
102
|
+
}),
|
|
103
|
+
type: 'website',
|
|
104
|
+
},
|
|
105
|
+
twitter: {
|
|
106
|
+
card: 'summary_large_image',
|
|
107
|
+
title: twitter_title || og_title || title || fallbackTitle,
|
|
108
|
+
description:
|
|
109
|
+
twitter_description ||
|
|
110
|
+
og_description ||
|
|
111
|
+
description ||
|
|
112
|
+
fallbackDescription,
|
|
113
|
+
site: twitterCreator,
|
|
114
|
+
creator: twitterCreator,
|
|
115
|
+
...(twitter_image && {
|
|
116
|
+
images: {
|
|
117
|
+
url: twitter_image || og_image,
|
|
118
|
+
},
|
|
119
|
+
}),
|
|
120
|
+
},
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
return { ...fallbackMetadata, ...customMetaData };
|
|
124
|
+
};
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export const generateIDFromString = (string: string): string => {
|
|
2
|
+
return string
|
|
3
|
+
.toLowerCase()
|
|
4
|
+
.replace(/<[^>]+>/g, '')
|
|
5
|
+
.replace(/[^a-zA-Z ]/g, '')
|
|
6
|
+
.split(' ')
|
|
7
|
+
.join('-');
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
export const getCurrentTimestamp = (): number => {
|
|
11
|
+
return Date.now();
|
|
12
|
+
};
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
@tailwind base;
|
|
2
|
+
@tailwind components;
|
|
3
|
+
@tailwind utilities;
|
|
4
|
+
|
|
5
|
+
@layer base {
|
|
6
|
+
.no-js {
|
|
7
|
+
@apply block;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
.no-js .nojs-hidden {
|
|
11
|
+
@apply hidden;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
.prose :where(a):not(:where([class~='not-prose'] *)) {
|
|
15
|
+
@apply underline;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
@layer components {
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
@layer utilities {
|
|
23
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
DeepMap,
|
|
3
|
+
FieldError,
|
|
4
|
+
FieldValues,
|
|
5
|
+
Path,
|
|
6
|
+
RegisterOptions,
|
|
7
|
+
UseFormRegister,
|
|
8
|
+
} from 'react-hook-form';
|
|
9
|
+
|
|
10
|
+
export type ComponentThemes = 'default';
|
|
11
|
+
|
|
12
|
+
export type InputProps = {
|
|
13
|
+
id?: string;
|
|
14
|
+
label: string;
|
|
15
|
+
name: string;
|
|
16
|
+
className?: string;
|
|
17
|
+
type?: 'text' | 'email';
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
export type FormInputProps<TFormValues extends FieldValues = FieldValues> = {
|
|
21
|
+
name: Path<TFormValues>;
|
|
22
|
+
rules?: RegisterOptions;
|
|
23
|
+
register?: UseFormRegister<TFormValues>;
|
|
24
|
+
errors?: Partial<DeepMap<TFormValues, FieldError>>;
|
|
25
|
+
} & Omit<InputProps, 'name'>;
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { ReactNode } from 'react';
|
|
2
|
+
|
|
3
|
+
const ConditionalWrapper = ({
|
|
4
|
+
condition,
|
|
5
|
+
wrapper,
|
|
6
|
+
children,
|
|
7
|
+
}: {
|
|
8
|
+
condition: boolean;
|
|
9
|
+
wrapper: (children: ReactNode) => ReactNode;
|
|
10
|
+
children: ReactNode;
|
|
11
|
+
}) => (condition ? wrapper(children) : children);
|
|
12
|
+
|
|
13
|
+
export default ConditionalWrapper;
|