bsmnt 0.2.0 → 0.2.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 +1 -1
- package/package.json +22 -5
- package/src/templates/next-default/.biome/plugins/no-anchor-element.grit +12 -0
- package/src/templates/next-default/.biome/plugins/no-relative-parent-imports.grit +10 -0
- package/src/templates/next-default/.biome/plugins/no-unnecessary-forwardref.grit +9 -0
- package/src/templates/next-default/.github/PULL_REQUEST_TEMPLATE.md +14 -0
- package/src/templates/next-default/.vscode/extensions.json +20 -0
- package/src/templates/next-default/.vscode/settings.json +105 -0
- package/src/templates/next-default/app/favicon.ico +0 -0
- package/src/templates/next-default/app/layout.tsx +104 -0
- package/src/templates/next-default/app/page.tsx +11 -0
- package/src/templates/next-default/app/robots.ts +15 -0
- package/src/templates/next-default/app/sitemap.ts +16 -0
- package/src/templates/next-default/biome.json +2 -2
- package/src/templates/next-default/components/layout/footer/index.tsx +31 -0
- package/src/templates/next-default/components/layout/header/index.tsx +9 -0
- package/src/templates/next-default/components/layout/theme/index.tsx +66 -0
- package/src/templates/next-default/components/layout/wrapper/index.tsx +63 -0
- package/src/templates/next-default/components/ui/README.md +77 -0
- package/src/templates/next-default/components/ui/image/README.md +37 -0
- package/src/templates/next-default/components/ui/image/index.tsx +219 -0
- package/src/templates/next-default/components/ui/link/index.tsx +152 -0
- package/src/templates/next-default/lib/utils/metadata.ts +26 -26
- package/src/templates/next-default/package.json +4 -4
- package/src/templates/next-default/public/fonts/geist/Geist-Mono.woff2 +0 -0
- package/src/templates/next-experiments/app/layout.tsx +18 -18
- package/src/templates/next-experiments/app/robots.ts +3 -3
- package/src/templates/next-experiments/app/sitemap.ts +4 -4
- package/src/templates/next-experiments/biome.json +2 -2
- package/src/templates/next-experiments/components/layout/theme/index.tsx +1 -1
- package/src/templates/next-experiments/components/layout/wrapper/index.tsx +7 -9
- package/src/templates/next-experiments/components/ui/image/index.tsx +39 -46
- package/src/templates/next-experiments/components/ui/link/index.tsx +6 -0
- package/src/templates/next-experiments/lib/utils/metadata.ts +26 -26
- package/src/templates/next-experiments/package.json +4 -4
- package/src/templates/next-webgl/app/layout.tsx +18 -18
- package/src/templates/next-webgl/app/robots.ts +3 -3
- package/src/templates/next-webgl/app/sitemap.ts +4 -4
- package/src/templates/next-webgl/biome.json +2 -2
- package/src/templates/next-webgl/components/layout/theme/index.tsx +1 -1
- package/src/templates/next-webgl/components/layout/wrapper/index.tsx +0 -2
- package/src/templates/next-webgl/components/ui/image/index.tsx +6 -11
- package/src/templates/next-webgl/components/ui/link/index.tsx +9 -2
- package/src/templates/next-webgl/components/webgl/components/scene/index.tsx +1 -0
- package/src/templates/next-webgl/lib/utils/metadata.ts +26 -26
- package/src/templates/next-webgl/package.json +4 -4
- package/src/templates/next-experiments/.cursor/rules/README.md +0 -184
- package/src/templates/next-experiments/.cursor/rules/architecture.mdc +0 -437
- package/src/templates/next-experiments/.cursor/rules/components.mdc +0 -436
- package/src/templates/next-experiments/.cursor/rules/integrations.mdc +0 -447
- package/src/templates/next-experiments/.cursor/rules/main.mdc +0 -278
- package/src/templates/next-experiments/.cursor/rules/styling.mdc +0 -433
- package/src/templates/next-experiments/.github/workflows/lighthouse-to-slack.yml +0 -136
- package/src/templates/next-webgl/.cursor/rules/README.md +0 -184
- package/src/templates/next-webgl/.cursor/rules/architecture.mdc +0 -437
- package/src/templates/next-webgl/.cursor/rules/components.mdc +0 -436
- package/src/templates/next-webgl/.cursor/rules/integrations.mdc +0 -447
- package/src/templates/next-webgl/.cursor/rules/main.mdc +0 -278
- package/src/templates/next-webgl/.cursor/rules/styling.mdc +0 -433
- package/src/templates/next-webgl/.github/workflows/lighthouse-to-slack.yml +0 -136
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# Image Component
|
|
2
|
+
|
|
3
|
+
Optimized images with smart loading, blur placeholders, and responsive sizing.
|
|
4
|
+
|
|
5
|
+
## Usage
|
|
6
|
+
|
|
7
|
+
```tsx
|
|
8
|
+
import { Image } from '@/components/ui/image'
|
|
9
|
+
|
|
10
|
+
// Basic
|
|
11
|
+
<Image src="/hero.jpg" alt="Hero" aspectRatio={16/9} />
|
|
12
|
+
|
|
13
|
+
// Priority (LCP images)
|
|
14
|
+
<Image src="/hero.jpg" alt="Hero" aspectRatio={16/9} priority />
|
|
15
|
+
|
|
16
|
+
// Responsive
|
|
17
|
+
<Image
|
|
18
|
+
src="/product.jpg"
|
|
19
|
+
alt="Product"
|
|
20
|
+
mobileSize="100vw"
|
|
21
|
+
desktopSize="33vw"
|
|
22
|
+
/>
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
## Props
|
|
26
|
+
|
|
27
|
+
| Prop | Description |
|
|
28
|
+
|------|-------------|
|
|
29
|
+
| `aspectRatio` | Prevents layout shift, enables blur placeholder |
|
|
30
|
+
| `priority` | Eager loading for above-the-fold images |
|
|
31
|
+
| `mobileSize` / `desktopSize` | Responsive sizing |
|
|
32
|
+
|
|
33
|
+
## Best Practices
|
|
34
|
+
|
|
35
|
+
- Always provide `aspectRatio` (prevents CLS)
|
|
36
|
+
- Use `priority` for LCP images
|
|
37
|
+
- Never use `next/image` directly
|
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Enhanced Image Component
|
|
3
|
+
*
|
|
4
|
+
* Next.js Image wrapper with optimized defaults and error handling.
|
|
5
|
+
* Always use this component instead of next/image directly.
|
|
6
|
+
*/
|
|
7
|
+
"use client"
|
|
8
|
+
|
|
9
|
+
import cn from "clsx"
|
|
10
|
+
import NextImage, { type ImageProps as NextImageProps } from "next/image"
|
|
11
|
+
import type { CSSProperties, Ref } from "react"
|
|
12
|
+
import { breakpoints } from "@/lib/styles/config"
|
|
13
|
+
import s from "./image.module.css"
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Enhanced Image component props extending Next.js Image.
|
|
17
|
+
*
|
|
18
|
+
* Adds responsive sizing, aspect ratio support, and automatic blur placeholders.
|
|
19
|
+
* Always use this component instead of next/image directly.
|
|
20
|
+
*/
|
|
21
|
+
export type ImageProps = Omit<NextImageProps, "objectFit" | "alt"> & {
|
|
22
|
+
/** CSS object-fit property for image positioning */
|
|
23
|
+
objectFit?: CSSProperties["objectFit"]
|
|
24
|
+
/** Display as block element (adds display: block) */
|
|
25
|
+
block?: boolean
|
|
26
|
+
/** Size on mobile devices (e.g., "100vw", "50vw") */
|
|
27
|
+
mobileSize?: `${number}vw`
|
|
28
|
+
/** Size on desktop devices (e.g., "33vw", "25vw") */
|
|
29
|
+
desktopSize?: `${number}vw`
|
|
30
|
+
/** Ref for accessing the underlying img element */
|
|
31
|
+
ref?: Ref<HTMLImageElement>
|
|
32
|
+
/** Alt text for accessibility (required for meaningful images) */
|
|
33
|
+
alt?: string
|
|
34
|
+
/** Aspect ratio for automatic placeholder and layout stability */
|
|
35
|
+
aspectRatio?: number
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// Memoize helper functions to avoid recreation
|
|
39
|
+
const toBase64 = (str: string) =>
|
|
40
|
+
typeof window === "undefined"
|
|
41
|
+
? Buffer.from(str).toString("base64")
|
|
42
|
+
: window.btoa(str)
|
|
43
|
+
|
|
44
|
+
// Helper to generate blur placeholder with transparent background by default
|
|
45
|
+
const generateShimmer = (w: number, h: number) => `
|
|
46
|
+
<svg width="${w}" height="${h}" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
|
47
|
+
<defs>
|
|
48
|
+
<linearGradient id="g">
|
|
49
|
+
<stop stop-color="rgba(255,255,255,0.1)" offset="20%" />
|
|
50
|
+
<stop stop-color="rgba(255,255,255,0.2)" offset="50%" />
|
|
51
|
+
<stop stop-color="rgba(255,255,255,0.1)" offset="70%" />
|
|
52
|
+
</linearGradient>
|
|
53
|
+
</defs>
|
|
54
|
+
<rect width="${w}" height="${h}" fill="rgba(0,0,0,0)" />
|
|
55
|
+
<rect id="r" width="${w}" height="${h}" fill="url(#g)" />
|
|
56
|
+
<animate xlink:href="#r" attributeName="x" from="-${w}" to="${w}" dur="1s" repeatCount="indefinite" />
|
|
57
|
+
</svg>`
|
|
58
|
+
|
|
59
|
+
// Helper to determine if blur placeholder should be used
|
|
60
|
+
const shouldUseBlurPlaceholder = (
|
|
61
|
+
src: NextImageProps["src"],
|
|
62
|
+
placeholder: string,
|
|
63
|
+
blurDataURL: string | undefined
|
|
64
|
+
): boolean => {
|
|
65
|
+
if (!src) return false
|
|
66
|
+
const isSvg = typeof src === "string" && src.includes(".svg")
|
|
67
|
+
return !isSvg && placeholder === "blur" && !blurDataURL
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// Helper to generate blur data URL
|
|
71
|
+
const generateBlurDataURL = (
|
|
72
|
+
shouldUse: boolean,
|
|
73
|
+
aspectRatio: number | undefined,
|
|
74
|
+
existingBlurDataURL: string | undefined
|
|
75
|
+
): string | undefined => {
|
|
76
|
+
if (!(shouldUse && aspectRatio)) return existingBlurDataURL
|
|
77
|
+
|
|
78
|
+
const shimmerSvg = generateShimmer(700, Math.round(700 / aspectRatio))
|
|
79
|
+
return `data:image/svg+xml;base64,${toBase64(shimmerSvg)}`
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// Helper to determine final placeholder value
|
|
83
|
+
const getFinalPlaceholder = (
|
|
84
|
+
shouldUse: boolean,
|
|
85
|
+
aspectRatio: number | undefined,
|
|
86
|
+
blurDataURL: string | undefined,
|
|
87
|
+
originalPlaceholder: NextImageProps["placeholder"]
|
|
88
|
+
): NextImageProps["placeholder"] => {
|
|
89
|
+
if (!shouldUse) {
|
|
90
|
+
return originalPlaceholder === "blur" && !blurDataURL
|
|
91
|
+
? "empty"
|
|
92
|
+
: originalPlaceholder
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
return aspectRatio || blurDataURL ? "blur" : "empty"
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Enhanced Image component with responsive sizing and automatic optimizations.
|
|
100
|
+
*
|
|
101
|
+
* Always use this component instead of next/image directly. Provides:
|
|
102
|
+
* - Automatic responsive sizes generation
|
|
103
|
+
* - Smart blur placeholders with aspect ratio support
|
|
104
|
+
* - Performance optimizations (lazy loading by default)
|
|
105
|
+
* - Priority flag for LCP images
|
|
106
|
+
*
|
|
107
|
+
* @param props - Image props extending Next.js Image
|
|
108
|
+
* @param props.aspectRatio - Aspect ratio for layout stability and blur placeholder
|
|
109
|
+
* @param props.mobileSize - Size on mobile (e.g., "100vw")
|
|
110
|
+
* @param props.desktopSize - Size on desktop (e.g., "50vw")
|
|
111
|
+
* @param props.block - Display as block element
|
|
112
|
+
* @param props.priority - Prioritize loading for LCP images
|
|
113
|
+
*
|
|
114
|
+
* @example
|
|
115
|
+
* ```tsx
|
|
116
|
+
* // Basic usage with aspect ratio
|
|
117
|
+
* <Image
|
|
118
|
+
* src="/hero.jpg"
|
|
119
|
+
* alt="Hero image"
|
|
120
|
+
* aspectRatio={16/9}
|
|
121
|
+
* />
|
|
122
|
+
* ```
|
|
123
|
+
*
|
|
124
|
+
* @example
|
|
125
|
+
* ```tsx
|
|
126
|
+
* // LCP image with priority
|
|
127
|
+
* <Image
|
|
128
|
+
* src="/hero.jpg"
|
|
129
|
+
* alt="Hero image"
|
|
130
|
+
* aspectRatio={16/9}
|
|
131
|
+
* priority // Preloads image for LCP
|
|
132
|
+
* />
|
|
133
|
+
* ```
|
|
134
|
+
*
|
|
135
|
+
* @example
|
|
136
|
+
* ```tsx
|
|
137
|
+
* // Responsive grid image
|
|
138
|
+
* <Image
|
|
139
|
+
* src="/product.jpg"
|
|
140
|
+
* alt="Product"
|
|
141
|
+
* aspectRatio={1}
|
|
142
|
+
* mobileSize="100vw"
|
|
143
|
+
* desktopSize="33vw"
|
|
144
|
+
* />
|
|
145
|
+
* ```
|
|
146
|
+
*/
|
|
147
|
+
export function Image({
|
|
148
|
+
style,
|
|
149
|
+
className,
|
|
150
|
+
objectFit = "cover",
|
|
151
|
+
quality = 90,
|
|
152
|
+
alt = "",
|
|
153
|
+
fill,
|
|
154
|
+
block = !fill,
|
|
155
|
+
width = block ? 1 : undefined,
|
|
156
|
+
height = block ? 1 : undefined,
|
|
157
|
+
mobileSize = "100vw",
|
|
158
|
+
desktopSize = "100vw",
|
|
159
|
+
sizes,
|
|
160
|
+
src,
|
|
161
|
+
unoptimized,
|
|
162
|
+
ref,
|
|
163
|
+
aspectRatio,
|
|
164
|
+
placeholder = "blur",
|
|
165
|
+
priority = false,
|
|
166
|
+
...props
|
|
167
|
+
}: ImageProps) {
|
|
168
|
+
// Generate responsive sizes if not provided
|
|
169
|
+
const finalSizes =
|
|
170
|
+
sizes ||
|
|
171
|
+
`(max-width: ${breakpoints.desktop}px) ${mobileSize}, ${desktopSize}`
|
|
172
|
+
|
|
173
|
+
// Early return after hooks
|
|
174
|
+
if (!src) return null
|
|
175
|
+
|
|
176
|
+
// Determine SVG status and placeholder logic
|
|
177
|
+
const isSvg = typeof src === "string" && src.includes(".svg")
|
|
178
|
+
const shouldUsePlaceholder = shouldUseBlurPlaceholder(
|
|
179
|
+
src,
|
|
180
|
+
placeholder,
|
|
181
|
+
props.blurDataURL
|
|
182
|
+
)
|
|
183
|
+
const blurDataURL = generateBlurDataURL(
|
|
184
|
+
shouldUsePlaceholder,
|
|
185
|
+
aspectRatio,
|
|
186
|
+
props.blurDataURL
|
|
187
|
+
)
|
|
188
|
+
const finalPlaceholder = getFinalPlaceholder(
|
|
189
|
+
shouldUsePlaceholder,
|
|
190
|
+
aspectRatio,
|
|
191
|
+
props.blurDataURL,
|
|
192
|
+
placeholder
|
|
193
|
+
)
|
|
194
|
+
|
|
195
|
+
return (
|
|
196
|
+
<NextImage
|
|
197
|
+
ref={ref}
|
|
198
|
+
fill={!block}
|
|
199
|
+
{...(width !== undefined && { width })}
|
|
200
|
+
{...(height !== undefined && { height })}
|
|
201
|
+
priority={priority}
|
|
202
|
+
quality={quality}
|
|
203
|
+
alt={alt}
|
|
204
|
+
style={{
|
|
205
|
+
objectFit,
|
|
206
|
+
...style,
|
|
207
|
+
}}
|
|
208
|
+
className={cn(className, block && s.block)}
|
|
209
|
+
sizes={finalSizes}
|
|
210
|
+
src={src}
|
|
211
|
+
unoptimized={unoptimized || isSvg}
|
|
212
|
+
draggable={false}
|
|
213
|
+
onDragStart={(e) => e.preventDefault()}
|
|
214
|
+
{...(finalPlaceholder && { placeholder: finalPlaceholder })}
|
|
215
|
+
{...(blurDataURL && { blurDataURL })}
|
|
216
|
+
{...props}
|
|
217
|
+
/>
|
|
218
|
+
)
|
|
219
|
+
}
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import NextLink from "next/link";
|
|
4
|
+
import { usePathname } from "next/navigation";
|
|
5
|
+
import {
|
|
6
|
+
type AnchorHTMLAttributes,
|
|
7
|
+
type ComponentProps,
|
|
8
|
+
type MouseEvent,
|
|
9
|
+
useEffect,
|
|
10
|
+
useState,
|
|
11
|
+
} from "react";
|
|
12
|
+
|
|
13
|
+
// Helper to extract props safe for button elements
|
|
14
|
+
function getButtonProps(props: Record<string, unknown>) {
|
|
15
|
+
const {
|
|
16
|
+
href,
|
|
17
|
+
target,
|
|
18
|
+
rel,
|
|
19
|
+
"data-external": _dataExternal,
|
|
20
|
+
...buttonProps
|
|
21
|
+
} = props;
|
|
22
|
+
return buttonProps;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// Helper to extract props safe for div elements
|
|
26
|
+
function getDivProps(props: Record<string, unknown>) {
|
|
27
|
+
const {
|
|
28
|
+
href,
|
|
29
|
+
target,
|
|
30
|
+
rel,
|
|
31
|
+
onClick,
|
|
32
|
+
"data-external": _dataExternal,
|
|
33
|
+
...divProps
|
|
34
|
+
} = props;
|
|
35
|
+
return divProps;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
type CustomLinkProps = Omit<
|
|
39
|
+
AnchorHTMLAttributes<HTMLAnchorElement>,
|
|
40
|
+
keyof ComponentProps<typeof NextLink> | "href"
|
|
41
|
+
> &
|
|
42
|
+
Omit<ComponentProps<typeof NextLink>, "href"> & {
|
|
43
|
+
href?: string;
|
|
44
|
+
onClick?: (e: MouseEvent<HTMLElement>) => void;
|
|
45
|
+
scroll?: boolean;
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
export function Link({
|
|
49
|
+
href,
|
|
50
|
+
children,
|
|
51
|
+
onClick,
|
|
52
|
+
scroll = false, // Default to false to prevent scroll restoration warnings with fixed/sticky elements
|
|
53
|
+
...props
|
|
54
|
+
}: CustomLinkProps) {
|
|
55
|
+
const [shouldPrefetch, setShouldPrefetch] = useState(false);
|
|
56
|
+
const [isExternal, setIsExternal] = useState(false);
|
|
57
|
+
const [isActive, setIsActive] = useState(false);
|
|
58
|
+
|
|
59
|
+
// Get pathname - deferred to avoid blocking static generation
|
|
60
|
+
// usePathname is safe to call but we defer the active check to useEffect
|
|
61
|
+
const pathname = usePathname();
|
|
62
|
+
|
|
63
|
+
useEffect(() => {
|
|
64
|
+
// Check if this link is active (current page)
|
|
65
|
+
if (href && pathname) {
|
|
66
|
+
setIsActive(pathname === href);
|
|
67
|
+
}
|
|
68
|
+
}, [href, pathname]);
|
|
69
|
+
|
|
70
|
+
useEffect(() => {
|
|
71
|
+
// Skip if no href
|
|
72
|
+
if (!href) return;
|
|
73
|
+
|
|
74
|
+
// Check if external link
|
|
75
|
+
try {
|
|
76
|
+
const url = new URL(href, window.location.href);
|
|
77
|
+
setIsExternal(url.host !== window.location.host);
|
|
78
|
+
} catch {
|
|
79
|
+
setIsExternal(false);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// Only prefetch on good connections
|
|
83
|
+
const connection = (
|
|
84
|
+
navigator as Navigator & {
|
|
85
|
+
connection?: { effectiveType: string; saveData: boolean };
|
|
86
|
+
}
|
|
87
|
+
).connection;
|
|
88
|
+
if (connection) {
|
|
89
|
+
const { effectiveType, saveData } = connection;
|
|
90
|
+
setShouldPrefetch(effectiveType === "4g" && !saveData);
|
|
91
|
+
} else {
|
|
92
|
+
// Default to prefetching if API not available
|
|
93
|
+
setShouldPrefetch(true);
|
|
94
|
+
}
|
|
95
|
+
}, [href]);
|
|
96
|
+
|
|
97
|
+
// If no href is provided but there's an onClick, render a button
|
|
98
|
+
if (!href && onClick) {
|
|
99
|
+
return (
|
|
100
|
+
<button
|
|
101
|
+
onClick={(e: MouseEvent<HTMLButtonElement>) => onClick(e)}
|
|
102
|
+
type="button"
|
|
103
|
+
{...getButtonProps(props)}
|
|
104
|
+
>
|
|
105
|
+
{children}
|
|
106
|
+
</button>
|
|
107
|
+
);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// If no href and no onClick, render a div
|
|
111
|
+
if (!href) {
|
|
112
|
+
return <div {...getDivProps(props)}>{children}</div>;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// Block dangerous URIs (javascript:, data:, vbscript:)
|
|
116
|
+
const isDangerousHref = /^(javascript|data|vbscript):/i.test(href);
|
|
117
|
+
if (isDangerousHref) {
|
|
118
|
+
return <span {...getDivProps(props)}>{children}</span>;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// For SSR, check if it's external based on the href pattern
|
|
122
|
+
const isExternalSSR =
|
|
123
|
+
href.startsWith("http://") || href.startsWith("https://");
|
|
124
|
+
|
|
125
|
+
if (isExternalSSR || isExternal) {
|
|
126
|
+
return (
|
|
127
|
+
<a
|
|
128
|
+
href={href}
|
|
129
|
+
target="_blank"
|
|
130
|
+
rel="noopener noreferrer"
|
|
131
|
+
data-external
|
|
132
|
+
onClick={onClick}
|
|
133
|
+
{...props}
|
|
134
|
+
>
|
|
135
|
+
{children}
|
|
136
|
+
</a>
|
|
137
|
+
);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
return (
|
|
141
|
+
<NextLink
|
|
142
|
+
href={href as ComponentProps<typeof NextLink>["href"]}
|
|
143
|
+
prefetch={shouldPrefetch}
|
|
144
|
+
scroll={scroll}
|
|
145
|
+
data-active={isActive || undefined}
|
|
146
|
+
{...(onClick && { onClick })}
|
|
147
|
+
{...props}
|
|
148
|
+
>
|
|
149
|
+
{children}
|
|
150
|
+
</NextLink>
|
|
151
|
+
);
|
|
152
|
+
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { Metadata } from "next"
|
|
1
|
+
import type { Metadata } from "next";
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
4
|
* Metadata Generation Utilities
|
|
@@ -8,26 +8,26 @@ import type { Metadata } from "next"
|
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
10
|
interface GenerateMetadataOptions {
|
|
11
|
-
title?: string
|
|
12
|
-
description?: string
|
|
13
|
-
keywords?: string[]
|
|
11
|
+
title?: string;
|
|
12
|
+
description?: string;
|
|
13
|
+
keywords?: string[];
|
|
14
14
|
image?: {
|
|
15
|
-
url?: string
|
|
16
|
-
width?: number
|
|
17
|
-
height?: number
|
|
18
|
-
alt?: string
|
|
19
|
-
}
|
|
20
|
-
url?: string
|
|
21
|
-
siteName?: string
|
|
22
|
-
noIndex?: boolean
|
|
23
|
-
type?: "website" | "article"
|
|
24
|
-
publishedTime?: string
|
|
25
|
-
modifiedTime?: string
|
|
26
|
-
authors?: string[]
|
|
15
|
+
url?: string;
|
|
16
|
+
width?: number;
|
|
17
|
+
height?: number;
|
|
18
|
+
alt?: string;
|
|
19
|
+
};
|
|
20
|
+
url?: string;
|
|
21
|
+
siteName?: string;
|
|
22
|
+
noIndex?: boolean;
|
|
23
|
+
type?: "website" | "article";
|
|
24
|
+
publishedTime?: string;
|
|
25
|
+
modifiedTime?: string;
|
|
26
|
+
authors?: string[];
|
|
27
27
|
}
|
|
28
28
|
|
|
29
29
|
const APP_BASE_URL =
|
|
30
|
-
process.env.NEXT_PUBLIC_BASE_URL ?? "
|
|
30
|
+
process.env.NEXT_PUBLIC_BASE_URL ?? "http://localhost:3000";
|
|
31
31
|
|
|
32
32
|
/**
|
|
33
33
|
* Generate complete metadata object for pages
|
|
@@ -62,13 +62,13 @@ export function generatePageMetadata(
|
|
|
62
62
|
publishedTime,
|
|
63
63
|
modifiedTime,
|
|
64
64
|
authors,
|
|
65
|
-
} = options
|
|
65
|
+
} = options;
|
|
66
66
|
|
|
67
|
-
const fullUrl = url ? `${APP_BASE_URL}${url}` : APP_BASE_URL
|
|
68
|
-
const imageUrl = image?.url || "/opengraph-image.jpg"
|
|
69
|
-
const imageWidth = image?.width || 1200
|
|
70
|
-
const imageHeight = image?.height || 630
|
|
71
|
-
const imageAlt = image?.alt || title || siteName
|
|
67
|
+
const fullUrl = url ? `${APP_BASE_URL}${url}` : APP_BASE_URL;
|
|
68
|
+
const imageUrl = image?.url || "/opengraph-image.jpg";
|
|
69
|
+
const imageWidth = image?.width || 1200;
|
|
70
|
+
const imageHeight = image?.height || 630;
|
|
71
|
+
const imageAlt = image?.alt || title || siteName;
|
|
72
72
|
|
|
73
73
|
const metadata: Metadata = {
|
|
74
74
|
metadataBase: new URL(APP_BASE_URL),
|
|
@@ -113,14 +113,14 @@ export function generatePageMetadata(
|
|
|
113
113
|
other: {
|
|
114
114
|
"fb:app_id": process.env.NEXT_PUBLIC_FACEBOOK_APP_ID || "",
|
|
115
115
|
},
|
|
116
|
-
}
|
|
116
|
+
};
|
|
117
117
|
|
|
118
118
|
if (noIndex) {
|
|
119
119
|
metadata.robots = {
|
|
120
120
|
index: false,
|
|
121
121
|
follow: false,
|
|
122
|
-
}
|
|
122
|
+
};
|
|
123
123
|
}
|
|
124
124
|
|
|
125
|
-
return metadata
|
|
125
|
+
return metadata;
|
|
126
126
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
{
|
|
2
|
-
"name": "
|
|
2
|
+
"name": "bsmnt-next-starter",
|
|
3
3
|
"description": "Basement Next.js starter template",
|
|
4
4
|
"version": "0.1.0",
|
|
5
5
|
"private": true,
|
|
@@ -26,7 +26,7 @@
|
|
|
26
26
|
},
|
|
27
27
|
"dependencies": {
|
|
28
28
|
"class-variance-authority": "^0.7.1",
|
|
29
|
-
"next": "16.1.
|
|
29
|
+
"next": "16.1.6",
|
|
30
30
|
"react": "19.2.4",
|
|
31
31
|
"react-dom": "19.2.4",
|
|
32
32
|
"react-use": "^17.6.0",
|
|
@@ -35,10 +35,10 @@
|
|
|
35
35
|
"zustand": "^5.0.10"
|
|
36
36
|
},
|
|
37
37
|
"devDependencies": {
|
|
38
|
-
"@biomejs/biome": "2.3.
|
|
38
|
+
"@biomejs/biome": "2.3.14",
|
|
39
39
|
"@clack/prompts": "^0.11.0",
|
|
40
40
|
"@csstools/postcss-global-data": "^3.1.0",
|
|
41
|
-
"@next/bundle-analyzer": "16.1.
|
|
41
|
+
"@next/bundle-analyzer": "16.1.6",
|
|
42
42
|
"@svgr/webpack": "^8.1.0",
|
|
43
43
|
"@tailwindcss/postcss": "^4.1.18",
|
|
44
44
|
"@types/bun": "^1.3.6",
|
|
Binary file
|
|
@@ -1,23 +1,23 @@
|
|
|
1
|
-
import type { Metadata, Viewport } from "next"
|
|
2
|
-
import { Geist } from "next/font/google"
|
|
3
|
-
import { type PropsWithChildren, Suspense } from "react"
|
|
4
|
-
import { Link } from "@/components/ui/link"
|
|
5
|
-
import { themes } from "@/lib/styles/colors"
|
|
6
|
-
import { fontsVariable } from "@/lib/styles/fonts"
|
|
7
|
-
import AppData from "@/package.json"
|
|
8
|
-
import "@/lib/styles/css/index.css"
|
|
9
|
-
import { cn } from "@/lib/styles/cn"
|
|
1
|
+
import type { Metadata, Viewport } from "next";
|
|
2
|
+
import { Geist } from "next/font/google";
|
|
3
|
+
import { type PropsWithChildren, Suspense } from "react";
|
|
4
|
+
import { Link } from "@/components/ui/link";
|
|
5
|
+
import { themes } from "@/lib/styles/colors";
|
|
6
|
+
import { fontsVariable } from "@/lib/styles/fonts";
|
|
7
|
+
import AppData from "@/package.json";
|
|
8
|
+
import "@/lib/styles/css/index.css";
|
|
9
|
+
import { cn } from "@/lib/styles/cn";
|
|
10
10
|
|
|
11
|
-
const APP_NAME = AppData.name
|
|
12
|
-
const APP_DEFAULT_TITLE = "Basement Starter"
|
|
13
|
-
const APP_TITLE_TEMPLATE = "%s - Basement Starter"
|
|
14
|
-
const APP_DESCRIPTION = AppData.description
|
|
11
|
+
const APP_NAME = AppData.name;
|
|
12
|
+
const APP_DEFAULT_TITLE = "Basement Starter";
|
|
13
|
+
const APP_TITLE_TEMPLATE = "%s - Basement Starter";
|
|
14
|
+
const APP_DESCRIPTION = AppData.description;
|
|
15
15
|
const APP_BASE_URL =
|
|
16
|
-
process.env.NEXT_PUBLIC_BASE_URL ?? "
|
|
16
|
+
process.env.NEXT_PUBLIC_BASE_URL ?? "http://localhost:3000";
|
|
17
17
|
|
|
18
18
|
const geist = Geist({
|
|
19
19
|
subsets: ["latin"],
|
|
20
|
-
})
|
|
20
|
+
});
|
|
21
21
|
|
|
22
22
|
export const metadata: Metadata = {
|
|
23
23
|
alternates: {
|
|
@@ -70,12 +70,12 @@ export const metadata: Metadata = {
|
|
|
70
70
|
template: APP_TITLE_TEMPLATE,
|
|
71
71
|
},
|
|
72
72
|
},
|
|
73
|
-
}
|
|
73
|
+
};
|
|
74
74
|
|
|
75
75
|
export const viewport: Viewport = {
|
|
76
76
|
colorScheme: "normal",
|
|
77
77
|
themeColor: themes.dark.primary,
|
|
78
|
-
}
|
|
78
|
+
};
|
|
79
79
|
|
|
80
80
|
export default async function Layout({ children }: PropsWithChildren) {
|
|
81
81
|
return (
|
|
@@ -100,5 +100,5 @@ export default async function Layout({ children }: PropsWithChildren) {
|
|
|
100
100
|
{children}
|
|
101
101
|
</body>
|
|
102
102
|
</html>
|
|
103
|
-
)
|
|
103
|
+
);
|
|
104
104
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import type { MetadataRoute } from "next"
|
|
1
|
+
import type { MetadataRoute } from "next";
|
|
2
2
|
|
|
3
3
|
const APP_BASE_URL =
|
|
4
|
-
process.env.NEXT_PUBLIC_BASE_URL ?? "
|
|
4
|
+
process.env.NEXT_PUBLIC_BASE_URL ?? "http://localhost:3000";
|
|
5
5
|
|
|
6
6
|
export default function robots(): MetadataRoute.Robots {
|
|
7
7
|
return {
|
|
@@ -11,5 +11,5 @@ export default function robots(): MetadataRoute.Robots {
|
|
|
11
11
|
disallow: [],
|
|
12
12
|
},
|
|
13
13
|
sitemap: `${APP_BASE_URL}/sitemap.xml`,
|
|
14
|
-
}
|
|
14
|
+
};
|
|
15
15
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import type { MetadataRoute } from "next"
|
|
1
|
+
import type { MetadataRoute } from "next";
|
|
2
2
|
|
|
3
3
|
const APP_BASE_URL =
|
|
4
|
-
process.env.NEXT_PUBLIC_BASE_URL ?? "
|
|
4
|
+
process.env.NEXT_PUBLIC_BASE_URL ?? "http://localhost:3000";
|
|
5
5
|
|
|
6
6
|
export default async function sitemap(): Promise<MetadataRoute.Sitemap> {
|
|
7
7
|
const baseRoutes: MetadataRoute.Sitemap = [
|
|
@@ -11,6 +11,6 @@ export default async function sitemap(): Promise<MetadataRoute.Sitemap> {
|
|
|
11
11
|
changeFrequency: "daily",
|
|
12
12
|
priority: 1,
|
|
13
13
|
},
|
|
14
|
-
]
|
|
15
|
-
return baseRoutes
|
|
14
|
+
];
|
|
15
|
+
return baseRoutes;
|
|
16
16
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
{
|
|
2
|
-
"root":
|
|
2
|
+
"root": true,
|
|
3
3
|
"$schema": "node_modules/@biomejs/biome/configuration_schema.json",
|
|
4
4
|
"assist": {
|
|
5
5
|
"actions": {
|
|
@@ -245,6 +245,6 @@
|
|
|
245
245
|
"vcs": {
|
|
246
246
|
"clientKind": "git",
|
|
247
247
|
"enabled": true,
|
|
248
|
-
"useIgnoreFile":
|
|
248
|
+
"useIgnoreFile": false
|
|
249
249
|
}
|
|
250
250
|
}
|
|
@@ -3,20 +3,18 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Customize the Header and Footer components for your project needs.
|
|
5
5
|
*/
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
import
|
|
9
|
-
import {
|
|
10
|
-
import {
|
|
11
|
-
import { Theme } from "@/components/layout/theme"
|
|
12
|
-
import type { ThemeName } from "@/lib/styles/config"
|
|
6
|
+
import cn from "clsx";
|
|
7
|
+
import { Footer } from "@/components/layout/footer";
|
|
8
|
+
import { Header } from "@/components/layout/header";
|
|
9
|
+
import { Theme } from "@/components/layout/theme";
|
|
10
|
+
import type { ThemeName } from "@/lib/styles/config";
|
|
13
11
|
|
|
14
12
|
/**
|
|
15
13
|
* Props for the Wrapper component.
|
|
16
14
|
*/
|
|
17
15
|
interface WrapperProps extends React.HTMLAttributes<HTMLDivElement> {
|
|
18
16
|
/** Theme to apply ('dark' | 'light'). Defaults to 'dark'. */
|
|
19
|
-
theme?: ThemeName
|
|
17
|
+
theme?: ThemeName;
|
|
20
18
|
}
|
|
21
19
|
|
|
22
20
|
/**
|
|
@@ -61,5 +59,5 @@ export function Wrapper({
|
|
|
61
59
|
</main>
|
|
62
60
|
<Footer />
|
|
63
61
|
</Theme>
|
|
64
|
-
)
|
|
62
|
+
);
|
|
65
63
|
}
|